agentic-flow-kit 1.6.0 → 1.7.15
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/README.md +40 -6
- package/modules/gates/README.md +1 -0
- package/modules/gates/test-protect/README.md +23 -0
- package/modules/gates/test-protect/check-test-protect.mjs +64 -0
- package/modules/gates/test-protect/local-pre-commit.line +1 -0
- package/modules/hosts/README.md +57 -0
- package/modules/hosts/claude/commands/wf-build.md +5 -24
- package/modules/hosts/claude/commands/wf-deploy.md +1 -0
- package/modules/hosts/claude/commands/wf-design.md +2 -2
- package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/claude/commands/wf-test.md +5 -4
- package/modules/hosts/codex/commands/wf-build.md +5 -24
- package/modules/hosts/codex/commands/wf-deploy.md +1 -0
- package/modules/hosts/codex/commands/wf-design.md +2 -2
- package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/codex/commands/wf-test.md +5 -4
- package/modules/hosts/cursor/commands/wf-build.md +5 -24
- package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
- package/modules/hosts/cursor/commands/wf-design.md +2 -2
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/cursor/commands/wf-test.md +5 -4
- package/modules/hosts/opencode/commands/wf-build.md +5 -24
- package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/opencode/commands/wf-test.md +5 -4
- package/modules/hosts/trae/commands/wf-build.md +5 -24
- package/modules/hosts/trae/commands/wf-deploy.md +1 -0
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/trae/commands/wf-test.md +5 -4
- package/package.json +2 -2
- package/src/add-gate.mjs +25 -1
- package/src/add-gate.test.mjs +103 -0
- package/src/cli.mjs +17 -2
- package/src/doctor.mjs +99 -11
- package/src/gates.test.mjs +17 -0
- package/src/init.mjs +7 -1
- package/src/init.test.mjs +19 -0
- package/src/instance-ref.test.mjs +62 -0
- package/src/jscpd-drift.test.mjs +144 -0
- package/src/render.mjs +5 -0
- package/src/rule-budget.test.mjs +103 -0
- package/src/run-tests.mjs +16 -9
- package/src/sync-hosts.mjs +4 -2
- package/src/trae-hooks.test.mjs +15 -3
- package/src/validate.mjs +72 -0
- package/src/validate.test.mjs +95 -0
- package/templates/AGENTS.md +3 -1
- package/templates/_agents/commands/build.md +5 -24
- package/templates/_agents/commands/deploy.md +1 -0
- package/templates/_agents/commands/design.md +2 -2
- package/templates/_agents/commands/gate-checklist.md +1 -3
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -3
- package/templates/_agents/commands/sync-hosts.md +1 -3
- package/templates/_agents/commands/test.md +5 -4
- package/templates/_agents/control-surfaces.txt +53 -0
- package/templates/_agents/scripts/agg-delegations.cjs +51 -6
- package/templates/_agents/scripts/agg-delegations.test.mjs +79 -1
- package/templates/_agents/scripts/check-engine-integrity.mjs +288 -0
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +227 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +7 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +18 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +16 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +2 -0
- package/templates/_agents/scripts/check-loop.mjs +247 -5
- package/templates/_agents/scripts/check-loop.test.mjs +367 -2
- package/templates/_agents/scripts/confirm-doc.mjs +50 -3
- package/templates/_agents/scripts/confirm-doc.test.mjs +93 -1
- package/templates/_agents/scripts/doctor.test.mjs +251 -1
- package/templates/_agents/scripts/draft-sediment.mjs +178 -0
- package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
- package/templates/_agents/scripts/gate-checklist.mjs +10 -2
- package/templates/_agents/scripts/gate-checklist.test.mjs +7 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +2 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +11 -5
- package/templates/_agents/scripts/kb-search.mjs +87 -12
- package/templates/_agents/scripts/kb-search.test.mjs +83 -0
- package/templates/_agents/scripts/pipeline-run.mjs +1 -1
- package/templates/_agents/scripts/policy.mjs +49 -0
- package/templates/_agents/scripts/solidify-task.mjs +1 -1
- package/templates/_agents/scripts/source-sync-check.test.mjs +9 -4
- package/templates/_agents/scripts/stage-gates.mjs +48 -1
- package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
- package/templates/_agents/scripts/verify.mjs +12 -4
- package/templates/_agents/scripts/verify.test.mjs +30 -3
- package/templates/_agents/scripts-test-exempt.txt +3 -1
- package/templates/_agents/skills/wiki/SKILL.md +5 -3
- package/templates/wiki/INDEX.md +44 -1
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
- package/templates/workflow/delegations.md +5 -4
- package/templates/workflow/incidents/_TEMPLATE.md +2 -0
- package/templates/workflow/intents/_TEMPLATE.md +3 -0
- package/templates/workflow/plans/_TEMPLATE.md +4 -0
- package/templates/workflow/specs/_TEMPLATE.md +23 -0
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import { computeFingerprint } from './confirm-doc.mjs';
|
|
14
14
|
import { POLICIES, loadKitPolicy } from './policy.mjs';
|
|
15
15
|
import { entryConfirmed, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
16
|
-
import { parseAddedDatesLog, addedDatesWithCache } from './check-loop.mjs';
|
|
16
|
+
import { parseAddedDatesLog, addedDatesWithCache, scenarioIdsOf, extractThenByScenario, sectionHasBody, sedimentLandingIsShell } from './check-loop.mjs';
|
|
17
17
|
|
|
18
18
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
@@ -2940,6 +2940,61 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2940
2940
|
}
|
|
2941
2941
|
|
|
2942
2942
|
|
|
2943
|
+
// ---- 场景 23:台账 scope 子项(2026-10-09 审查 A1 干净版)——缺 scope 出账 / 摘要失配 hard / 存量与旧 policy 零告警 ----
|
|
2944
|
+
// 背景:委托代录的授权凭据原是「用户原话」(自由文本、AI 可零成本复用)。干净版改记机器派生的
|
|
2945
|
+
// scope{files,sha256}(AI 无输入面),本子项校验受管行带该字段且摘要自洽。
|
|
2946
|
+
{
|
|
2947
|
+
const AFTER = '2026-10-09T12:00:00Z'; // ≥ policy v7 scopeSince(生效时刻锚)
|
|
2948
|
+
const BEFORE = '2026-10-01T00:00:00Z'; // 存量(生效前):豁免、不追溯
|
|
2949
|
+
const { scopeDigest } = await import('./policy.mjs');
|
|
2950
|
+
|
|
2951
|
+
// 23a:受管行缺 scope → advisory 出账(灰度第一档,不阻断)
|
|
2952
|
+
const Ta = mkfix();
|
|
2953
|
+
w(Ta, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2954
|
+
writeLedger(Ta, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-a.md', stage: 'approved', fingerprint: 'a'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以' }]);
|
|
2955
|
+
const ra = run(Ta);
|
|
2956
|
+
check('场景 23a 缺机器改动面:受管行无 scope → WARN 出账且不阻断',
|
|
2957
|
+
ra.status === 0 && outOf(ra).includes('缺机器改动面'), `exit=${ra.status}\n${outOf(ra).slice(-400)}`);
|
|
2958
|
+
rmfix(Ta);
|
|
2959
|
+
|
|
2960
|
+
// 23b:scope 存在但摘要与 files 重算不符(「改了 files 没改摘要」)→ hard 阻断
|
|
2961
|
+
const Tb = mkfix();
|
|
2962
|
+
w(Tb, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2963
|
+
const good = scopeDigest(['workflow/intents/2026-10-09-b.md', 'src/x.mjs']);
|
|
2964
|
+
writeLedger(Tb, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-b.md', stage: 'approved', fingerprint: 'b'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以', scope: { ...good, files: [...good.files, 'src/悄悄加的.mjs'] } }]);
|
|
2965
|
+
const rb = run(Tb);
|
|
2966
|
+
check('场景 23b scope 摘要失配:files 被改未同步 sha256 → hard 阻断',
|
|
2967
|
+
rb.status !== 0 && outOf(rb).includes('台账 scope 失配'), `exit=${rb.status}\n${outOf(rb).slice(-400)}`);
|
|
2968
|
+
rmfix(Tb);
|
|
2969
|
+
|
|
2970
|
+
// 23c:scope 完整自洽 → 零 scope 告警(断言非恒红)
|
|
2971
|
+
const Tc = mkfix();
|
|
2972
|
+
w(Tc, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2973
|
+
writeLedger(Tc, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-c.md', stage: 'approved', fingerprint: 'c'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以', scope: scopeDigest(['workflow/intents/2026-10-09-c.md']) }]);
|
|
2974
|
+
const rc = run(Tc);
|
|
2975
|
+
check('场景 23c scope 自洽:零 scope 告警(断言非恒红)',
|
|
2976
|
+
!outOf(rc).includes('缺机器改动面') && !outOf(rc).includes('台账 scope 失配'), outOf(rc).slice(-300));
|
|
2977
|
+
rmfix(Tc);
|
|
2978
|
+
|
|
2979
|
+
// 23d:存量行(ts < scopeSince)缺 scope → 零告警(生效日锚不追溯,不产不可消除噪声)
|
|
2980
|
+
const Td = mkfix();
|
|
2981
|
+
w(Td, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2982
|
+
writeLedger(Td, [{ ts: BEFORE, doc: 'workflow/intents/2026-10-01-d.md', stage: 'approved', fingerprint: 'd'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以' }]);
|
|
2983
|
+
const rd = run(Td);
|
|
2984
|
+
check('场景 23d 存量豁免:ts < scopeSince 的行缺 scope → 零告警(不追溯)',
|
|
2985
|
+
!outOf(rd).includes('缺机器改动面'), outOf(rd).slice(-300));
|
|
2986
|
+
rmfix(Td);
|
|
2987
|
+
|
|
2988
|
+
// 23e:旧 policy(v6 无 scopeSince 键)→ 子项整体跳过,存量 406 行零新增告警
|
|
2989
|
+
const Te = mkfix();
|
|
2990
|
+
w(Te, '.agents/kit.json', '{"policyVersion":6}\n');
|
|
2991
|
+
writeLedger(Te, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-e.md', stage: 'approved', fingerprint: 'e'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以' }]);
|
|
2992
|
+
const re = run(Te);
|
|
2993
|
+
check('场景 23e 旧 policy 跳过:v6 无 scopeSince 键 → 子项整体跳过(装户零新增告警)',
|
|
2994
|
+
!outOf(re).includes('缺机器改动面') && !outOf(re).includes('台账 scope 失配'), outOf(re).slice(-300));
|
|
2995
|
+
rmfix(Te);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2943
2998
|
// ---- 场景 22:addedDates 持久缓存(2026-10-09 engine-quality-round3 W1)----
|
|
2944
2999
|
{
|
|
2945
3000
|
const log = '@2026-10-02T10:00:00+08:00\nworkflow/intents/a.md\nworkflow/plans/a.md\n@2026-10-01T09:00:00+08:00\nworkflow/intents/a.md\n';
|
|
@@ -2968,5 +3023,315 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2968
3023
|
fs.rmSync(cacheP, { force: true });
|
|
2969
3024
|
}
|
|
2970
3025
|
|
|
3026
|
+
// ---- 场景 23:场景覆盖子判据(2026-10-10 scenario-coverage,灰度 warning)----
|
|
3027
|
+
// scenarioIdsOf 纯函数 + 端到端(spec 有「验收场景」节 → done intent 须逐场景引用;缺 = warning 不阻断)
|
|
3028
|
+
{
|
|
3029
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'scen-ids-'));
|
|
3030
|
+
fs.writeFileSync(path.join(d, 's1.md'),
|
|
3031
|
+
'---\n状态: draft\n---\n# SPEC\n## 功能行为\n### S0 不该被提取(节外)\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- WHEN y\n- THEN z\n### S2 退款\n- GIVEN a\n## 数据流\n### S3 也不提取(节外)\n');
|
|
3032
|
+
const ids = scenarioIdsOf(path.join(d, 's1.md'));
|
|
3033
|
+
check('场景 24a scenarioIdsOf:只提「验收场景」节内 ID,按序去重', ids.join(',') === 'S1,S2', JSON.stringify(ids));
|
|
3034
|
+
fs.writeFileSync(path.join(d, 's2.md'), '---\n状态: draft\n---\n# SPEC\n## 功能行为\n无场景节\n');
|
|
3035
|
+
check('场景 24b scenarioIdsOf:无「验收场景」节 → 空数组', scenarioIdsOf(path.join(d, 's2.md')).length === 0);
|
|
3036
|
+
check('场景 24c scenarioIdsOf:文件不可读 → 空数组(fail-open)', scenarioIdsOf(path.join(d, 'nope.md')).length === 0);
|
|
3037
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
3038
|
+
}
|
|
3039
|
+
{
|
|
3040
|
+
// 端到端:spec 两场景 + intent 全引用 → exit 0 且无「场景未覆盖」warning
|
|
3041
|
+
const T = mkfix();
|
|
3042
|
+
w(T, 'workflow/specs/2026-09-12-sc.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — sc\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- WHEN y\n- THEN z\n### S2 退款\n- GIVEN a\n');
|
|
3043
|
+
w(T, 'workflow/intents/2026-09-12-sc.md', INTENT('sc', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3044
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1 折扣生效(证据:冒烟 checkout.json)\n- [x] 场景:S2 退款到账(证据:refund.test.js)\n'));
|
|
3045
|
+
w(T, 'workflow/plans/2026-09-12-sc.md', PLAN('sc', '状态: done\n级别: L1'));
|
|
3046
|
+
const r = run(T);
|
|
3047
|
+
check('场景 24d spec 有场景且 intent 全引用 → exit 0 无告警', r.status === 0 && !outOf(r).includes('场景未覆盖'), `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
|
|
3048
|
+
}
|
|
3049
|
+
{
|
|
3050
|
+
// 端到端:缺 S2 引用 → warning(灰度,不阻断:exit 仍 0)
|
|
3051
|
+
const T = mkfix();
|
|
3052
|
+
w(T, 'workflow/specs/2026-09-12-sc.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — sc\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n### S2 退款\n- GIVEN a\n');
|
|
3053
|
+
w(T, 'workflow/intents/2026-09-12-sc.md', INTENT('sc', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3054
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1 折扣生效(证据:冒烟 checkout.json)\n'));
|
|
3055
|
+
w(T, 'workflow/plans/2026-09-12-sc.md', PLAN('sc', '状态: done\n级别: L1'));
|
|
3056
|
+
const r = run(T);
|
|
3057
|
+
check('场景 24e 缺场景引用 → warning 且不阻断(exit 0)',
|
|
3058
|
+
r.status === 0 && outOf(r).includes('场景未覆盖') && outOf(r).includes('缺 S2'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3059
|
+
}
|
|
3060
|
+
{
|
|
3061
|
+
// 端到端:spec 无场景节 → 零变化(不出现「场景未覆盖」)
|
|
3062
|
+
const T = mkfix();
|
|
3063
|
+
w(T, 'workflow/specs/2026-09-12-sc.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — sc\n## 功能行为\n普通 spec\n');
|
|
3064
|
+
w(T, 'workflow/intents/2026-09-12-sc.md', INTENT('sc', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3065
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n'));
|
|
3066
|
+
w(T, 'workflow/plans/2026-09-12-sc.md', PLAN('sc', '状态: done\n级别: L1'));
|
|
3067
|
+
const r = run(T);
|
|
3068
|
+
check('场景 24f spec 无场景节 → 零变化(无场景告警,exit 0)', r.status === 0 && !outOf(r).includes('场景未覆盖'), `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// ---- 场景 25:沉淀追踪子判据(2026-10-10 sediment-tracking,灰度 warning)----
|
|
3072
|
+
// sectionHasBody 纯函数 + 端到端(done intent 含影响面/触达红线 且 frontmatter 无「沉淀」→ WARN 未沉淀;
|
|
3073
|
+
// incident closed 且三件套齐全 → 同款;「沉淀: 无」豁免;路径断档 → WARN 沉淀断档)
|
|
3074
|
+
{
|
|
3075
|
+
check('场景 25a sectionHasBody:标题后有实质行 → true',
|
|
3076
|
+
sectionHasBody(['## 影响面', '- 页面 A', ''], '影响面') === true);
|
|
3077
|
+
check('场景 25b sectionHasBody:光标题(模板未填)→ false',
|
|
3078
|
+
sectionHasBody(['## 影响面', '<页面 / 接口 / 数据范围>', ''], '影响面') === false);
|
|
3079
|
+
check('场景 25c sectionHasBody:无该节 → false',
|
|
3080
|
+
sectionHasBody(['## 目标', '做 X'], '影响面') === false);
|
|
3081
|
+
check('场景 25d sectionHasBody:前缀匹配 + 节外有同名行不算',
|
|
3082
|
+
sectionHasBody(['## 目标', '提影响面', '## 影响面与边界', '- 内容'], '影响面') === true);
|
|
3083
|
+
}
|
|
3084
|
+
{
|
|
3085
|
+
// intent 侧:done + 影响面有内容 + 无沉淀字段 → WARN 未沉淀(不阻断)
|
|
3086
|
+
const T = mkfix();
|
|
3087
|
+
w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3088
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A / 接口 B\n'));
|
|
3089
|
+
w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
|
|
3090
|
+
const r = run(T);
|
|
3091
|
+
check('场景 25e done intent 高价值无沉淀 → WARN 未沉淀且不阻断',
|
|
3092
|
+
r.status === 0 && outOf(r).includes('未沉淀') && outOf(r).includes('2026-09-12-sed.md'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3093
|
+
}
|
|
3094
|
+
{
|
|
3095
|
+
// 「沉淀: 无」显式豁免 → 无未沉淀告警
|
|
3096
|
+
const T = mkfix();
|
|
3097
|
+
w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: 无',
|
|
3098
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
|
|
3099
|
+
w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
|
|
3100
|
+
const r = run(T);
|
|
3101
|
+
check('场景 25f 「沉淀: 无」显式豁免 → 无未沉淀告警',
|
|
3102
|
+
r.status === 0 && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3103
|
+
}
|
|
3104
|
+
{
|
|
3105
|
+
// 沉淀字段指向不存在的文件 → WARN 沉淀断档
|
|
3106
|
+
const T = mkfix();
|
|
3107
|
+
w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: wiki/项目规范/不存在.md',
|
|
3108
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
|
|
3109
|
+
w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
|
|
3110
|
+
const r = run(T);
|
|
3111
|
+
check('场景 25g 沉淀字段指向缺失文件 → WARN 沉淀断档',
|
|
3112
|
+
r.status === 0 && outOf(r).includes('沉淀断档') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3113
|
+
}
|
|
3114
|
+
{
|
|
3115
|
+
// intent 无影响面/触达红线 → 零变化(不出现未沉淀告警)
|
|
3116
|
+
const T = mkfix();
|
|
3117
|
+
w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3118
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n'));
|
|
3119
|
+
w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
|
|
3120
|
+
const r = run(T);
|
|
3121
|
+
check('场景 25h intent 无高价值节 → 零变化', r.status === 0 && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
|
|
3122
|
+
}
|
|
3123
|
+
{
|
|
3124
|
+
// incident 侧:closed 且三件套齐全 且无沉淀 → WARN 未沉淀
|
|
3125
|
+
const T = mkfix();
|
|
3126
|
+
w(T, 'workflow/incidents/2026-09-12-inc.md',
|
|
3127
|
+
`---\n状态: closed\n级别: L1\n发现: 2026-09-12\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
|
|
3128
|
+
w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
|
|
3129
|
+
const r = run(T);
|
|
3130
|
+
check('场景 25i closed incident 三件套齐全无沉淀 → WARN 未沉淀',
|
|
3131
|
+
r.status === 0 && outOf(r).includes('未沉淀') && outOf(r).includes('2026-09-12-inc.md'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3132
|
+
}
|
|
3133
|
+
{
|
|
3134
|
+
// incident 三件套不全 → 沉淀子判据不触发(不出未沉淀,只出三件套不全)
|
|
3135
|
+
const T = mkfix();
|
|
3136
|
+
w(T, 'workflow/incidents/2026-09-12-inc.md',
|
|
3137
|
+
`---\n状态: closed\n级别: L1\n发现: 2026-09-12\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n1. 结构性修复\n - 修复 commit:abc1234\n - 是否需要新 intent:\n - 否 → 理由:单点修复\n`);
|
|
3138
|
+
w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
|
|
3139
|
+
const r = run(T);
|
|
3140
|
+
check('场景 25j incident 三件套不全 → 只告三件套不全,不出未沉淀',
|
|
3141
|
+
r.status === 0 && outOf(r).includes('三件套不全') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// ---- 场景 25 续:落点真实性(2026-10-11 landing-reality,P1-3,灰度 warning)----
|
|
3145
|
+
// sedimentLandingIsShell 纯函数 + 端到端(落点只有标题/占位 → WARN 落点空壳;带「来源:」frontmatter
|
|
3146
|
+
// 或 ≥3 行非占位正文 → 不出;不存在 → 仍 WARN 沉淀断档(回归);指向目录(读取失败)→ 空壳并注明读取失败)
|
|
3147
|
+
{
|
|
3148
|
+
check('场景 25k sedimentLandingIsShell:只有标题+占位(空壳)→ true',
|
|
3149
|
+
sedimentLandingIsShell('---\n标题: 某主题\n---\n# 某主题\n<状态:待整理>\n') === true);
|
|
3150
|
+
check('场景 25l sedimentLandingIsShell:frontmatter 有「来源:」→ false',
|
|
3151
|
+
sedimentLandingIsShell('---\n来源: workflow/incidents/2026-09-12-inc.md\n---\n# 某主题\n') === false);
|
|
3152
|
+
check('场景 25m sedimentLandingIsShell:非占位正文 ≥3 行 → false',
|
|
3153
|
+
sedimentLandingIsShell('# 某主题\n- 要点一\n- 要点二\n- 要点三\n') === false);
|
|
3154
|
+
check('场景 25n sedimentLandingIsShell:非占位正文仅 2 行 → true',
|
|
3155
|
+
sedimentLandingIsShell('# 某主题\n- 要点一\n- 要点二\n') === true);
|
|
3156
|
+
check('场景 25o sedimentLandingIsShell:读取失败(null)→ true',
|
|
3157
|
+
sedimentLandingIsShell(null) === true);
|
|
3158
|
+
}
|
|
3159
|
+
{
|
|
3160
|
+
// incident 侧:closed + 三件套齐全 + 沉淀指向只有标题/占位的文件 → WARN 落点空壳(不阻断)
|
|
3161
|
+
const T = mkfix();
|
|
3162
|
+
w(T, 'workflow/incidents/2026-09-12-inc.md',
|
|
3163
|
+
`---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范/shell.md\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
|
|
3164
|
+
w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
|
|
3165
|
+
fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
|
|
3166
|
+
w(T, 'wiki/项目规范/shell.md', '---\n标题: 某主题沉淀\n---\n# 某主题沉淀\n<状态:待整理>\n');
|
|
3167
|
+
const r = run(T);
|
|
3168
|
+
check('场景 25p incident 沉淀落点为标题/占位空壳 → WARN 落点空壳',
|
|
3169
|
+
r.status === 0 && outOf(r).includes('落点空壳') && outOf(r).includes('2026-09-12-inc.md')
|
|
3170
|
+
&& !outOf(r).includes('沉淀断档') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3171
|
+
}
|
|
3172
|
+
{
|
|
3173
|
+
// intent 侧对称:done + 影响面有内容 + 沉淀指向空壳文件 → WARN 落点空壳(不阻断)
|
|
3174
|
+
const T = mkfix();
|
|
3175
|
+
w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: wiki/项目规范/shell.md',
|
|
3176
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
|
|
3177
|
+
w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
|
|
3178
|
+
fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
|
|
3179
|
+
w(T, 'wiki/项目规范/shell.md', '# 某主题沉淀\n<待整理>\n');
|
|
3180
|
+
const r = run(T);
|
|
3181
|
+
check('场景 25q intent 沉淀落点为标题/占位空壳 → WARN 落点空壳',
|
|
3182
|
+
r.status === 0 && outOf(r).includes('落点空壳') && outOf(r).includes('2026-09-12-sed.md')
|
|
3183
|
+
&& !outOf(r).includes('沉淀断档') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3184
|
+
}
|
|
3185
|
+
{
|
|
3186
|
+
// 落点带「来源:」frontmatter → 非空壳(无落点空壳告警)
|
|
3187
|
+
const T = mkfix();
|
|
3188
|
+
w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: wiki/项目规范/real.md',
|
|
3189
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
|
|
3190
|
+
w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
|
|
3191
|
+
fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
|
|
3192
|
+
w(T, 'wiki/项目规范/real.md', '---\n来源: workflow/incidents/2026-09-12-inc.md\n---\n# 某主题沉淀\n');
|
|
3193
|
+
const r = run(T);
|
|
3194
|
+
check('场景 25r 沉淀落点带来源 frontmatter → 不出落点空壳',
|
|
3195
|
+
r.status === 0 && !outOf(r).includes('落点空壳') && !outOf(r).includes('沉淀断档'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3196
|
+
}
|
|
3197
|
+
{
|
|
3198
|
+
// 落点有 ≥3 行非占位正文 → 非空壳(incident 侧非空壳路径)
|
|
3199
|
+
const T = mkfix();
|
|
3200
|
+
w(T, 'workflow/incidents/2026-09-12-inc.md',
|
|
3201
|
+
`---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范/real.md\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
|
|
3202
|
+
w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
|
|
3203
|
+
fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
|
|
3204
|
+
w(T, 'wiki/项目规范/real.md', '---\n标题: 某主题沉淀\n---\n# 某主题沉淀\n- 要点一\n- 要点二\n- 要点三\n');
|
|
3205
|
+
const r = run(T);
|
|
3206
|
+
check('场景 25s 沉淀落点有 ≥3 行非占位正文 → 不出落点空壳',
|
|
3207
|
+
r.status === 0 && !outOf(r).includes('落点空壳') && !outOf(r).includes('沉淀断档'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3208
|
+
}
|
|
3209
|
+
{
|
|
3210
|
+
// 回归:沉淀指向不存在的文件 → 仍是 WARN 沉淀断档(incident 侧;intent 侧由既有 25g 锚定)
|
|
3211
|
+
const T = mkfix();
|
|
3212
|
+
w(T, 'workflow/incidents/2026-09-12-inc.md',
|
|
3213
|
+
`---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范/不存在.md\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
|
|
3214
|
+
w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
|
|
3215
|
+
const r = run(T);
|
|
3216
|
+
check('场景 25t 沉淀指向缺失文件 → 仍 WARN 沉淀断档(不出落点空壳)',
|
|
3217
|
+
r.status === 0 && outOf(r).includes('沉淀断档') && !outOf(r).includes('落点空壳'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3218
|
+
}
|
|
3219
|
+
{
|
|
3220
|
+
// 落点指向目录(readFileSync 抛异常)→ 按空壳出账并在文案注明读取失败
|
|
3221
|
+
const T = mkfix();
|
|
3222
|
+
w(T, 'workflow/incidents/2026-09-12-inc.md',
|
|
3223
|
+
`---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
|
|
3224
|
+
w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
|
|
3225
|
+
fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
|
|
3226
|
+
const r = run(T);
|
|
3227
|
+
check('场景 25u 沉淀落点读取失败(目录)→ WARN 落点空壳并注明读取失败',
|
|
3228
|
+
r.status === 0 && outOf(r).includes('落点空壳') && outOf(r).includes('读取失败'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// ---- 场景 92:THEN 断言抽取(2026-10-11 then-assertion,P2-2,灰度 warning)----
|
|
3232
|
+
// extractThenByScenario 纯函数 + 端到端(声明场景缺 THEN 行 = 空壳场景 → WARN 场景无断言,聚合一条;
|
|
3233
|
+
// 验收场景节本身可选——节不存在零变化)
|
|
3234
|
+
{
|
|
3235
|
+
const spec = [
|
|
3236
|
+
'## 验收场景(可选)',
|
|
3237
|
+
'<说明行,不属于任何场景块>',
|
|
3238
|
+
'### S1 折扣',
|
|
3239
|
+
'- GIVEN 购物车有商品 THEN 不误抽(行中段 THEN 字样不算断言)',
|
|
3240
|
+
'- WHEN 结算',
|
|
3241
|
+
'- THEN 折扣金额入账',
|
|
3242
|
+
' - THEN 重复结算幂等(缩进变体也认)',
|
|
3243
|
+
'### S2 退款',
|
|
3244
|
+
'THEN: 退款单生成(无连字符带冒号也认)',
|
|
3245
|
+
'THEN 审计流水落库(无连字符空格也认)',
|
|
3246
|
+
].join('\n');
|
|
3247
|
+
const m = extractThenByScenario(spec);
|
|
3248
|
+
check('场景 92a extractThenByScenario:- THEN / 缩进 / THEN: / THEN 变体均抽取',
|
|
3249
|
+
m.get('S1').length === 2 && m.get('S2').length === 2
|
|
3250
|
+
&& m.get('S1')[0] === '- THEN 折扣金额入账' && m.get('S2')[0] === 'THEN: 退款单生成(无连字符带冒号也认)',
|
|
3251
|
+
JSON.stringify([...m]));
|
|
3252
|
+
check('场景 92b extractThenByScenario:GIVEN 行中段 THEN 字样不误抽',
|
|
3253
|
+
!m.get('S1').some((s) => s.includes('GIVEN')), JSON.stringify(m.get('S1')));
|
|
3254
|
+
check('场景 92c extractThenByScenario:无验收场景节 → 空 Map(节外 THEN 不认)',
|
|
3255
|
+
extractThenByScenario('# SPEC\n## 功能行为\n- THEN 节外不认\n').size === 0);
|
|
3256
|
+
check('场景 92d extractThenByScenario:非字符串(读取失败)→ 空 Map(不可判定不误报)',
|
|
3257
|
+
extractThenByScenario(null).size === 0);
|
|
3258
|
+
}
|
|
3259
|
+
{
|
|
3260
|
+
// 端到端:全场景带 THEN → 无「场景无断言」
|
|
3261
|
+
const T = mkfix();
|
|
3262
|
+
w(T, 'workflow/specs/2026-09-12-thn.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — thn\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- WHEN y\n- THEN z\n### S2 退款\n- GIVEN a\n- THEN b\n');
|
|
3263
|
+
w(T, 'workflow/intents/2026-09-12-thn.md', INTENT('thn', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3264
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1 折扣生效(证据:checkout.json)\n- [x] 场景:S2 退款到账(证据:refund.test.js)\n'));
|
|
3265
|
+
w(T, 'workflow/plans/2026-09-12-thn.md', PLAN('thn', '状态: done\n级别: L1'));
|
|
3266
|
+
const r = run(T);
|
|
3267
|
+
check('场景 92e 全场景带 THEN → 无场景无断言告警(exit 0)',
|
|
3268
|
+
r.status === 0 && !outOf(r).includes('场景无断言'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3269
|
+
}
|
|
3270
|
+
{
|
|
3271
|
+
// 端到端:S2/S3 缺 THEN → 聚合一条 WARN,S1(有断言)不在列
|
|
3272
|
+
const T = mkfix();
|
|
3273
|
+
w(T, 'workflow/specs/2026-09-12-thn.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — thn\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- THEN z\n### S2 退款\n- GIVEN a\n### S3 对账\n- WHEN c\n');
|
|
3274
|
+
w(T, 'workflow/intents/2026-09-12-thn.md', INTENT('thn', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3275
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n- [x] 场景:S2(证据:b.log)\n- [x] 场景:S3(证据:c.log)\n'));
|
|
3276
|
+
w(T, 'workflow/plans/2026-09-12-thn.md', PLAN('thn', '状态: done\n级别: L1'));
|
|
3277
|
+
const r = run(T);
|
|
3278
|
+
check('场景 92f 场景缺 THEN → 聚合 WARN 且只列缺断言的 ID(S2、S3)',
|
|
3279
|
+
r.status === 0 && outOf(r).includes('场景无断言') && outOf(r).includes('缺 THEN 断言:S2、S3'),
|
|
3280
|
+
`exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3281
|
+
}
|
|
3282
|
+
{
|
|
3283
|
+
// 端到端:spec 无验收场景节 → THEN 对账不适用(零变化)
|
|
3284
|
+
const T = mkfix();
|
|
3285
|
+
w(T, 'workflow/specs/2026-09-12-thn.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — thn\n## 功能行为\n普通 spec,无场景节\n');
|
|
3286
|
+
w(T, 'workflow/intents/2026-09-12-thn.md', INTENT('thn', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3287
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n'));
|
|
3288
|
+
w(T, 'workflow/plans/2026-09-12-thn.md', PLAN('thn', '状态: done\n级别: L1'));
|
|
3289
|
+
const r = run(T);
|
|
3290
|
+
check('场景 92g spec 无验收场景节 → 零场景告警(exit 0)',
|
|
3291
|
+
r.status === 0 && !outOf(r).includes('场景无断言') && !outOf(r).includes('场景引用断链'),
|
|
3292
|
+
`exit=${r.status}\n${outOf(r).slice(0, 400)}`);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// ---- 场景 93:引用断链反向对账(2026-10-11 dangling-ref,P2-1,灰度 warning)----
|
|
3296
|
+
// 端到端(与「场景未覆盖」共用同一次解析):验收标准引用 spec 未声明的场景 ID → WARN 场景引用断链
|
|
3297
|
+
{
|
|
3298
|
+
// 引用 S1(已声明且覆盖)+ S9(未声明)→ 只出断链,不出未覆盖
|
|
3299
|
+
const T = mkfix();
|
|
3300
|
+
w(T, 'workflow/specs/2026-09-12-dr.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — dr\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- THEN z\n');
|
|
3301
|
+
w(T, 'workflow/intents/2026-09-12-dr.md', INTENT('dr', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3302
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n- [x] 场景:S9 幻影场景(证据:ghost.log)\n'));
|
|
3303
|
+
w(T, 'workflow/plans/2026-09-12-dr.md', PLAN('dr', '状态: done\n级别: L1'));
|
|
3304
|
+
const r = run(T);
|
|
3305
|
+
check('场景 93a 引用 spec 未声明的 S9 → WARN 场景引用断链(不阻断)',
|
|
3306
|
+
r.status === 0 && outOf(r).includes('场景引用断链') && outOf(r).includes('S9'),
|
|
3307
|
+
`exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3308
|
+
check('场景 93b 已覆盖的 S1 不触发未覆盖(两方向互不误伤)',
|
|
3309
|
+
!outOf(r).includes('场景未覆盖'), outOf(r).slice(0, 400));
|
|
3310
|
+
}
|
|
3311
|
+
{
|
|
3312
|
+
// 引用与声明完全一致 → 无断链
|
|
3313
|
+
const T = mkfix();
|
|
3314
|
+
w(T, 'workflow/specs/2026-09-12-dr.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — dr\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- THEN z\n### S2 退款\n- GIVEN a\n- THEN b\n');
|
|
3315
|
+
w(T, 'workflow/intents/2026-09-12-dr.md', INTENT('dr', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3316
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n- [x] 场景:S2(证据:b.log)\n'));
|
|
3317
|
+
w(T, 'workflow/plans/2026-09-12-dr.md', PLAN('dr', '状态: done\n级别: L1'));
|
|
3318
|
+
const r = run(T);
|
|
3319
|
+
check('场景 93c 引用与声明一致 → 无断链无未覆盖(exit 0)',
|
|
3320
|
+
r.status === 0 && !outOf(r).includes('场景引用断链') && !outOf(r).includes('场景未覆盖'),
|
|
3321
|
+
`exit=${r.status}\n${outOf(r).slice(0, 400)}`);
|
|
3322
|
+
}
|
|
3323
|
+
{
|
|
3324
|
+
// spec 无场景节但 intent 引用 S1 → 同样算断链(引用了不存在的场景)
|
|
3325
|
+
const T = mkfix();
|
|
3326
|
+
w(T, 'workflow/specs/2026-09-12-dr.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — dr\n## 功能行为\n普通 spec,无场景节\n');
|
|
3327
|
+
w(T, 'workflow/intents/2026-09-12-dr.md', INTENT('dr', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
3328
|
+
'\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n'));
|
|
3329
|
+
w(T, 'workflow/plans/2026-09-12-dr.md', PLAN('dr', '状态: done\n级别: L1'));
|
|
3330
|
+
const r = run(T);
|
|
3331
|
+
check('场景 93d spec 无场景节且引用 S1 → 断链 WARN(exit 0)',
|
|
3332
|
+
r.status === 0 && outOf(r).includes('场景引用断链') && outOf(r).includes('S1'),
|
|
3333
|
+
`exit=${r.status}\n${outOf(r).slice(0, 500)}`);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
2971
3336
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2972
|
-
process.exit(fail ? 1 : 0);
|
|
3337
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -38,8 +38,9 @@ import fs from 'node:fs';
|
|
|
38
38
|
import path from 'node:path';
|
|
39
39
|
import readline from 'node:readline';
|
|
40
40
|
import { createHash, randomBytes } from 'node:crypto';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
41
|
+
import { spawnSync } from 'node:child_process';
|
|
42
|
+
import { confirmGateFor, clarifyGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
43
|
+
import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeDigest } from './policy.mjs';
|
|
43
44
|
|
|
44
45
|
// hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
45
46
|
// policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
|
|
@@ -65,6 +66,33 @@ function docRelOf(root, doc) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// gitDirtyFiles(root):机器实测的「工作树处于改动态的文件集合」(已跟踪改动 + 未跟踪新件,gitignore 项天然排除)。
|
|
70
|
+
// 解析 `git status --porcelain`(-z 不用,逐行解析足够;重命名行取 `->` 后段)。
|
|
71
|
+
// 非 git 仓库 / git 不可用 / 命令失败 → [](**静默降级为「无额外改动面」而非假装有**——scope 退化成
|
|
72
|
+
// 只含被确认文档本身,仍然自洽可验;不因探测失败而拒绝关单)。
|
|
73
|
+
function gitDirtyFiles(root) {
|
|
74
|
+
try {
|
|
75
|
+
const r = spawnSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8', timeout: 10_000 });
|
|
76
|
+
if (r.status !== 0 || !r.stdout) return [];
|
|
77
|
+
return r.stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
78
|
+
const p = line.slice(3).trim();
|
|
79
|
+
const arrow = p.lastIndexOf(' -> ');
|
|
80
|
+
const rel = arrow >= 0 ? p.slice(arrow + 4) : p;
|
|
81
|
+
return rel.replace(/^"(.*)"$/, '$1');
|
|
82
|
+
}).filter(Boolean);
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// machineScope(root, doc):落账用的改动面事实(2026-10-09 审查 A1 干净版)。
|
|
89
|
+
// **AI 无输入面**——files 全部由机器派生(被确认文档 + git 实测工作树改动集),不是调用方能填的参数。
|
|
90
|
+
// 这正是相对旧设计的核心改进:旧的 quote 是 AI 自填的自由文本(可复用、可伪造、机器不可校验);
|
|
91
|
+
// scope 是机器事实,AI 填不了假,对质时问的是「这 N 个文件」而非「一句说过的话」。
|
|
92
|
+
// 归一/摘要计算单源在 policy.scopeDigest(check-loop 检查 15 侧校验复用同一实现)。
|
|
93
|
+
export function machineScope(root, doc) {
|
|
94
|
+
return scopeDigest([docRelOf(root, doc), ...gitDirtyFiles(root)]);
|
|
95
|
+
}
|
|
68
96
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
69
97
|
export function computeFingerprint(text) {
|
|
70
98
|
const norm = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
@@ -330,6 +358,19 @@ if (isMain) {
|
|
|
330
358
|
refused++;
|
|
331
359
|
continue;
|
|
332
360
|
}
|
|
361
|
+
// 待澄清项门(2026-10-12 needs-clarification):起草期挂「待澄清」标记**不阻断**(零阻塞),
|
|
362
|
+
// 但 draft→approved 确认时若仍挂着未澄清项则拒绝——确认即「认可这份文档」,文档里还写着
|
|
363
|
+
// 「这里我不知道」,确认就没有意义。与上面的前置门分开出账:拒绝原因必须指向真正的原因
|
|
364
|
+
// (合并会让作者去查「前置」,而前置其实是好的)。
|
|
365
|
+
// ⚠️ 文案锚点硬约束:本段禁止并入上方 gate 分支的输出契约(既有 confirm-doc.test.mjs 断言
|
|
366
|
+
// 锚定「未过逐阶段前置门(stage-gates)」子串),故单列独立分支。
|
|
367
|
+
const cl = clarifyGateFor(abs);
|
|
368
|
+
if (!cl.ok) {
|
|
369
|
+
console.error(`❌ ${doc} 含未澄清项(needs-clarification):${cl.reason}`);
|
|
370
|
+
console.error(` ${cl.hint}(本份未落账、未写盘)`);
|
|
371
|
+
refused++;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
333
374
|
}
|
|
334
375
|
// done 前置门(2026-09-30 confirm-gate-approved-history):→done 须 git 历史已留 approved 态
|
|
335
376
|
// (先 done 后提交 / 两跳同批提交 → 拒绝;防「确认态缺失」缺陷复发;判据单源见 stage-gates.doneGateFor)。
|
|
@@ -360,6 +401,9 @@ if (isMain) {
|
|
|
360
401
|
}
|
|
361
402
|
}
|
|
362
403
|
const fp = computeFingerprint(text);
|
|
404
|
+
// 改动面事实(2026-10-09 审查 A1 干净版):机器派生,三形态(TTY / 委托代录 / AI 自治)一律记入——
|
|
405
|
+
// TTY 形态同样需要(事后对质问的是「放行时盘面上哪些文件在动」,与谁点的确认无关)。
|
|
406
|
+
const scope = machineScope(root, doc);
|
|
363
407
|
// AI 自治放行分支(2026-09-30 ai-autonomy-trust):
|
|
364
408
|
// ① 仅 L0/L1(读 frontmatter「级别」,缺级别按不可自治处理——fail-closed);
|
|
365
409
|
// ② Level 1:仅 draft→approved;Level 2:draft→approved 与 approved→done;
|
|
@@ -378,7 +422,7 @@ if (isMain) {
|
|
|
378
422
|
appendLedger(root, {
|
|
379
423
|
ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
|
|
380
424
|
source: `ai-auto-trust-L${trustConfig.level}`, quote: `AI 自治放行 (Trust Level ${trustConfig.level})`,
|
|
381
|
-
batch, seq, of: docs.length,
|
|
425
|
+
batch, seq, of: docs.length, scope,
|
|
382
426
|
});
|
|
383
427
|
console.log(`✓ ${doc} ${st} → ${target}(指纹 ${fp.slice(0, 16)},AI 自治已记账:source=ai-auto-trust-L${trustConfig.level})`);
|
|
384
428
|
confirmed++;
|
|
@@ -401,6 +445,9 @@ if (isMain) {
|
|
|
401
445
|
source: 'chat-delegated', quote: String(delegatedQuote),
|
|
402
446
|
// 调用事实(batch-ledger-audit):seq=本次调用内件序(从 1),of=本次调用总份数
|
|
403
447
|
batch, seq, of: docs.length,
|
|
448
|
+
// 改动面事实(A1 干净版):quote 降级为「人类可读摘要」,分辨力改由 scope(机器派生)+ fingerprint
|
|
449
|
+
// (文档内容)承担——「不复用同句」纪律据此退役(复用一句摘要不再削弱任何判据)。
|
|
450
|
+
scope,
|
|
404
451
|
// 简洁审计标记(hybrid-governance-risk-lanes):L0/L1 协作道行带 brief(quote 仍入账供对质),
|
|
405
452
|
// 检查 15 并录审计对全 brief 批次豁免「确认并录」告警
|
|
406
453
|
...(low ? { brief: true } : {}),
|
|
@@ -13,7 +13,8 @@ import os from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { computeFingerprint, nextStage, applyTransition, appendLedger, resolveTransition } from './confirm-doc.mjs';
|
|
16
|
+
import { computeFingerprint, nextStage, applyTransition, appendLedger, resolveTransition, machineScope } from './confirm-doc.mjs';
|
|
17
|
+
import { scopeDigest, scopeMatches } from './policy.mjs';
|
|
17
18
|
|
|
18
19
|
let pass = 0;
|
|
19
20
|
let fail = 0;
|
|
@@ -385,6 +386,65 @@ const readLedgerOf = (root) => {
|
|
|
385
386
|
fs.rmSync(root, { recursive: true, force: true });
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
// ---- S19b【待澄清门】spec 挂未澄清项 → 拒绝落账(2026-10-12 needs-clarification)----
|
|
390
|
+
// 夹具沿 S23 的正路入口(台账行 + approved),确保**前置门已过**——否则拒绝原因会指向前置而非待澄清,
|
|
391
|
+
// 用例就测不出新门。断言拒绝文案点名「needs-clarification」且**不含**前置门文案(两门分开出账)。
|
|
392
|
+
{
|
|
393
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl1-'));
|
|
394
|
+
for (const sub of ['intents', 'specs']) fs.mkdirSync(path.join(root, 'workflow', sub), { recursive: true });
|
|
395
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
396
|
+
const fp = 'c'.repeat(64);
|
|
397
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-12-n.md'), `---\n状态: approved\n级别: L2\n日期: 2026-10-12\n确认指纹: ${fp.slice(0, 16)}\n---\n# I\n`);
|
|
398
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-10-12T02:00:00.000Z', doc: 'workflow/intents/2026-10-12-n.md', stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 'n1', seq: 1, of: 1 }) + '\n');
|
|
399
|
+
const specP = path.join(root, 'workflow', 'specs', '2026-10-12-n.md');
|
|
400
|
+
fs.writeFileSync(specP, '---\n状态: draft\n级别: L2\n---\n# S\n\n## 功能行为\n- 待澄清:错误率的判据是 5xx 占比还是全量?\n');
|
|
401
|
+
const before = fs.readFileSync(specP, 'utf8');
|
|
402
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-10-12-n.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
403
|
+
const after = fs.readFileSync(specP, 'utf8');
|
|
404
|
+
const led = readLedgerOf(root);
|
|
405
|
+
check('S19b 待澄清门:挂未澄清项 → exit 2 + 点名行号与内容 + 未落盘 + 零新增台账',
|
|
406
|
+
r.status === 2 && /needs-clarification/.test(r.stderr) && /第8行/.test(r.stderr) && /5xx 占比还是全量/.test(r.stderr)
|
|
407
|
+
&& after === before && led.length === 1,
|
|
408
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, ledger: led.length }));
|
|
409
|
+
check('S19b 待澄清门:拒绝原因不误指向前置门(两门分开出账,否则作者查错地方)',
|
|
410
|
+
!/逐阶段前置门/.test(r.stderr), r.stderr);
|
|
411
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---- S19c【待澄清门】解决后(行首改「已澄清」)→ 放行;纯删行亦可 ----
|
|
415
|
+
{
|
|
416
|
+
const mkClarified = (body) => {
|
|
417
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl2-'));
|
|
418
|
+
for (const sub of ['intents', 'specs']) fs.mkdirSync(path.join(root, 'workflow', sub), { recursive: true });
|
|
419
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
420
|
+
const fp = 'd'.repeat(64);
|
|
421
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-12-o.md'), `---\n状态: approved\n级别: L2\n日期: 2026-10-12\n确认指纹: ${fp.slice(0, 16)}\n---\n# I\n`);
|
|
422
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-10-12T02:00:00.000Z', doc: 'workflow/intents/2026-10-12-o.md', stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 'o1', seq: 1, of: 1 }) + '\n');
|
|
423
|
+
fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-10-12-o.md'), body);
|
|
424
|
+
return root;
|
|
425
|
+
};
|
|
426
|
+
for (const [name, body] of [
|
|
427
|
+
['改前缀为「已澄清」(保留行、留痕曾不清楚)', '---\n状态: draft\n级别: L2\n---\n# S\n\n- 已澄清:错误率判据 = 5xx 占比(与用户 10-12 对齐)\n'],
|
|
428
|
+
['直接删行', '---\n状态: draft\n级别: L2\n---\n# S\n\n## 功能行为\n- 错误率判据 = 5xx 占比\n'],
|
|
429
|
+
]) {
|
|
430
|
+
const root = mkClarified(body);
|
|
431
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-10-12-o.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
432
|
+
const after = fs.readFileSync(path.join(root, 'workflow', 'specs', '2026-10-12-o.md'), 'utf8');
|
|
433
|
+
check(`S19c 待澄清门:${name} → 放行落态`, r.status === 0 && after.includes('状态: approved'),
|
|
434
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
435
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
// intents 不受本门约束(歧义标记的落点是 spec/plan 的设计判据;intent 是需求单,澄清天然发生在此处)
|
|
438
|
+
const root3 = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl3-'));
|
|
439
|
+
fs.mkdirSync(path.join(root3, 'workflow', 'intents'), { recursive: true });
|
|
440
|
+
fs.writeFileSync(path.join(root3, 'workflow', 'intents', '2026-10-12-p.md'), '---\n状态: draft\n级别: L1\n---\n# I\n\n- 待澄清:范围含不含移动端?\n');
|
|
441
|
+
const r3 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-12-p.md', '--delegated', '可以'], { cwd: root3, encoding: 'utf8' });
|
|
442
|
+
check('S19c 待澄清门:intent 不受本门约束(澄清天然发生在需求单处)',
|
|
443
|
+
r3.status === 0 && fs.readFileSync(path.join(root3, 'workflow', 'intents', '2026-10-12-p.md'), 'utf8').includes('状态: approved'),
|
|
444
|
+
JSON.stringify({ status: r3.status, stderr: r3.stderr }));
|
|
445
|
+
fs.rmSync(root3, { recursive: true, force: true });
|
|
446
|
+
}
|
|
447
|
+
|
|
388
448
|
// ---- S20【逐阶段门】入口存在但未确认(draft)→ 拒绝 ----
|
|
389
449
|
{
|
|
390
450
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg2-'));
|
|
@@ -788,5 +848,37 @@ const writeTrust = (root, cfg) => {
|
|
|
788
848
|
r43.status === 0 && /无测试绿凭证/.test(r43.stderr), String(r43.stderr).slice(0, 200));
|
|
789
849
|
}
|
|
790
850
|
|
|
851
|
+
// ---- S41 台账 scope:机器派生改动面(2026-10-09 审查 A1 干净版)----
|
|
852
|
+
// 背景:委托代录的授权凭据原是 `--delegated` 的用户原话——自由文本机器不可校验、AI 可零成本复用。
|
|
853
|
+
// 干净版把分辨力换成机器派生的 scope{files,sha256}:写入侧机器派生(AI 无输入面),校验侧重算比对。
|
|
854
|
+
// ① 归一与摘要单源(反斜杠 / 前导 ./ / 重复项 / 顺序无关 → 同一摘要);② scopeMatches 抓「改了 files
|
|
855
|
+
// 没改摘要」;③ 委托落账行带 scope 且含被确认文档;④ 非 git 夹具降级不崩(scope 退化为仅含 doc)。
|
|
856
|
+
{
|
|
857
|
+
const d = scopeDigest(['b.md', 'a.md', './a.md', 'x\\y.md']);
|
|
858
|
+
check('S41① scopeDigest 归一:反斜杠→posix、去重、排序无关,摘要自洽',
|
|
859
|
+
d.files.join(',') === 'a.md,b.md,x/y.md' && d.sha256 === scopeDigest(['x/y.md', 'a.md', 'b.md']).sha256
|
|
860
|
+
&& scopeMatches({ scope: d }), JSON.stringify(d));
|
|
861
|
+
check('S41② scopeMatches:files 被改而摘要未同步 → 判否',
|
|
862
|
+
!scopeMatches({ scope: { files: [...d.files, 'zz.md'], sha256: d.sha256 } })
|
|
863
|
+
&& !scopeMatches({}) && !scopeMatches({ scope: { files: d.files } }));
|
|
864
|
+
|
|
865
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-scope-'));
|
|
866
|
+
const docP = path.join(root, 'workflow', 'intents'); // 用 intents:无逐阶段前置门,本用例只验 scope 落账
|
|
867
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
868
|
+
fs.writeFileSync(path.join(docP, '2026-10-09-s.md'), '---\n状态: draft\n---\n# I\n');
|
|
869
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-09-s.md', '--delegated', '放行'], { cwd: root, encoding: 'utf8' });
|
|
870
|
+
const led = readLedgerOf(root);
|
|
871
|
+
const j = led[led.length - 1];
|
|
872
|
+
check('S41③ 委托落账行带机器派生 scope:含被确认文档 + 摘要自洽',
|
|
873
|
+
r.status === 0 && j && j.scope && Array.isArray(j.scope.files)
|
|
874
|
+
&& j.scope.files.includes('workflow/intents/2026-10-09-s.md') && scopeMatches(j),
|
|
875
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, scope: j && j.scope }));
|
|
876
|
+
check('S41④ 非 git 夹具降级:machineScope 不崩且仍含 doc(静默退化为无额外改动面,不拒绝关单)',
|
|
877
|
+
(() => { const s = machineScope(root, 'workflow/intents/2026-10-09-s.md');
|
|
878
|
+
return Array.isArray(s.files) && s.files.includes('workflow/intents/2026-10-09-s.md') && scopeMatches({ scope: s }); })(),
|
|
879
|
+
JSON.stringify(machineScope(root, 'workflow/intents/2026-10-09-s.md')));
|
|
880
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
881
|
+
}
|
|
882
|
+
|
|
791
883
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
792
884
|
process.exit(fail ? 1 : 0);
|