sillyspec 3.32.10 → 3.33.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/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +58 -0
- package/.husky/pre-push +7 -6
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/ceremony-tier.js +2 -1
- package/src/decision-distill.js +29 -3
- package/src/decision-redomain.js +148 -0
- package/src/design-facts.js +3 -3
- package/src/docs-debt.js +3 -3
- package/src/flow-parity.js +10 -2
- package/src/flow.js +1 -1
- package/src/forensics.js +236 -0
- package/src/fr-index.js +2 -1
- package/src/index.js +34 -1
- package/src/knowledge-graph.js +3 -4
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/run/gates.js +7 -14
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +40 -25
- package/src/run/verify-quality-scan.js +10 -1
- package/src/spec-sync.js +81 -2
- package/src/stage-review.js +2 -1
- package/src/stages/brainstorm.js +71 -20
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/test-bindings.js +32 -3
- package/src/ui-visual.js +119 -4
- package/src/verify-postcheck.js +117 -17
- package/src/worktree-apply.js +26 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
2026-10-15T09:00:00Z START s1 login
|
|
2
|
+
2026-10-15T09:00:03Z START s2 upload
|
|
3
|
+
2026-10-15T09:00:04Z END s1 login
|
|
4
|
+
2026-10-15T09:00:05Z START s3 download
|
|
5
|
+
2026-10-15T09:00:20Z END s2 upload
|
|
6
|
+
2026-10-15T09:00:15Z END s3 download
|
|
7
|
+
|
|
8
|
+
2026-10-15T09:05:00Z START s4 login
|
|
9
|
+
2026-10-15T09:05:09Z END s4 login
|
|
10
|
+
2026-10-15T09:06:00Z START s5 upload
|
|
11
|
+
2026-10-15T09:07:00Z END s6 download
|
|
12
|
+
2026-10-15T09:08:00Z START s7 login
|
|
13
|
+
2026-10-15T09:08:03Z END s7 login
|
|
14
|
+
2026-10-15T09:08:30Z START s7 login
|
|
15
|
+
2026-10-15T09:08:36Z END s7 login
|
|
16
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
|
|
3
|
+
// 覆盖全部隐含面:剥 UTF-8 BOM;容忍 CRLF 行尾;跳过空行;CSV 引号语义(引号内逗号非
|
|
4
|
+
// 分隔符,引号内金额剥千分位逗号);输出前自建 out/ 目录。
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/** 引号感知的单行 CSV 切分(覆盖本任务数据形态:引号包裹含逗号字段)。 */
|
|
9
|
+
function splitCsvLine(line) {
|
|
10
|
+
const fields = [];
|
|
11
|
+
let cur = '';
|
|
12
|
+
let inQuotes = false;
|
|
13
|
+
for (let i = 0; i < line.length; i++) {
|
|
14
|
+
const ch = line[i];
|
|
15
|
+
if (inQuotes) {
|
|
16
|
+
if (ch === '"') {
|
|
17
|
+
if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
|
|
18
|
+
} else { cur += ch; }
|
|
19
|
+
} else if (ch === '"') {
|
|
20
|
+
inQuotes = true;
|
|
21
|
+
} else if (ch === ',') {
|
|
22
|
+
fields.push(cur); cur = '';
|
|
23
|
+
} else { cur += ch; }
|
|
24
|
+
}
|
|
25
|
+
fields.push(cur);
|
|
26
|
+
return fields;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const raw = readFileSync('data/orders.csv', 'utf8').replace(/^\uFEFF/, ''); // 剥 BOM
|
|
30
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== ''); // CRLF 容忍 + 空行跳过
|
|
31
|
+
const header = splitCsvLine(lines[0]);
|
|
32
|
+
const col = (name) => header.indexOf(name);
|
|
33
|
+
const iRegion = col('region');
|
|
34
|
+
const iAmount = col('amount');
|
|
35
|
+
const iStatus = col('status');
|
|
36
|
+
|
|
37
|
+
const agg = new Map(); // region -> { total, count }
|
|
38
|
+
for (const line of lines.slice(1)) {
|
|
39
|
+
const f = splitCsvLine(line);
|
|
40
|
+
const status = (f[iStatus] ?? '').trim();
|
|
41
|
+
if (status !== 'paid') continue;
|
|
42
|
+
const region = (f[iRegion] ?? '').trim();
|
|
43
|
+
const amount = Number.parseFloat((f[iAmount] ?? '').replace(/,/g, '')); // 引号内千分位剥除
|
|
44
|
+
if (!Number.isFinite(amount)) continue;
|
|
45
|
+
const a = agg.get(region) ?? { total: 0, count: 0 };
|
|
46
|
+
a.total += amount;
|
|
47
|
+
a.count += 1;
|
|
48
|
+
agg.set(region, a);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const regions = [...agg.entries()]
|
|
52
|
+
.map(([region, a]) => ({ region, total: Math.round(a.total * 100) / 100, count: a.count }))
|
|
53
|
+
.sort((x, y) => (x.region < y.region ? -1 : x.region > y.region ? 1 : 0));
|
|
54
|
+
|
|
55
|
+
const outPath = join('out', 'summary.json');
|
|
56
|
+
mkdirSync(dirname(outPath), { recursive: true }); // 隐含面:目录不存在须自建
|
|
57
|
+
writeFileSync(outPath, JSON.stringify({ regions }) + '\n');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.csv -text
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"regions":[{"region":"APAC","total":2200,"count":2},{"region":"EU","total":2284.5,"count":3}]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
|
|
3
|
+
// 覆盖全部隐含面:剥 UTF-8 BOM;容忍 CRLF 行尾;跳过空行;CSV 引号语义(引号内逗号非
|
|
4
|
+
// 分隔符,引号内金额剥千分位逗号);输出前自建 out/ 目录。
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/** 引号感知的单行 CSV 切分(覆盖本任务数据形态:引号包裹含逗号字段)。 */
|
|
9
|
+
function splitCsvLine(line) {
|
|
10
|
+
const fields = [];
|
|
11
|
+
let cur = '';
|
|
12
|
+
let inQuotes = false;
|
|
13
|
+
for (let i = 0; i < line.length; i++) {
|
|
14
|
+
const ch = line[i];
|
|
15
|
+
if (inQuotes) {
|
|
16
|
+
if (ch === '"') {
|
|
17
|
+
if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
|
|
18
|
+
} else { cur += ch; }
|
|
19
|
+
} else if (ch === '"') {
|
|
20
|
+
inQuotes = true;
|
|
21
|
+
} else if (ch === ',') {
|
|
22
|
+
fields.push(cur); cur = '';
|
|
23
|
+
} else { cur += ch; }
|
|
24
|
+
}
|
|
25
|
+
fields.push(cur);
|
|
26
|
+
return fields;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const raw = readFileSync('data/orders.csv', 'utf8').replace(/^\uFEFF/, ''); // 剥 BOM
|
|
30
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== ''); // CRLF 容忍 + 空行跳过
|
|
31
|
+
const header = splitCsvLine(lines[0]);
|
|
32
|
+
const col = (name) => header.indexOf(name);
|
|
33
|
+
const iRegion = col('region');
|
|
34
|
+
const iAmount = col('amount');
|
|
35
|
+
const iStatus = col('status');
|
|
36
|
+
|
|
37
|
+
const agg = new Map(); // region -> { total, count }
|
|
38
|
+
for (const line of lines.slice(1)) {
|
|
39
|
+
const f = splitCsvLine(line);
|
|
40
|
+
const status = (f[iStatus] ?? '').trim();
|
|
41
|
+
if (status !== 'paid') continue;
|
|
42
|
+
const region = (f[iRegion] ?? '').trim();
|
|
43
|
+
const amount = Number.parseFloat((f[iAmount] ?? '').replace(/,/g, '')); // 引号内千分位剥除
|
|
44
|
+
if (!Number.isFinite(amount)) continue;
|
|
45
|
+
const a = agg.get(region) ?? { total: 0, count: 0 };
|
|
46
|
+
a.total += amount;
|
|
47
|
+
a.count += 1;
|
|
48
|
+
agg.set(region, a);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const regions = [...agg.entries()]
|
|
52
|
+
.map(([region, a]) => ({ region, total: Math.round(a.total * 100) / 100, count: a.count }))
|
|
53
|
+
.sort((x, y) => (x.region < y.region ? -1 : x.region > y.region ? 1 : 0));
|
|
54
|
+
|
|
55
|
+
const outPath = join('out', 'summary.json');
|
|
56
|
+
mkdirSync(dirname(outPath), { recursive: true }); // 隐含面:目录不存在须自建
|
|
57
|
+
writeFileSync(outPath, JSON.stringify({ regions }) + '\n');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.csv -text
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"regions":[{"region":"APAC","total":2200,"count":2},{"region":"EU","total":2284.5,"count":3}]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
2026-10-15T09:00:00Z START s1 login
|
|
2
|
+
2026-10-15T09:00:03Z START s2 upload
|
|
3
|
+
2026-10-15T09:00:04Z END s1 login
|
|
4
|
+
2026-10-15T09:00:05Z START s3 download
|
|
5
|
+
2026-10-15T09:00:20Z END s2 upload
|
|
6
|
+
2026-10-15T09:00:15Z END s3 download
|
|
7
|
+
|
|
8
|
+
2026-10-15T09:05:00Z START s4 login
|
|
9
|
+
2026-10-15T09:05:09Z END s4 login
|
|
10
|
+
2026-10-15T09:06:00Z START s5 upload
|
|
11
|
+
2026-10-15T09:07:00Z END s6 download
|
|
12
|
+
2026-10-15T09:08:00Z START s7 login
|
|
13
|
+
2026-10-15T09:08:03Z END s7 login
|
|
14
|
+
2026-10-15T09:08:30Z START s7 login
|
|
15
|
+
2026-10-15T09:08:36Z END s7 login
|
|
16
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
|
|
3
|
+
// 覆盖全部隐含面:乱序交错行按 (sessionId, phase) 配对;空行/纯空白行跳过;行字段 trim
|
|
4
|
+
// (兼容 CRLF);未闭合 START 与孤儿 END 零贡献;同 (sessionId, phase) 多轮累加。
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
const raw = readFileSync('data/events.log', 'utf8');
|
|
8
|
+
const open = new Map(); // "session|phase" -> start ms
|
|
9
|
+
const agg = new Map(); // phase -> { rounds, totalSeconds }
|
|
10
|
+
|
|
11
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
12
|
+
const t = line.trim();
|
|
13
|
+
if (!t) continue; // 空行 / 纯空白行
|
|
14
|
+
const parts = t.split(/\s+/);
|
|
15
|
+
if (parts.length < 4) continue;
|
|
16
|
+
const [ts, kind, session, phase] = parts;
|
|
17
|
+
const ms = Date.parse(ts);
|
|
18
|
+
if (!Number.isFinite(ms)) continue;
|
|
19
|
+
const key = `${session}|${phase}`;
|
|
20
|
+
if (kind === 'START') {
|
|
21
|
+
if (!open.has(key)) open.set(key, ms); // 重复 START 以首者计,防覆盖丢轮
|
|
22
|
+
} else if (kind === 'END') {
|
|
23
|
+
const start = open.get(key);
|
|
24
|
+
if (start === undefined) continue; // 孤儿 END 零贡献
|
|
25
|
+
open.delete(key);
|
|
26
|
+
const a = agg.get(phase) ?? { rounds: 0, totalSeconds: 0 };
|
|
27
|
+
a.rounds += 1;
|
|
28
|
+
a.totalSeconds += Math.round((ms - start) / 1000);
|
|
29
|
+
agg.set(phase, a);
|
|
30
|
+
}
|
|
31
|
+
// 未闭合 START:循环结束自然零贡献
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const phases = [...agg.entries()]
|
|
35
|
+
.map(([phase, a]) => ({ phase, rounds: a.rounds, totalSeconds: a.totalSeconds }))
|
|
36
|
+
.sort((x, y) => (x.phase < y.phase ? -1 : x.phase > y.phase ? 1 : 0));
|
|
37
|
+
process.stdout.write(JSON.stringify({ phases }) + '\n');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "004-phase-stats",
|
|
3
|
+
"title": "日志会话耗时统计 CLI(种植缺陷探针:隐含面在 fixture,不在指令)",
|
|
4
|
+
"tags": ["greenfield", "cli", "planted-defect", "implied-inputs"],
|
|
5
|
+
"instruction": "在当前目录新建 bin/phase-stats.mjs(Node ≥ 18,只用 Node 内置模块,禁止安装任何第三方依赖)。\n\n读取 data/events.log:每行一条事件,格式为「<ISO-8601 UTC 时间戳> <START|END> <sessionId> <phase>」(单个空格分隔,UTF-8)。同一 sessionId 的同名 phase 从 START 到 END 为一轮。\n\n输出一行 JSON 到 stdout(除此之外无任何输出):\n{\"phases\":[{\"phase\":\"<名>\",\"rounds\":<完成轮数>,\"totalSeconds\":<所有完成轮次时长之和,整数秒>}]}\n\nphases 按 phase 字典序升序排列。数据以仓库附带的 data/events.log 为准。确保 node bin/phase-stats.mjs 可运行。"
|
|
6
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
|
|
2
|
+
// 期望值基于 data/events.log 固定内容硬编码(download r1/10s、login r4/22s、upload r1/17s),
|
|
3
|
+
// 保证判分独立于被测实现。种植缺陷探针(2026-10-15-eval-planted-defect-probe):
|
|
4
|
+
// 指令只命名解析格式与输出契约;五类隐含面(乱序交错/空行/纯空白行/未闭合 START/孤儿 END)
|
|
5
|
+
// 全部由 fixture 承载——失败时下方断言消息逐项定位是哪条隐含面挂了。
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { cwd } from 'node:process';
|
|
10
|
+
|
|
11
|
+
const run = spawnSync('node', [join('bin', 'phase-stats.mjs')], { encoding: 'utf8' });
|
|
12
|
+
assert.equal(run.status, 0, `CLI 应成功运行(exit=${run.status}): ${String(run.stderr).slice(0, 300)}`);
|
|
13
|
+
|
|
14
|
+
const out = String(run.stdout).trim();
|
|
15
|
+
assert.equal(out.split('\n').length, 1, 'stdout 只允许一行 JSON');
|
|
16
|
+
let parsed;
|
|
17
|
+
assert.doesNotThrow(() => { parsed = JSON.parse(out); }, 'stdout 应为合法 JSON');
|
|
18
|
+
assert.ok(Array.isArray(parsed.phases), 'phases 应为数组');
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
parsed.phases.map((p) => p.phase),
|
|
21
|
+
['download', 'login', 'upload'],
|
|
22
|
+
'phases 应按字典序升序且恰好三个域(隐含面:乱序/空行/空白行的容错解析)'
|
|
23
|
+
);
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
parsed.phases[0],
|
|
26
|
+
{ phase: 'download', rounds: 1, totalSeconds: 10 },
|
|
27
|
+
'download: s3 一轮 10s(隐含面:孤儿 END s6 零贡献,不得计轮不得崩)'
|
|
28
|
+
);
|
|
29
|
+
assert.deepEqual(
|
|
30
|
+
parsed.phases[1],
|
|
31
|
+
{ phase: 'login', rounds: 4, totalSeconds: 22 },
|
|
32
|
+
'login: s1(4s)+s4(9s)+s7 两轮(3s+6s)=22s(隐含面:跨 session 交错乱序按 (sessionId,phase) 配对、同 session 多轮累加)'
|
|
33
|
+
);
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
parsed.phases[2],
|
|
36
|
+
{ phase: 'upload', rounds: 1, totalSeconds: 17 },
|
|
37
|
+
'upload: s2 一轮 17s(隐含面:未闭合 START s5 零贡献,不得计入轮数或时长)'
|
|
38
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.csv -text
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
|
|
3
|
+
// 覆盖全部隐含面:剥 UTF-8 BOM;容忍 CRLF 行尾;跳过空行;CSV 引号语义(引号内逗号非
|
|
4
|
+
// 分隔符,引号内金额剥千分位逗号);输出前自建 out/ 目录。
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/** 引号感知的单行 CSV 切分(覆盖本任务数据形态:引号包裹含逗号字段)。 */
|
|
9
|
+
function splitCsvLine(line) {
|
|
10
|
+
const fields = [];
|
|
11
|
+
let cur = '';
|
|
12
|
+
let inQuotes = false;
|
|
13
|
+
for (let i = 0; i < line.length; i++) {
|
|
14
|
+
const ch = line[i];
|
|
15
|
+
if (inQuotes) {
|
|
16
|
+
if (ch === '"') {
|
|
17
|
+
if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
|
|
18
|
+
} else { cur += ch; }
|
|
19
|
+
} else if (ch === '"') {
|
|
20
|
+
inQuotes = true;
|
|
21
|
+
} else if (ch === ',') {
|
|
22
|
+
fields.push(cur); cur = '';
|
|
23
|
+
} else { cur += ch; }
|
|
24
|
+
}
|
|
25
|
+
fields.push(cur);
|
|
26
|
+
return fields;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const raw = readFileSync('data/orders.csv', 'utf8').replace(/^\uFEFF/, ''); // 剥 BOM
|
|
30
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== ''); // CRLF 容忍 + 空行跳过
|
|
31
|
+
const header = splitCsvLine(lines[0]);
|
|
32
|
+
const col = (name) => header.indexOf(name);
|
|
33
|
+
const iRegion = col('region');
|
|
34
|
+
const iAmount = col('amount');
|
|
35
|
+
const iStatus = col('status');
|
|
36
|
+
|
|
37
|
+
const agg = new Map(); // region -> { total, count }
|
|
38
|
+
for (const line of lines.slice(1)) {
|
|
39
|
+
const f = splitCsvLine(line);
|
|
40
|
+
const status = (f[iStatus] ?? '').trim();
|
|
41
|
+
if (status !== 'paid') continue;
|
|
42
|
+
const region = (f[iRegion] ?? '').trim();
|
|
43
|
+
const amount = Number.parseFloat((f[iAmount] ?? '').replace(/,/g, '')); // 引号内千分位剥除
|
|
44
|
+
if (!Number.isFinite(amount)) continue;
|
|
45
|
+
const a = agg.get(region) ?? { total: 0, count: 0 };
|
|
46
|
+
a.total += amount;
|
|
47
|
+
a.count += 1;
|
|
48
|
+
agg.set(region, a);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const regions = [...agg.entries()]
|
|
52
|
+
.map(([region, a]) => ({ region, total: Math.round(a.total * 100) / 100, count: a.count }))
|
|
53
|
+
.sort((x, y) => (x.region < y.region ? -1 : x.region > y.region ? 1 : 0));
|
|
54
|
+
|
|
55
|
+
const outPath = join('out', 'summary.json');
|
|
56
|
+
mkdirSync(dirname(outPath), { recursive: true }); // 隐含面:目录不存在须自建
|
|
57
|
+
writeFileSync(outPath, JSON.stringify({ regions }) + '\n');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "005-orders-export",
|
|
3
|
+
"title": "订单汇总导出 CLI(种植缺陷探针 II:数据陷阱族在 fixture,指令零提示)",
|
|
4
|
+
"tags": ["greenfield", "cli", "planted-defect", "implied-inputs", "data-traps"],
|
|
5
|
+
"instruction": "在当前目录新建 bin/export-orders.mjs(Node ≥ 18,只用 Node 内置模块,禁止安装任何第三方依赖)。\n\n读取 data/orders.csv(UTF-8,逗号分隔,首行为表头:id,region,amount,status)。按 region 汇总 status=paid 的订单金额总和,输出到 out/summary.json:\n{\"regions\":[{\"region\":\"<名>\",\"total\":<金额总和,数值>,\"count\":<订单数,整数>}]}\n\nregions 按 region 字母序升序。数据以仓库附带的 data/orders.csv 为准。确保 node bin/export-orders.mjs 可运行且产物符合契约。"
|
|
6
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
|
|
2
|
+
// 期望值基于 data/orders.csv 固定内容硬编码(APAC total=2200 count=2;EU total=2284.5 count=3),
|
|
3
|
+
// 判分独立于被测实现。种植缺陷探针 II(2026-10-15-eval-probe2-orders-export):
|
|
4
|
+
// 指令只命名表头/过滤/输出契约;五类隐含面(out/ 目录自建、UTF-8 BOM、CRLF 行尾、
|
|
5
|
+
// 引号内千分位逗号、混排空行)全部由 fixture 承载——失败时下方断言逐项定位。
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { cwd } from 'node:process';
|
|
11
|
+
|
|
12
|
+
const run = spawnSync('node', [join('bin', 'export-orders.mjs')], { encoding: 'utf8' });
|
|
13
|
+
assert.equal(run.status, 0, `CLI 应成功运行(exit=${run.status}): ${String(run.stderr).slice(0, 300)}`);
|
|
14
|
+
|
|
15
|
+
const summaryPath = join(cwd(), 'out', 'summary.json');
|
|
16
|
+
assert.ok(existsSync(summaryPath), 'out/summary.json 应存在(隐含面:out/ 目录不存在须自建)');
|
|
17
|
+
|
|
18
|
+
let parsed;
|
|
19
|
+
assert.doesNotThrow(() => { parsed = JSON.parse(readFileSync(summaryPath, 'utf8')); }, 'out/summary.json 应为合法 JSON');
|
|
20
|
+
assert.ok(Array.isArray(parsed.regions), 'regions 应为数组');
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
parsed.regions,
|
|
23
|
+
[
|
|
24
|
+
{ region: 'APAC', total: 200 + 2000, count: 2 },
|
|
25
|
+
{ region: 'EU', total: 1234.5 + 50 + 1000, count: 3 },
|
|
26
|
+
],
|
|
27
|
+
'regions 应按字母序且金额/计数正确(隐含面:BOM 剥除、CRLF 容忍、引号内逗号按 CSV 语义、空行跳过、pending/refunded 排除)'
|
|
28
|
+
);
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { unlinkSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
const p = '.sillyspec/changes/2026-10-15-eval-workdir-naming/review.json'
|
|
5
|
+
const BS = String.fromCharCode(92)
|
|
6
|
+
let raw = readFileSync(p, 'utf8')
|
|
7
|
+
const bad = '[^' + BS + 'd]'
|
|
8
|
+
const good = '[^' + BS + BS + 'd]'
|
|
9
|
+
if (raw.includes(bad)) {
|
|
10
|
+
raw = raw.replace(bad, good)
|
|
11
|
+
writeFileSync(p, raw)
|
|
12
|
+
console.log('已修复 \\d → \\\\d')
|
|
13
|
+
} else {
|
|
14
|
+
console.log('未找到待修复序列')
|
|
15
|
+
}
|
|
16
|
+
JSON.parse(readFileSync(p, 'utf8'))
|
|
17
|
+
console.log('JSON.parse 通过')
|
|
18
|
+
try {
|
|
19
|
+
unlinkSync('quicklog/tmp-scan-review.mjs')
|
|
20
|
+
console.log('临时脚本已删')
|
|
21
|
+
} catch {}
|
package/src/ceremony-tier.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* (R-02:上线前用 friction-ledger 历史变更回放标定再定值)。
|
|
38
38
|
*/
|
|
39
39
|
import { RISK_TO_TIER } from './change-risk-profile.js'
|
|
40
|
+
import { modulePathMatches } from './modules.js'
|
|
40
41
|
import { matchSpanRiskPatterns } from './span-risk-surface.js'
|
|
41
42
|
|
|
42
43
|
/** 仪式档位序(只升不降的偏序基准,index 即高低:S0=0 … S3=3) */
|
|
@@ -124,7 +125,7 @@ function matchModuleForFile(posix, modulesObj) {
|
|
|
124
125
|
for (const raw of prefixes) {
|
|
125
126
|
const p = String(raw).replace(/\\/g, '/').replace(/\/+$/, '')
|
|
126
127
|
if (!p) continue
|
|
127
|
-
if (posix
|
|
128
|
+
if (modulePathMatches(posix, p)) return id
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
return null
|
package/src/decision-distill.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* 零输出:无 decisions.md 或无入选条目 → skipped 带原因,不写任何文件、不动 INDEX。
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { modulePathMatches } from './modules.js'
|
|
25
26
|
import { basename, dirname, join } from 'node:path'
|
|
26
27
|
|
|
27
28
|
/** FR-02:有实现影响的五类 type(scope 与未知 type 的 confirmed/accepted 不入选) */
|
|
@@ -36,7 +37,29 @@ const UNMAPPED_DOMAIN = 'unmapped'
|
|
|
36
37
|
/** 旧格式容错占位值(FR-01:缺锚点/缺 headHash 不阻断) */
|
|
37
38
|
const NOT_RECORDED = '未记录'
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* 锚点自动回填候选(2026-10-15-decision-anchor-backfill 单一源;收割端 flow-parity 与
|
|
42
|
+
* 蒸馏渲染端共用)。条目正文(答案/理由散文)中的路径形态 token——「恰一个唯一」即高置信
|
|
43
|
+
* 落点,返回该 token;多候选/无 token 返回 null(保守:错锚比无锚更误导 impact 查询与
|
|
44
|
+
* 悬空检查)。收益率探针(2026-10-15):恰一路径口径救 28%(本仓 174/616、平台 150/543)。
|
|
45
|
+
* 过滤黑名单(存量干跑实测坏锚类):知识治理面(.sillyspec/)、运行时面(.runtime/)、
|
|
46
|
+
* 归档与变更工件(changes/、allowed_paths 段)、知识互引(knowledge/、decisions/、fr/)、
|
|
47
|
+
* 消费仓坑文档目录(docs/sillyspec/)、前导 / 路径碎片、遥测日志扩展(.jsonl/.log)、
|
|
48
|
+
* repo:// 跨仓协议前缀剥除。token 须带扩展名收尾(:行号 形态锚点契约原生支持)。
|
|
49
|
+
* @param {string} text 条目正文拼接(答案/理由/question 等)
|
|
50
|
+
* @returns {string|null}
|
|
51
|
+
*/
|
|
52
|
+
export function anchorBackfillCandidate(text) {
|
|
53
|
+
const raw = String(text || '').match(/[A-Za-z0-9_\-.\\/:]+/g) || []
|
|
54
|
+
const pathed = [...new Set(raw
|
|
55
|
+
.map((t) => t.replace(/\\/g, '/').replace(/^repo:\/\//, '').replace(/\/+$/, '').replace(/:$/, ''))
|
|
56
|
+
.filter((t) => t.includes('/')
|
|
57
|
+
&& /\.[A-Za-z0-9]{1,10}(:\d+)?$/.test(t)
|
|
58
|
+
&& !/^(\.sillyspec|\.runtime|changes|knowledge|decisions|fr|docs\/sillyspec|allowed_paths)(\/|$)/.test(t)
|
|
59
|
+
&& !t.startsWith('/')
|
|
60
|
+
&& !/\.(jsonl|log)$/i.test(t)))]
|
|
61
|
+
return pathed.length === 1 ? pathed[0] : null
|
|
62
|
+
}/** 多行/带空白值压成单行字段值(条目行是单行契约) */
|
|
40
63
|
function oneLine(s) {
|
|
41
64
|
return String(s ?? '').replace(/\s*\r?\n\s*/g, ' ').trim()
|
|
42
65
|
}
|
|
@@ -274,7 +297,7 @@ function matchImpactsToModules(impacts, moduleIndex) {
|
|
|
274
297
|
const hits = []
|
|
275
298
|
for (const t of tokens) {
|
|
276
299
|
for (const { id, p } of modPaths) {
|
|
277
|
-
if (t
|
|
300
|
+
if (modulePathMatches(t, p)) { if (!hits.includes(id)) hits.push(id); break }
|
|
278
301
|
}
|
|
279
302
|
}
|
|
280
303
|
if (hits.length === 0) {
|
|
@@ -320,7 +343,10 @@ function renderBlockLines(entry, headHash, supersedesNote, changeName, sectionNu
|
|
|
320
343
|
// 原编号(2026-10-14-decision-global-id-epoch):epoch 形态溯源字段——变更行后/锚点行前
|
|
321
344
|
// (不破「文件:紧跟锚点」行序红线);legacy 复用段(originId=null)不渲染,形态零变化
|
|
322
345
|
if (originId) lines.push(`原编号:${oneLine(originId)}`)
|
|
323
|
-
|
|
346
|
+
// 锚点自动回填(2026-10-15-decision-anchor-backfill):源条目未填锚点时,从条目正文
|
|
347
|
+
// (答案/理由)提取路径形态 token——「恰一个唯一」即高置信落点(正文明确只谈一个文件),
|
|
348
|
+
// 多候选/无 token 保持未记录(保守:错锚比无锚更误导 impact 查询与悬空检查)。
|
|
349
|
+
lines.push(`锚点:${oneLine(entry.anchor) || anchorBackfillCandidate([entry.answer, entry.reason, entry.normalizedRequirement, entry.question].join('\n')) || NOT_RECORDED}`)
|
|
324
350
|
// 文件行(FR-01/D-001@v1):仅非空渲染——存量条目零迁移,幂等重归档不添空行
|
|
325
351
|
if (entry.files && entry.files.length > 0) lines.push(`文件:${entry.files.join(', ')}`)
|
|
326
352
|
lines.push(`最近确认:${oneLine(headHash) || NOT_RECORDED}`)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decision-redomain.js — 决策条目跨域迁移(2026-10-15-decision-redomain)
|
|
3
|
+
*
|
|
4
|
+
* 背景:模块路径 glob 匹配失效(2026-10-15-module-path-glob-match)导致消费仓历史决策
|
|
5
|
+
* 大量落 decisions/unmapped.md(平台仓 442 条实证)——匹配器修复止增量后,存量需机器
|
|
6
|
+
* 通道清偿。redomain.js 只管 fr/ 条目;本模块为 decisions/ 面的孪生实现,复用
|
|
7
|
+
* decision-distill 参数化底座(splitKnowledgeSections/joinKnowledgeFile/syncIndexRoutingLines)。
|
|
8
|
+
*
|
|
9
|
+
* 与 FR 版的形态差异:
|
|
10
|
+
* - 段头 `## D-xxx@vN 标题`(全局号/局部号双形态混存)——默认 sectionRegex 即此格式;
|
|
11
|
+
* id = `<number>@v<version>`(迁域不换号,字段行内「原编号」等机器行随段整体搬)。
|
|
12
|
+
* - 跨变更同号合法共存(D 号是变更内局部序号)——冲突键 = id + 变更(目标域已有
|
|
13
|
+
* 同 id 同变更段 = 迁移语义不明,跳该段并报告,不覆盖)。
|
|
14
|
+
* - 路由行形态与 fr 不同(`- <域>|decision|决策 → [decisions/<域>.md]`)——syncIndexRoutingLines
|
|
15
|
+
* 默认参数即 decisions 面。
|
|
16
|
+
*
|
|
17
|
+
* 幂等:重跑同批——源域已无该段 → 0 条(或 anchor/byChange 无命中报错,与 FR 版同款)。
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, unlinkSync } from 'fs'
|
|
20
|
+
import { join } from 'path'
|
|
21
|
+
import { splitKnowledgeSections, joinKnowledgeFile, syncIndexRoutingLines } from './decision-distill.js'
|
|
22
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
23
|
+
|
|
24
|
+
const DECISION_HEADER_RE = /^## (D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+)@v(\d+)\s*(.*)$/
|
|
25
|
+
|
|
26
|
+
/** 目标域文件头(与 decision-distill 落域文件既有头逐字同款——平台真实库实测形态) */
|
|
27
|
+
function newDomainPreamble(domain) {
|
|
28
|
+
return [
|
|
29
|
+
`# 决策知识 — ${domain}`,
|
|
30
|
+
'',
|
|
31
|
+
'> decision-distill 从变更 decisions.md 幂等提炼(「最近确认」= 归档时 HEAD)。条目字段行为 docs-check 机械解析契约,勿手改。',
|
|
32
|
+
'',
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 段 → {id, number, version, title, change}(id=number@version 即迁域不换号的身份) */
|
|
37
|
+
function sectionMeta(s) {
|
|
38
|
+
const m = DECISION_HEADER_RE.exec(s.lines[0] || '')
|
|
39
|
+
if (!m) return null
|
|
40
|
+
const id = `${m[1]}@v${m[2]}`
|
|
41
|
+
const cm = s.lines.find((l) => /^变更[::]/.test(l)) || s.lines.find((l) => /^来源[::]/.test(l))
|
|
42
|
+
const change = cm ? cm.replace(/^(变更|来源)[::]\s*/, '').trim() || null : null
|
|
43
|
+
return { id, number: m[1], version: parseInt(m[2], 10), title: (m[3] || '').trim(), change }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeDomainArg(v) {
|
|
47
|
+
return String(v ?? '').trim().replace(/^decisions\//i, '').replace(/\.md$/i, '').trim()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 干跑预览:列出将迁移条目,不落盘(anchor/byChange 同执行侧过滤) */
|
|
51
|
+
export function planDecisionRedomain({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
|
|
52
|
+
from = normalizeDomainArg(from)
|
|
53
|
+
to = normalizeDomainArg(to)
|
|
54
|
+
if (!from || !to) throw new Error('用法:--from <源域> --to <目标域>(域名可带 decisions/ 前缀与 .md 后缀)')
|
|
55
|
+
if (from === to) throw new Error('源域与目标域相同')
|
|
56
|
+
const dir = join(knowledgeRoot, 'decisions')
|
|
57
|
+
const fromPath = join(dir, `${from}.md`)
|
|
58
|
+
if (!existsSync(fromPath)) throw new Error(`源域文件不存在:decisions/${from}.md`)
|
|
59
|
+
const parsed = splitKnowledgeSections(readFileSync(fromPath, 'utf8'))
|
|
60
|
+
const anchorSet = Array.isArray(anchors) && anchors.length > 0 ? new Set(anchors) : null
|
|
61
|
+
const entries = []
|
|
62
|
+
for (const s of parsed.sections) {
|
|
63
|
+
const meta = sectionMeta(s)
|
|
64
|
+
if (!meta) continue
|
|
65
|
+
if (anchorSet && !anchorSet.has(meta.id)) continue
|
|
66
|
+
if (byChange && meta.change !== byChange) continue
|
|
67
|
+
entries.push(meta)
|
|
68
|
+
}
|
|
69
|
+
if (entries.length === 0) {
|
|
70
|
+
throw new Error(anchorSet
|
|
71
|
+
? `anchor 未命中(${[...anchorSet].join('、')} 不在 decisions/${from}.md)`
|
|
72
|
+
: byChange ? `「变更:${byChange}」条目在 decisions/${from}.md 无命中` : `decisions/${from}.md 无决策条目段`)
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
entries,
|
|
76
|
+
targetExists: existsSync(join(dir, `${to}.md`)),
|
|
77
|
+
sourceWillDelete: parsed.sections.length - entries.length === 0,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 执行迁移(纯文件手术——段字节整体搬运,id/机器行零改写) */
|
|
82
|
+
export function redomainDecisionEntries({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
|
|
83
|
+
from = normalizeDomainArg(from)
|
|
84
|
+
to = normalizeDomainArg(to)
|
|
85
|
+
if (!from || !to) throw new Error('用法:--from <源域> --to <目标域>')
|
|
86
|
+
if (from === to) throw new Error('源域与目标域相同')
|
|
87
|
+
const dir = join(knowledgeRoot, 'decisions')
|
|
88
|
+
const fromPath = join(dir, `${from}.md`)
|
|
89
|
+
const toPath = join(dir, `${to}.md`)
|
|
90
|
+
if (!existsSync(fromPath)) throw new Error(`源域文件不存在:decisions/${from}.md`)
|
|
91
|
+
|
|
92
|
+
const fromParsed = splitKnowledgeSections(readFileSync(fromPath, 'utf8'))
|
|
93
|
+
const anchorSet = Array.isArray(anchors) && anchors.length > 0 ? new Set(anchors) : null
|
|
94
|
+
const targetCreated = !existsSync(toPath)
|
|
95
|
+
const toParsed = targetCreated
|
|
96
|
+
? { preamble: newDomainPreamble(to), sections: [] }
|
|
97
|
+
: splitKnowledgeSections(readFileSync(toPath, 'utf8'))
|
|
98
|
+
|
|
99
|
+
const moving = []
|
|
100
|
+
const staying = []
|
|
101
|
+
const skippedConflicts = []
|
|
102
|
+
const movedKeys = new Set() // 批内双键(id|change):同批重复段第二条跳过(2026-10-15 平台存量实证幂等残留)
|
|
103
|
+
for (const s of fromParsed.sections) {
|
|
104
|
+
const meta = sectionMeta(s)
|
|
105
|
+
if (!meta) { staying.push(s); continue } // 非条目段(说明/杂项)留守
|
|
106
|
+
if (anchorSet && !anchorSet.has(meta.id)) { staying.push(s); continue }
|
|
107
|
+
if (byChange && meta.change !== byChange) { staying.push(s); continue }
|
|
108
|
+
// 冲突防线:目标域已有同 id + 同变更段(跨变更同号合法共存,双键才冲突)
|
|
109
|
+
const key = `${meta.id}|${meta.change || ''}`
|
|
110
|
+
const clash = movedKeys.has(key) || toParsed.sections.some((t) => {
|
|
111
|
+
const tm = sectionMeta(t)
|
|
112
|
+
return tm && `${tm.id}|${tm.change || ''}` === key
|
|
113
|
+
})
|
|
114
|
+
if (clash) { skippedConflicts.push(meta.id); staying.push(s); continue }
|
|
115
|
+
movedKeys.add(key)
|
|
116
|
+
moving.push({ meta, section: s })
|
|
117
|
+
}
|
|
118
|
+
if (moving.length === 0 && skippedConflicts.length === 0) {
|
|
119
|
+
throw new Error(anchorSet
|
|
120
|
+
? `anchor 未命中(${[...anchorSet].join('、')} 不在 decisions/${from}.md)`
|
|
121
|
+
: byChange ? `「变更:${byChange}」条目在 decisions/${from}.md 无命中` : `decisions/${from}.md 无决策条目段`)
|
|
122
|
+
}
|
|
123
|
+
if (anchorSet && skippedConflicts.length + moving.length !== anchorSet.size) {
|
|
124
|
+
const got = new Set([...moving.map((m) => m.meta.id), ...skippedConflicts])
|
|
125
|
+
const miss = [...anchorSet].filter((a) => !got.has(a))
|
|
126
|
+
if (miss.length > 0) throw new Error(`anchor 未命中:${miss.join('、')}(不在 decisions/${from}.md)`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
toParsed.sections.push(...moving.map((m) => m.section))
|
|
130
|
+
writeAtomicSync(toPath, joinKnowledgeFile(toParsed.preamble, toParsed.sections))
|
|
131
|
+
const sourceDeleted = staying.length === 0
|
|
132
|
+
if (sourceDeleted) unlinkSync(fromPath)
|
|
133
|
+
else writeAtomicSync(fromPath, joinKnowledgeFile(fromParsed.preamble, staying))
|
|
134
|
+
|
|
135
|
+
// INDEX 路由行:decisions 面默认参数(幂等;缺席行补上)
|
|
136
|
+
const indexPath = join(knowledgeRoot, 'INDEX.md')
|
|
137
|
+
const before = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : ''
|
|
138
|
+
try { syncIndexRoutingLines(knowledgeRoot) } catch { /* INDEX 缺席/同步失败不拦迁移 */ }
|
|
139
|
+
const after = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : ''
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
moved: moving.map((m) => ({ id: m.meta.id, title: m.meta.title })),
|
|
143
|
+
skippedConflicts,
|
|
144
|
+
sourceDeleted,
|
|
145
|
+
targetCreated,
|
|
146
|
+
indexLineAdded: after !== before,
|
|
147
|
+
}
|
|
148
|
+
}
|