@xulthekl/team-flow 0.49.0 → 0.51.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/CHANGELOG.md +67 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +2 -2
- package/README.md +1 -1
- package/agents/architecture-design.md +1 -1
- package/agents/code-reviewer.md +1 -1
- package/docs/README_en.md +2 -2
- package/docs/artifact-contract.md +2 -0
- package/docs/decision-points.md +23 -0
- package/docs/state-machine.md +1 -1
- package/docs/usage-guide.md +7 -4
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/ensure-branch.mjs +58 -32
- package/scripts/guard/checks/tasks-complete.mjs +39 -3
- package/scripts/guard/checks/tasks-gate-exemptions.mjs +49 -0
- package/scripts/lib/arch-merge.mjs +32 -9
- package/scripts/lib/arch-precheck.mjs +190 -0
- package/scripts/lib/cmd-arch.mjs +5 -1
- package/scripts/lib/cmd-deisolate.mjs +15 -16
- package/scripts/lib/cmd-doctor.mjs +9 -2
- package/scripts/lib/cmd-publish.mjs +4 -7
- package/scripts/lib/cmd-state.mjs +2 -0
- package/scripts/lib/conventions-generator.mjs +9 -7
- package/scripts/lib/execution-plan.mjs +18 -3
- package/scripts/lib/git-utils.mjs +172 -14
- package/scripts/lib/glaf4-delegation.mjs +5 -2
- package/scripts/lib/severity.mjs +71 -0
- package/scripts/lib/solutions-capture.mjs +14 -0
- package/scripts/lib/solutions-index-gen.mjs +2 -2
- package/scripts/lib/solutions-inject.mjs +2 -3
- package/scripts/lib/solutions-promote.mjs +16 -8
- package/scripts/lib/state-loader.mjs +7 -0
- package/scripts/lint/rules/behavior-consistency.mjs +3 -1
- package/scripts/team-flow.mjs +8 -1
- package/skills/architecture-design/SKILL.md +17 -3
- package/skills/build-executor/SKILL.md +2 -2
- package/skills/build-executor/references/execution-modes.md +1 -1
- package/skills/build-executor/task-reviewer-prompt.md +5 -4
- package/skills/ce-compound/references/promotion-rules.md +9 -2
- package/skills/ce-compound/references/three-tier-index.md +1 -1
- package/skills/ce-compound/references/write-flow.md +1 -1
- package/skills/code-reviewer/SKILL.md +1 -1
- package/skills/code-reviewer/code-reviewer-prompt.md +5 -4
- package/skills/contract-builder/SKILL.md +35 -4
- package/skills/release-archivist/SKILL.md +14 -3
- package/skills/release-archivist/references/closing-procedures.md +1 -1
- package/skills/release-archivist/references/worktree-merge.md +1 -1
- package/skills/workflow-start/SKILL.md +18 -7
- package/skills/workflow-start/references/routing-rules.md +3 -1
- package/templates/learnings.md +2 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// scripts/guard/checks/tasks-gate-exemptions.mjs — v0.22 §85 tasks-complete 门禁豁免
|
|
2
|
+
//
|
|
3
|
+
// 背景(workflow-feedback 20260909-103500-guard):
|
|
4
|
+
// hotfix/tweak 跳过 spec-writer(workflow-start SKILL.md:172/173),tasks.md 的任务文本
|
|
5
|
+
// 无原生产出者;而 guard 的 executing:closing 仍考核 tasks-complete(guard.mjs:75/82)。
|
|
6
|
+
// 修复前该矛盾导致 closing 死锁,只能由编排层补录 tasks.md 绕过。
|
|
7
|
+
//
|
|
8
|
+
// 本模块与 test-gate-exemptions.mjs 同构:显式跳过优于静默豁免(v0.13 §48 原则),
|
|
9
|
+
// 跳过必须附理由才放行;存量 change(无 schema_version)沿用 legacy 豁免。
|
|
10
|
+
import { isLegacyChange } from './test-gate-exemptions.mjs';
|
|
11
|
+
|
|
12
|
+
export { isLegacyChange };
|
|
13
|
+
|
|
14
|
+
/** v0.22 §85:显式跳过 tasks.md 且理由非空(可审计豁免)。 */
|
|
15
|
+
export function hasExplicitTasksSkip(state) {
|
|
16
|
+
return state?.tasks_skipped === 'true'
|
|
17
|
+
&& typeof state?.tasks_skip_reason === 'string'
|
|
18
|
+
&& state.tasks_skip_reason.trim().length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** skip 已置但理由缺失 → 门禁拒绝并引导补理由。 */
|
|
22
|
+
export function tasksSkipMissingReason(state) {
|
|
23
|
+
return state?.tasks_skipped === 'true' && !hasExplicitTasksSkip(state);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* v0.22 §85.4(LT 2026-09-09 决策):`tasks_skipped` 仅对 hotfix/tweak 合法。
|
|
28
|
+
* full 的 tasks.md 是 spec-writer 核心产物,必然产出——缺失即流程异常,应 FAIL 暴露而非豁免掩盖。
|
|
29
|
+
* `auto`/缺失归一化为 full(与 cmd-state.mjs:163 的 auto→full 归一化同口径,安全侧默认)。
|
|
30
|
+
* 无存量风险:tasks_skipped 是 v0.50.0 新字段。
|
|
31
|
+
*/
|
|
32
|
+
export function isFullWorkflow(state) {
|
|
33
|
+
const workflow = state?.workflow;
|
|
34
|
+
return workflow == null || workflow === '' || workflow === 'auto' || workflow === 'full';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** full/auto 置了 skip → 拒绝,并给出两条可操作出路(不是死锁)。 */
|
|
38
|
+
export function fullWorkflowSkipViolation(state) {
|
|
39
|
+
return state?.tasks_skipped === 'true' && isFullWorkflow(state);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const TASKS_SKIP_REASON_HINT =
|
|
43
|
+
'tasks_skipped=true requires tasks_skip_reason — record it: '
|
|
44
|
+
+ "tf state set <dir> tasks_skip_reason '<why this change needs no tasks.md>'";
|
|
45
|
+
|
|
46
|
+
export const TASKS_SKIP_FULL_FORBIDDEN_HINT =
|
|
47
|
+
'tasks_skipped is only valid for hotfix/tweak — full workflow must produce tasks.md. '
|
|
48
|
+
+ 'Either produce tasks.md, or, if this change is truly a hotfix/tweak, '
|
|
49
|
+
+ 'declare the mode first: tf state set <dir> workflow <hotfix|tweak>';
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
import {
|
|
19
19
|
readFileSync, writeFileSync as fsWriteFileSync, existsSync, mkdirSync, readdirSync, rmdirSync, cpSync,
|
|
20
20
|
} from 'node:fs';
|
|
21
|
-
import { join, basename, dirname, resolve } from 'node:path';
|
|
21
|
+
import { join, basename, dirname, relative, resolve } from 'node:path';
|
|
22
22
|
import { execFileSync } from 'node:child_process';
|
|
23
23
|
import { pathToFileURL } from 'node:url';
|
|
24
24
|
import {
|
|
25
25
|
parseTableAfter, extractEndpoints, extractAggregates,
|
|
26
26
|
extractTablesFromSql, extractTablesFromDatabaseMd, readFrontmatter,
|
|
27
27
|
} from './arch-parse.mjs';
|
|
28
|
-
import { detectWorkspaceRoot, getGitRoot } from './git-utils.mjs';
|
|
28
|
+
import { detectWorkspaceRoot, getGitRoot, parsePorcelainPaths } from './git-utils.mjs';
|
|
29
29
|
|
|
30
30
|
const MARKER_BEGIN = '<!-- arch:current-state:begin -->';
|
|
31
31
|
const MARKER_END = '<!-- arch:current-state:end -->';
|
|
@@ -420,8 +420,29 @@ function gitCommit(projectRoot, globalArchDir, touchedFiles, changeName, dryRun)
|
|
|
420
420
|
for (const f of touchedFiles) {
|
|
421
421
|
execFileSync('git', ['add', f], { cwd: projectRoot, stdio: 'pipe' });
|
|
422
422
|
}
|
|
423
|
-
|
|
424
|
-
|
|
423
|
+
// v0.23 §93.3.2:commit 必须带 pathspec——裸 `git commit` 提交索引中**全部已暂存内容**,
|
|
424
|
+
// 索引里若有他人/前序工具(如 tf publish --arch)留下的 staged 文件会被一并卷走。
|
|
425
|
+
execFileSync('git', ['commit', '-m', commitMsg, '--', ...touchedFiles], { cwd: projectRoot, stdio: 'pipe' });
|
|
426
|
+
|
|
427
|
+
// v0.23 §93.3.3:回读实际提交清单与白名单比对(第二道防线,不阻断——提交已完成)
|
|
428
|
+
// 前提:projectRoot 即 git 仓根(`git show --name-only` 输出仓根相对路径,此处按 projectRoot
|
|
429
|
+
// 还原为绝对路径)。projectRoot 取自 detectWorkspaceRoot(changeDir) || getGitRoot(changeDir),
|
|
430
|
+
// 实践中成立;显式传 --project-root 指向子目录时该前提不成立,会误报为白名单外(已知边界)。
|
|
431
|
+
const committed = String(execFileSync(
|
|
432
|
+
'git', ['show', '--name-only', '--format=', 'HEAD'],
|
|
433
|
+
{ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
434
|
+
)).split('\n').filter(Boolean).map(p => resolve(projectRoot, p));
|
|
435
|
+
const whitelist = new Set(touchedFiles.map(p => resolve(projectRoot, p)));
|
|
436
|
+
const unexpected = committed.filter(p => !whitelist.has(p));
|
|
437
|
+
if (unexpected.length > 0) {
|
|
438
|
+
console.warn(` [WARN] 本次 commit 含白名单外的文件(${unexpected.length} 个):${unexpected.map(p => relative(projectRoot, p)).join(', ')}`);
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
committed: true,
|
|
442
|
+
message: commitMsg,
|
|
443
|
+
files: committed.map(p => relative(projectRoot, p)),
|
|
444
|
+
unexpected,
|
|
445
|
+
};
|
|
425
446
|
} catch (e) {
|
|
426
447
|
return { committed: false, error: e.message };
|
|
427
448
|
}
|
|
@@ -433,10 +454,11 @@ function detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles) {
|
|
|
433
454
|
const status = execFileSync('git', ['status', '--porcelain', '--', globalArchDir], {
|
|
434
455
|
cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
435
456
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
457
|
+
// v0.23 §93.3.1:改用共享解析器。原 `^\S+\s+` 无法剥离 ` M path`(行首空格 = 未暂存修改,
|
|
458
|
+
// 最常见形态)→ 路径残留状态码 → 本次 touch 的文件被误报为「非本次 touch 的脏文件」。
|
|
459
|
+
// 路径口径统一为「相对 projectRoot 的绝对路径」后再与白名单比对,不依赖 process.cwd()。
|
|
460
|
+
const touched = new Set(touchedFiles.map(p => resolve(projectRoot, p)));
|
|
461
|
+
return parsePorcelainPaths(status).filter(p => p && !touched.has(resolve(projectRoot, p)));
|
|
440
462
|
} catch {
|
|
441
463
|
return [];
|
|
442
464
|
}
|
|
@@ -533,7 +555,8 @@ export function run(args = {}) {
|
|
|
533
555
|
].filter(p => existsSync(p));
|
|
534
556
|
const dirty = detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles);
|
|
535
557
|
if (dirty.length > 0) {
|
|
536
|
-
console.warn(` [WARN] docs/architecture/
|
|
558
|
+
console.warn(` [WARN] docs/architecture/ 检测到 ${dirty.length} 个非本次 touch 的改动:${dirty.slice(0, 5).join(', ')}${dirty.length > 5 ? ' …' : ''}`);
|
|
559
|
+
console.warn(` 本次提交范围以白名单为准(${touchedFiles.length} 个文件);上述文件不会被提交,请确认是否他人未提交工作。`);
|
|
537
560
|
}
|
|
538
561
|
const commitResult = gitCommit(projectRoot, globalArchDir, touchedFiles, changeName, dryRun);
|
|
539
562
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// scripts/lib/arch-precheck.mjs — tf arch precheck:架构判断门的确定性证据工具(v0.22 §88.3.2)
|
|
2
|
+
//
|
|
3
|
+
// 定位:**证据工具,不是判断者**。本命令只输出"架构信号命中情况",
|
|
4
|
+
// 判断仍归 architecture-design 子代理(v0.9 §26 的独立性不变)。
|
|
5
|
+
// 退出码恒为 0 —— 不阻断流程,也不替代判断。
|
|
6
|
+
//
|
|
7
|
+
// 动机(workflow-feedback 20260909-112755):hotfix 对表现层文案改动跑完整架构门
|
|
8
|
+
// 消耗 41,864 tk,结论必然 skipped。让子代理在 signal=none 时只读 brief + 本命令输出
|
|
9
|
+
// (不读全量 docs/architecture/)可大幅降本,同时保留独立判断与回退能力。
|
|
10
|
+
//
|
|
11
|
+
// 设计约束(H1 实测校准):
|
|
12
|
+
// - 关键词须为**高置信度**短语——裸 `schema` / `api` 会误命中测试矩阵术语("schema 覆盖声明")
|
|
13
|
+
// - 否定表述须识别——"不动 filter.args" / "无聚合" 是**负向**证据,不能计为命中
|
|
14
|
+
// - 文件提取须去噪——`node_modules/` 引用非改动文件;同名文件按最完整路径去重
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
/** 架构关键词表(对应 architecture-design SKILL 五项检查 + 配置契约 key)。 */
|
|
19
|
+
const ARCH_KEYWORDS = [
|
|
20
|
+
// 1 聚合
|
|
21
|
+
'聚合', '聚合根', 'aggregate',
|
|
22
|
+
// 2 限界上下文
|
|
23
|
+
'限界上下文', 'bounded context',
|
|
24
|
+
// 3 读写模型
|
|
25
|
+
'读写模型', 'cqrs', '写模型', '读模型',
|
|
26
|
+
// 4 API(精确短语,避免裸 api 假阳性)
|
|
27
|
+
'api 签名', 'api 端点', 'api 变更', 'api 新增', '新增 api', 'api 契约', 'endpoint', '端点',
|
|
28
|
+
// 5 DB schema(精确短语,避免裸 schema 误命中测试矩阵术语)
|
|
29
|
+
'db schema', '数据库 schema', '表结构', 'ddl', '数据库表', '索引变更',
|
|
30
|
+
// 配置契约 key
|
|
31
|
+
'filter.args',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 否定上下文(行级):否定词可能出现在关键词**之后**同句(如"范围外…`filter.args` …一律不动")。
|
|
36
|
+
* 宽松匹配是刻意的——误判为 negated 只会让门走完整路径(安全),
|
|
37
|
+
* 误判为 positive 才会放行 fast path(危险)。
|
|
38
|
+
*/
|
|
39
|
+
const LINE_NEGATION_RE = /(不动|不涉及|不变更|不新增|不修改|不改变|不调整|不包含|严禁|不得|禁止|零变更|无变更|无新增|无修改|保持不变|unchanged|no change|without)/i;
|
|
40
|
+
const NEGATION_ADJACENT_RE = /(不|无|非|未|零|no|not)\s*$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 变更语境:关键词前文出现变更动词才计为**正向**信号。
|
|
44
|
+
* 仅"提到"关键词(如"按 filter.args 断言写回")属中性,不构成架构变更信号。
|
|
45
|
+
*/
|
|
46
|
+
const CHANGE_VERB_RE = /(新增|修改|变更|调整|增加|删除|引入|扩展|重构|改造|迁移|升级|add|new|modify|change|update|refactor|migrate)/i;
|
|
47
|
+
|
|
48
|
+
const PRESENTATION_EXTS = new Set(['.vue', '.jsx', '.tsx', '.less', '.css', '.scss', '.sass', '.html']);
|
|
49
|
+
const BACKEND_EXTS = new Set(['.java', '.go', '.py', '.rb', '.rs', '.kt', '.kts', '.cs']);
|
|
50
|
+
const CONFIG_EXTS = new Set(['.yaml', '.yml', '.json', '.sql', '.toml', '.ini', '.properties']);
|
|
51
|
+
const SCRIPT_EXTS = new Set(['.js', '.ts', '.mjs', '.cjs', '.mts', '.cts']);
|
|
52
|
+
const TEST_PATTERNS = [
|
|
53
|
+
/\.spec\.[cm]?[jt]sx?$/i,
|
|
54
|
+
/\.test\.[cm]?[jt]sx?$/i,
|
|
55
|
+
/Tests?\.java$/,
|
|
56
|
+
/_test\.go$/i,
|
|
57
|
+
/^test_.*\.py$/i,
|
|
58
|
+
/Tests?\.(kt|cs)$/,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const CODE_EXT_PATTERN = [...PRESENTATION_EXTS, ...BACKEND_EXTS, ...CONFIG_EXTS, ...SCRIPT_EXTS]
|
|
62
|
+
.map(e => e.slice(1))
|
|
63
|
+
.sort((a, b) => b.length - a.length)
|
|
64
|
+
.join('|');
|
|
65
|
+
|
|
66
|
+
const FILE_RE = new RegExp(`(?:[\\w.-]+/)*[\\w.-]+\\.(?:${CODE_EXT_PATTERN})(?![\\w])`, 'gi');
|
|
67
|
+
|
|
68
|
+
function classifyFile(file) {
|
|
69
|
+
const ext = path.extname(file).toLowerCase();
|
|
70
|
+
const base = path.basename(file);
|
|
71
|
+
if (TEST_PATTERNS.some(re => re.test(base))) return 'test';
|
|
72
|
+
if (PRESENTATION_EXTS.has(ext)) return 'presentation';
|
|
73
|
+
if (BACKEND_EXTS.has(ext)) return 'backend';
|
|
74
|
+
if (CONFIG_EXTS.has(ext)) return 'config';
|
|
75
|
+
if (SCRIPT_EXTS.has(ext)) return 'script';
|
|
76
|
+
return 'other';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 文件提取:剔除 node_modules 引用,同名文件保留最完整路径。 */
|
|
80
|
+
function extractFiles(text) {
|
|
81
|
+
const byBase = new Map();
|
|
82
|
+
for (const m of text.matchAll(FILE_RE)) {
|
|
83
|
+
const f = m[0];
|
|
84
|
+
if (/(^|\/)node_modules\//.test(f)) continue;
|
|
85
|
+
const base = path.basename(f);
|
|
86
|
+
const prev = byBase.get(base);
|
|
87
|
+
if (!prev || f.length > prev.length) byBase.set(base, f);
|
|
88
|
+
}
|
|
89
|
+
return [...byBase.values()];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 关键词逐次出现的语境统计:negated(否定)/ positive(变更动词)/ neutral(仅提及)。 */
|
|
93
|
+
function findKeyword(text, keyword) {
|
|
94
|
+
const kw = keyword.toLowerCase();
|
|
95
|
+
const stat = { occurrences: 0, positive: 0, negated: 0, neutral: 0 };
|
|
96
|
+
for (const line of text.split('\n')) {
|
|
97
|
+
const lowerLine = line.toLowerCase();
|
|
98
|
+
let idx = lowerLine.indexOf(kw);
|
|
99
|
+
while (idx !== -1) {
|
|
100
|
+
stat.occurrences += 1;
|
|
101
|
+
const before = line.slice(Math.max(0, idx - 30), idx);
|
|
102
|
+
if (LINE_NEGATION_RE.test(line) || NEGATION_ADJACENT_RE.test(before)) stat.negated += 1;
|
|
103
|
+
else if (CHANGE_VERB_RE.test(before)) stat.positive += 1;
|
|
104
|
+
else stat.neutral += 1;
|
|
105
|
+
idx = lowerLine.indexOf(kw, idx + kw.length);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return stat;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 扫描 change 的规划制品,返回架构信号证据。
|
|
113
|
+
* @returns {{ signal: 'none'|'weak'|'strong', hits: object, evidence: string[], sources: string[] }}
|
|
114
|
+
*/
|
|
115
|
+
export function collectArchSignals(changeDir) {
|
|
116
|
+
const sources = [];
|
|
117
|
+
for (const name of ['change-brief.md', 'execution-contract.md']) {
|
|
118
|
+
const p = path.join(changeDir, name);
|
|
119
|
+
if (fs.existsSync(p)) sources.push({ name, text: fs.readFileSync(p, 'utf-8') });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const text = sources.map(s => s.text).join('\n');
|
|
123
|
+
|
|
124
|
+
const keywords = [];
|
|
125
|
+
const negated = [];
|
|
126
|
+
const neutral = [];
|
|
127
|
+
for (const kw of ARCH_KEYWORDS) {
|
|
128
|
+
const stat = findKeyword(text, kw);
|
|
129
|
+
if (stat.occurrences === 0) continue;
|
|
130
|
+
if (stat.positive > 0) keywords.push(kw);
|
|
131
|
+
else if (stat.negated > 0) negated.push(kw);
|
|
132
|
+
else neutral.push(kw);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const files = { presentation: [], backend: [], config: [], script: [], test: [], other: [] };
|
|
136
|
+
for (const f of extractFiles(text)) files[classifyFile(f)].push(f);
|
|
137
|
+
|
|
138
|
+
const hasSql = files.config.some(f => f.toLowerCase().endsWith('.sql'));
|
|
139
|
+
|
|
140
|
+
let signal;
|
|
141
|
+
if (keywords.length > 0) signal = 'strong';
|
|
142
|
+
else if (files.backend.length > 0 || hasSql) signal = 'weak';
|
|
143
|
+
else if (files.presentation.length > 0) signal = 'none';
|
|
144
|
+
else signal = 'weak';
|
|
145
|
+
|
|
146
|
+
const evidence = [];
|
|
147
|
+
if (sources.length === 0) {
|
|
148
|
+
evidence.push('no change-brief.md / execution-contract.md found — signal defaults to weak');
|
|
149
|
+
}
|
|
150
|
+
evidence.push(keywords.length === 0
|
|
151
|
+
? 'no architecture keyword in change context'
|
|
152
|
+
: `architecture keyword in change context: ${keywords.join(', ')}`);
|
|
153
|
+
if (negated.length > 0) evidence.push(`negated (not a signal): ${negated.join(', ')}`);
|
|
154
|
+
if (neutral.length > 0) evidence.push(`mentioned without change context (not a signal): ${neutral.join(', ')}`);
|
|
155
|
+
evidence.push(
|
|
156
|
+
`code files — presentation=${files.presentation.length}, backend=${files.backend.length}, `
|
|
157
|
+
+ `config=${files.config.length}, script=${files.script.length}, test=${files.test.length}`,
|
|
158
|
+
);
|
|
159
|
+
if (signal === 'none') {
|
|
160
|
+
evidence.push('all evidence points to presentation-layer only — fast-path candidate (sub-agent must still confirm)');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
signal,
|
|
165
|
+
hits: { keywords, negatedKeywords: negated, neutralKeywords: neutral, files },
|
|
166
|
+
evidence,
|
|
167
|
+
sources: sources.map(s => s.name),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function run(positionals, values) {
|
|
172
|
+
const changeDirArg = positionals[0];
|
|
173
|
+
if (!changeDirArg) {
|
|
174
|
+
console.error('Usage: tf arch precheck <change-dir> [--json]');
|
|
175
|
+
process.exit(2);
|
|
176
|
+
}
|
|
177
|
+
const changeDir = path.resolve(changeDirArg);
|
|
178
|
+
const result = collectArchSignals(changeDir);
|
|
179
|
+
|
|
180
|
+
if (values.json) {
|
|
181
|
+
console.log(JSON.stringify(result, null, 2));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`arch precheck — signal: ${result.signal}`);
|
|
184
|
+
console.log(` sources: ${result.sources.join(', ') || '(none)'}`);
|
|
185
|
+
for (const line of result.evidence) console.log(` - ${line}`);
|
|
186
|
+
console.log(' note: evidence only — the architecture-design sub-agent still makes the decision.');
|
|
187
|
+
}
|
|
188
|
+
// 证据工具:恒以 0 退出,不阻断流程。
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
package/scripts/lib/cmd-arch.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
|
+
import * as archPrecheck from './arch-precheck.mjs';
|
|
9
10
|
|
|
10
11
|
const ARCH_STATE_FILE = '.team-flow/arch-state.json';
|
|
11
12
|
|
|
@@ -16,6 +17,7 @@ export async function run(args) {
|
|
|
16
17
|
'project-root': { type: 'string' },
|
|
17
18
|
mode: { type: 'string', default: 'reconstruction' },
|
|
18
19
|
'baseline-ref': { type: 'string', default: 'prd/vN/' },
|
|
20
|
+
json: { type: 'boolean', default: false },
|
|
19
21
|
},
|
|
20
22
|
allowPositionals: true,
|
|
21
23
|
});
|
|
@@ -23,7 +25,9 @@ export async function run(args) {
|
|
|
23
25
|
const sub = positionals[0];
|
|
24
26
|
if (sub === 'init') return init(values);
|
|
25
27
|
if (sub === 'show') return show(values);
|
|
26
|
-
|
|
28
|
+
// v0.22 §88.3.2:架构门判据的确定性证据工具(证据 only,退出码恒 0)
|
|
29
|
+
if (sub === 'precheck') return archPrecheck.run(positionals.slice(1), values);
|
|
30
|
+
console.error('Usage: tf arch init [--mode reconstruction|design] [--baseline-ref <prd/vN/>] | tf arch show | tf arch precheck <change-dir> [--json]');
|
|
27
31
|
process.exit(2);
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
// Case B 单仓库:<change-dir> 同级 ../<repo>-<change-name> 存在
|
|
16
16
|
|
|
17
17
|
import { execFileSync } from 'node:child_process';
|
|
18
|
-
import { existsSync,
|
|
18
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
19
19
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { parseArgs } from 'node:util';
|
|
21
|
-
import { detectWorkspaceRoot } from './git-utils.mjs';
|
|
21
|
+
import { detectWorkspaceRoot, listCodeReposDeep } from './git-utils.mjs';
|
|
22
22
|
|
|
23
23
|
export async function run(args) {
|
|
24
24
|
const { positionals, values } = parseArgs({
|
|
@@ -79,24 +79,23 @@ export async function run(args) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
// Case A: <root>/.worktrees/<change>/<repo>/
|
|
82
|
+
// Case A: <root>/.worktrees/<change>/<repo>/ 或 <root>/.worktrees/<change>/<container>/<repo>/
|
|
83
|
+
// v0.23 §92.3.7(P4 plugin-validator M1):复用两层探测——ensure-branch 现按 relPath 建 worktree,
|
|
84
|
+
// 嵌套布局(service/<repo>)在 <change>/ 下的直接子项是容器目录(无 .git),旧的一层扫描会漏检
|
|
85
|
+
// → deisolate 报 "No worktrees found",closing 阶段代码永远停在 worktree 分支。
|
|
83
86
|
function detectCaseA(root, changeName) {
|
|
84
87
|
const changeWorktreeDir = join(root, '.worktrees', changeName);
|
|
85
88
|
if (!existsSync(changeWorktreeDir)) return null;
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.map(e => ({
|
|
97
|
-
name: e.name,
|
|
98
|
-
worktreePath: join(changeWorktreeDir, e.name),
|
|
99
|
-
repoPath: join(root, e.name),
|
|
90
|
+
// excludeNonRepoDirs: false —— worktree 目录结构由 ensure-branch 按 config 权威清单创建,
|
|
91
|
+
// 仓名可能恰是 NON_REPO_DIRS 中的词(data / prototype / docs / …);沿用探测期排除表会漏检
|
|
92
|
+
// → 代码静默留在 worktree 分支(P4 plugin-validator 复审残留 M1)。
|
|
93
|
+
const repos = listCodeReposDeep(changeWorktreeDir, { excludeNonRepoDirs: false })
|
|
94
|
+
.map(repo => ({
|
|
95
|
+
// 用 relPath 作展示名:嵌套布局下可区分 service/a 与 service/b(扁平布局等价于目录名)
|
|
96
|
+
name: repo.relPath,
|
|
97
|
+
worktreePath: join(changeWorktreeDir, repo.relPath),
|
|
98
|
+
repoPath: join(root, repo.relPath),
|
|
100
99
|
layout: 'A',
|
|
101
100
|
}))
|
|
102
101
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -5,6 +5,8 @@ import { loadConfig } from './config-loader.mjs';
|
|
|
5
5
|
import { PLATFORM_RUNTIME_INVENTORY } from './platform-runtime-inventory.mjs';
|
|
6
6
|
// 非法 state 巡检所需的共享常量与读取器(来源:workflow-feedback 2026-08-01,#100004)。
|
|
7
7
|
import { readState, VALID_STATES } from './state-loader.mjs';
|
|
8
|
+
// v0.22 §85:tasks_skipped 豁免判定复用 guard 侧唯一真相源,避免同一规则两处实现漂移。
|
|
9
|
+
import { hasExplicitTasksSkip } from '../guard/checks/tasks-gate-exemptions.mjs';
|
|
8
10
|
|
|
9
11
|
const RUNTIME_SKILLS = new Set([
|
|
10
12
|
'workflow-start', 'need-explorer', 'spec-writer', 'contract-builder',
|
|
@@ -258,8 +260,8 @@ function checkChangeStates(root) {
|
|
|
258
260
|
return { pass: true, message: `${checked} change(s) have legal state values` };
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
// v0.13 §52 B4 测试门禁卫生巡检(C1-domain-policy
|
|
262
|
-
// 1. test_matrix_skipped=true
|
|
263
|
+
// v0.13 §52 B4 测试门禁卫生巡检(C1-domain-policy 事件修复);v0.22 §85 扩展 tasks_skipped:
|
|
264
|
+
// 1. test_matrix_skipped / tasks_skipped = true 必须附对应 reason(可审计豁免);
|
|
263
265
|
// 2. 状态引用的产物文件必须存在(arch_review_report / test_evidence_path)——
|
|
264
266
|
// C1 现场曾出现 arch_review_report 指向不存在的 auto-review.md。
|
|
265
267
|
// DP 时间戳顺序检查评估后未纳入:closing 后的 DP-7 时间戳晚于 last_transition 属合法,
|
|
@@ -283,6 +285,11 @@ function checkChangeTestGates(root) {
|
|
|
283
285
|
&& !(typeof state.test_matrix_skip_reason === 'string' && state.test_matrix_skip_reason.trim())) {
|
|
284
286
|
issues.push(`${d}: test_matrix_skipped=true without test_matrix_skip_reason`);
|
|
285
287
|
}
|
|
288
|
+
// v0.22 §85:tasks_skipped 沿用同一可审计豁免范式(跳过必须附理由),
|
|
289
|
+
// 判定复用 tasks-gate-exemptions 的共享谓词(与 guard 同一真相源)。
|
|
290
|
+
if (state.tasks_skipped === 'true' && !hasExplicitTasksSkip(state)) {
|
|
291
|
+
issues.push(`${d}: tasks_skipped=true without tasks_skip_reason`);
|
|
292
|
+
}
|
|
286
293
|
for (const field of ['arch_review_report', 'test_evidence_path']) {
|
|
287
294
|
const ref = state[field];
|
|
288
295
|
if (typeof ref === 'string' && ref.trim() && !existsSync(join(changeDir, ref))) {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import { existsSync, mkdirSync, readdirSync, rmdirSync } from 'node:fs';
|
|
21
21
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
22
|
-
import { detectWorkspaceRoot } from './git-utils.mjs';
|
|
22
|
+
import { detectWorkspaceRoot, parsePorcelainPaths } from './git-utils.mjs';
|
|
23
23
|
|
|
24
24
|
const LOCK_PATH = '.publish.lock';
|
|
25
25
|
|
|
@@ -126,12 +126,9 @@ function detectOutsideDirty(projectRoot, whitelist) {
|
|
|
126
126
|
try {
|
|
127
127
|
const status = git(projectRoot, 'status', '--porcelain');
|
|
128
128
|
if (!status) return [];
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const raw = line.slice(3).replace(/^"|"$/g, '').replace(/\\ /g, ' ');
|
|
133
|
-
return raw;
|
|
134
|
-
})
|
|
129
|
+
// v0.23 §93.3.1:解析统一走 git-utils.parsePorcelainPaths(本文件的 slice(3) 实现
|
|
130
|
+
// 是正确版本,已上移共享;arch-merge 原用的 `^\S+\s+` 正则会漏掉 ` M path`)。
|
|
131
|
+
return parsePorcelainPaths(status)
|
|
135
132
|
.filter(p => {
|
|
136
133
|
if (!p) return false;
|
|
137
134
|
const norm = p.replace(/\/+$/, ''); // porcelain 目录级输出带尾斜杠,先去规范化
|
|
@@ -38,6 +38,8 @@ const SETTABLE_FIELDS = [
|
|
|
38
38
|
'compound_skipped',
|
|
39
39
|
// Test matrix gate (v0.12 §45.4 + v0.13 §48.2)
|
|
40
40
|
'test_matrix_skipped', 'test_matrix_skip_reason',
|
|
41
|
+
// Tasks gate (v0.22 §85:hotfix/tweak 显式跳过 tasks.md,须附理由)
|
|
42
|
+
'tasks_skipped', 'tasks_skip_reason',
|
|
41
43
|
];
|
|
42
44
|
|
|
43
45
|
export async function run(args) {
|
|
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from
|
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import path, { join } from 'node:path';
|
|
15
15
|
import { parseArgs } from 'node:util';
|
|
16
|
+
import { listCodeReposDeep, NON_REPO_DIRS } from './git-utils.mjs';
|
|
16
17
|
|
|
17
18
|
// ── glaf4-dev 运行时探测(v2.1 §4 接口①)─────────────────────────────────────
|
|
18
19
|
|
|
@@ -109,11 +110,7 @@ export function writeGlaf4DevConfig(root, glaf4DevInfo) {
|
|
|
109
110
|
|
|
110
111
|
// ── repo_layout 检测(multi-repo-support-design v1.0 §3,2026-08-21)─────────────
|
|
111
112
|
|
|
112
|
-
//
|
|
113
|
-
const NON_REPO_DIRS = new Set([
|
|
114
|
-
'changes', '.team-flow', 'docs', 'doc', 'node_modules', '.worktrees',
|
|
115
|
-
'requirement', 'prototype', 'specs', 'data', 'path', 'template',
|
|
116
|
-
]);
|
|
113
|
+
// 非代码仓库目录集合统一在 git-utils.mjs 定义(v0.23 §92:唯一真相源),见顶部 import。
|
|
117
114
|
|
|
118
115
|
/**
|
|
119
116
|
* 识别项目布局(single / monorepo / multi-repo,v1.0 §3.1)
|
|
@@ -130,15 +127,20 @@ export function detectRepoLayout(root) {
|
|
|
130
127
|
return { mode: subModules.length > 0 ? 'monorepo' : 'single', repos };
|
|
131
128
|
}
|
|
132
129
|
// 2. 根无技术栈特征 → 扫描子目录识别独立代码仓库(含 .git 或有技术栈语言)
|
|
130
|
+
// v0.23 §92.3.3:含 .git 的仓库改用 listCodeReposDeep 探测,支持 `service/<repo>` 两层布局
|
|
131
|
+
//(原实现只扫直接子项 → emp-auth 12 仓被误判 single,config 靠人工修正)。
|
|
133
132
|
const repos = {};
|
|
133
|
+
for (const repo of listCodeReposDeep(root)) {
|
|
134
|
+
repos[repo.relPath] = repo.absPath;
|
|
135
|
+
}
|
|
134
136
|
const entries = readdirSync(root, { withFileTypes: true });
|
|
135
137
|
for (const entry of entries) {
|
|
136
138
|
if (!entry.isDirectory()) continue;
|
|
137
139
|
if (entry.name.startsWith('.') || NON_REPO_DIRS.has(entry.name)) continue;
|
|
140
|
+
if (repos[entry.name]) continue;
|
|
138
141
|
const sub = join(root, entry.name);
|
|
139
|
-
const hasGit = existsSync(join(sub, '.git'));
|
|
140
142
|
const tech = detectTechStack(sub);
|
|
141
|
-
if (
|
|
143
|
+
if (tech.language) {
|
|
142
144
|
repos[entry.name] = sub;
|
|
143
145
|
}
|
|
144
146
|
}
|
|
@@ -5,7 +5,8 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
5
5
|
import { computeArtifactsHash, computeContractHash, hashObject, stableJson } from './hash.mjs';
|
|
6
6
|
import { getOverlayPaths } from './sdd-overlay.mjs';
|
|
7
7
|
import { readState } from './state-loader.mjs';
|
|
8
|
-
import { detectWorkspaceRoot, findSubRepo } from './git-utils.mjs';
|
|
8
|
+
import { detectWorkspaceRoot, findSubRepo, resolveCodeRepos } from './git-utils.mjs';
|
|
9
|
+
import { loadConfig } from './config-loader.mjs';
|
|
9
10
|
|
|
10
11
|
// v2.1 §6.6:扩入 glaf4-delegation(GLAF4 change 整体委托 glaf4-dev 七模式流,S1 结构统一)
|
|
11
12
|
export const EXECUTION_MODES = ['inline', 'batch-inline', 'sdd', 'glaf4-delegation'];
|
|
@@ -334,8 +335,22 @@ function validateReviewRange(changeDir, base, head, repoPath) {
|
|
|
334
335
|
const workspaceRoot = detectWorkspaceRoot(changeDir);
|
|
335
336
|
if (!workspaceRoot) throw primaryError;
|
|
336
337
|
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
// v0.23 §92.3.4:传入 config(repo_layout 权威 + 两层探测)
|
|
339
|
+
const config = loadConfig(workspaceRoot);
|
|
340
|
+
const subRoot = findSubRepo(workspaceRoot, head, config);
|
|
341
|
+
if (!subRoot) {
|
|
342
|
+
// v0.23 §92.3.5:文案区分「SHA 不存在」与「不在已扫描仓中」——后者给出扫描清单与 --repo 指引
|
|
343
|
+
const { repos, unresolved, source } = resolveCodeRepos(workspaceRoot, config);
|
|
344
|
+
const scanned = repos.length ? repos.map(r => r.relPath).join(', ') : '(none)';
|
|
345
|
+
throw new Error(
|
|
346
|
+
`${primaryError.message}\n`
|
|
347
|
+
+ ` Scanned repos (source=${source}, ${repos.length}): ${scanned}\n`
|
|
348
|
+
+ (unresolved.length
|
|
349
|
+
? ` Unresolved repo_layout.repos entries (${unresolved.length}): ${unresolved.map(u => u.key).join(', ')}\n`
|
|
350
|
+
: '')
|
|
351
|
+
+ ' If the commit lives in a nested repo not listed above, pass --repo <abs-path>.',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
339
354
|
|
|
340
355
|
resolvedBase = resolveGitCommit(subRoot, base, 'base');
|
|
341
356
|
resolvedHead = resolveGitCommit(subRoot, head, 'head');
|