sillyspec 3.27.2 → 3.27.5
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-execute/SKILL.md +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +45 -5
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +90 -29
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/foreign-declared.js +78 -0
- package/src/index.js +89 -9
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +50 -4
- package/src/run/command.js +122 -43
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +5 -2
- package/src/run/gates.js +87 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +11 -0
- package/src/run/shared.js +152 -10
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +2 -1
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +7 -0
- package/src/stages/brainstorm.js +11 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +32 -6
- package/src/stages/plan-postcheck.js +76 -17
- package/src/stages/plan.js +1 -0
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +31 -14
- package/src/sync.js +41 -3
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +10 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +46 -4
- package/src/worktree-apply.js +184 -7
- package/src/worktree-deps.js +57 -0
- package/src/worktree.js +201 -58
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "001-slugify",
|
|
3
|
+
"title": "实现 slugify 工具函数",
|
|
4
|
+
"tags": ["greenfield", "utility", "edge-case"],
|
|
5
|
+
"instruction": "在当前目录新建 src/slugify.mjs(package.json 已存在且 type=module,禁止安装任何第三方依赖),实现并导出具名函数 slugify(同时提供指向同一函数的 default 导出)。\n\n函数签名:slugify(value, options = {}),返回字符串。\n\n转换规则(按顺序执行,分隔符记为 sep,默认 '-'):\n1. value 为 null 或 undefined 时返回空字符串;其余值先 String() 转字符串。\n2. 整体 toLowerCase() 小写化。\n3. 连续空白字符(\\s+)替换为单个 sep。\n4. 删除所有不属于 [a-z0-9] 且不等于 sep 的字符。\n5. 连续多个 sep 合并为一个,并去掉首尾的 sep。\n\noptions 支持:\n- options.hyphen:分隔符 sep,默认 '-'。必须是长度为 1 的字符串,否则抛出 TypeError('hyphen must be a single character')。\n\n示例:\nslugify(' Hello World! ') === 'hello-world'\nslugify('a--b') === 'a-b'\nslugify('Ünïcode Test') === 'ncode-test'\nslugify(null) === ''\nslugify(42) === '42'\nslugify('Hello World', { hyphen: '_' }) === 'hello_world'"
|
|
6
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 判分脚本:以「被测工作目录」为 cwd 运行(node <本文件>,cwd=workdir)。退出码 0 = 通过。
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { cwd } from 'node:process';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const mod = await import(pathToFileURL(join(cwd(), 'src', 'slugify.mjs')).href);
|
|
8
|
+
const slugify = mod.slugify ?? mod.default?.slugify;
|
|
9
|
+
assert.equal(typeof slugify, 'function', '应导出具名函数 slugify');
|
|
10
|
+
assert.equal(typeof mod.default, 'function', '应提供 default 导出');
|
|
11
|
+
|
|
12
|
+
assert.equal(slugify(null), '');
|
|
13
|
+
assert.equal(slugify(undefined), '');
|
|
14
|
+
assert.equal(slugify(''), '');
|
|
15
|
+
assert.equal(slugify(42), '42');
|
|
16
|
+
assert.equal(slugify(' Hello World! '), 'hello-world');
|
|
17
|
+
assert.equal(slugify('Foo Bar BAZ'), 'foo-bar-baz');
|
|
18
|
+
assert.equal(slugify('a--b'), 'a-b');
|
|
19
|
+
assert.equal(slugify('!!!'), '');
|
|
20
|
+
assert.equal(slugify('Ünïcode Test'), 'ncode-test');
|
|
21
|
+
assert.equal(slugify('tab\tand\nnewline'), 'tab-and-newline');
|
|
22
|
+
assert.equal(slugify('-leading and trailing-'), 'leading-and-trailing');
|
|
23
|
+
assert.equal(slugify('Hello World', { hyphen: '_' }), 'hello_world');
|
|
24
|
+
assert.equal(slugify('a b', { hyphen: '+' }), 'a+b');
|
|
25
|
+
assert.throws(() => slugify('a b', { hyphen: '--' }), TypeError);
|
|
26
|
+
assert.throws(() => slugify('a b', { hyphen: '' }), TypeError);
|
|
27
|
+
|
|
28
|
+
console.log('001-slugify verify OK');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// 参考解(dry-run 假 agent 使用,也供人工对照)。
|
|
2
|
+
const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/;
|
|
3
|
+
const DAY_MS = 86_400_000;
|
|
4
|
+
|
|
5
|
+
function parseUtc(value) {
|
|
6
|
+
if (typeof value !== 'string') throw new TypeError('invalid date');
|
|
7
|
+
const m = DATE_PREFIX.exec(value.trim());
|
|
8
|
+
if (!m) throw new TypeError('invalid date');
|
|
9
|
+
const y = Number(m[1]);
|
|
10
|
+
const mo = Number(m[2]);
|
|
11
|
+
const d = Number(m[3]);
|
|
12
|
+
const ts = Date.UTC(y, mo - 1, d);
|
|
13
|
+
const dt = new Date(ts);
|
|
14
|
+
// 往返校验拦住 '2024-02-30' 这类被 Date 自动进位的假日期
|
|
15
|
+
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) {
|
|
16
|
+
throw new TypeError('invalid date');
|
|
17
|
+
}
|
|
18
|
+
return ts;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function eachDay(start, end) {
|
|
22
|
+
const s = parseUtc(start);
|
|
23
|
+
const e = parseUtc(end);
|
|
24
|
+
if (e < s) throw new RangeError('end must not be before start');
|
|
25
|
+
const days = [];
|
|
26
|
+
for (let t = s; t <= e; t += DAY_MS) {
|
|
27
|
+
days.push(new Date(t).toISOString().slice(0, 10));
|
|
28
|
+
}
|
|
29
|
+
return days;
|
|
30
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "002-each-day",
|
|
3
|
+
"title": "实现 eachDay 日期区间函数(棕地补全)",
|
|
4
|
+
"tags": ["brownfield", "date", "validation"],
|
|
5
|
+
"instruction": "src/dates.mjs 已有函数骨架 eachDay(start, end)(当前直接 throw),在原文件完成实现,保持具名导出 eachDay 不变,禁止引入第三方依赖。\n\n契约:\n- 入参为字符串,接受 'YYYY-MM-DD',或以它开头的时间形式(如 '2024-01-01T10:00:00Z'、'2024-01-01 08:00');只取日期部分参与运算。\n- 返回闭区间 [start, end] 内每一天的 'YYYY-MM-DD' 字符串数组(含两端;起止同天返回单元素数组)。\n- 日期运算一律用 UTC(避免时区/DST 干扰)。\n- end 的日期小于 start 时抛 RangeError。\n- 格式非法(不匹配 'YYYY-MM-DD' 前缀、日期不存在如 '2024-02-30'、或非字符串)抛 TypeError('invalid date')。\n\n示例:\neachDay('2024-02-27', '2024-03-02') → ['2024-02-27','2024-02-28','2024-02-29','2024-03-01','2024-03-02']\neachDay('2024-01-15', '2024-01-15') → ['2024-01-15']"
|
|
6
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { cwd } from 'node:process';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const mod = await import(pathToFileURL(join(cwd(), 'src', 'dates.mjs')).href);
|
|
8
|
+
const eachDay = mod.eachDay ?? mod.default?.eachDay;
|
|
9
|
+
assert.equal(typeof eachDay, 'function', '应导出具名函数 eachDay');
|
|
10
|
+
|
|
11
|
+
assert.deepEqual(
|
|
12
|
+
eachDay('2024-02-27', '2024-03-02'),
|
|
13
|
+
['2024-02-27', '2024-02-28', '2024-02-29', '2024-03-01', '2024-03-02'],
|
|
14
|
+
);
|
|
15
|
+
assert.deepEqual(
|
|
16
|
+
eachDay('2024-12-30', '2025-01-02'),
|
|
17
|
+
['2024-12-30', '2024-12-31', '2025-01-01', '2025-01-02'],
|
|
18
|
+
);
|
|
19
|
+
assert.deepEqual(eachDay('2024-01-15', '2024-01-15'), ['2024-01-15']);
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
eachDay('2024-01-01T10:00:00Z', '2024-01-03T23:00:00Z'),
|
|
22
|
+
['2024-01-01', '2024-01-02', '2024-01-03'],
|
|
23
|
+
);
|
|
24
|
+
// 2100 是「逢百年不闰」的非闰年
|
|
25
|
+
assert.deepEqual(eachDay('2100-02-28', '2100-03-01'), ['2100-02-28', '2100-03-01']);
|
|
26
|
+
assert.throws(() => eachDay('2024-03-01', '2024-02-28'), RangeError);
|
|
27
|
+
assert.throws(() => eachDay('2024-02-30', '2024-03-01'), TypeError);
|
|
28
|
+
assert.throws(() => eachDay('2024-1-1', '2024-01-02'), TypeError);
|
|
29
|
+
assert.throws(() => eachDay(null, '2024-01-02'), TypeError);
|
|
30
|
+
|
|
31
|
+
console.log('002-each-day verify OK');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 参考解(dry-run 假 agent 使用,也供人工对照)。
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const out = { input: 'data/notes.txt', top: 5, minLen: 1 };
|
|
7
|
+
for (let i = 0; i < argv.length; i++) {
|
|
8
|
+
const a = argv[i];
|
|
9
|
+
if (a === '--input') out.input = argv[++i];
|
|
10
|
+
else if (a === '--top') out.top = Number(argv[++i]);
|
|
11
|
+
else if (a === '--min-len') out.minLen = Number(argv[++i]);
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { input, top, minLen } = parseArgs(process.argv.slice(2));
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = readFileSync(input, 'utf8');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(`无法读取输入文件: ${input}(${err.code ?? err.message})`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const counts = new Map();
|
|
26
|
+
for (const word of text.toLowerCase().split(/\s+/)) {
|
|
27
|
+
if (!word || word.length < minLen) continue;
|
|
28
|
+
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
const totalWords = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
31
|
+
const topWords = [...counts.entries()]
|
|
32
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
|
|
33
|
+
.slice(0, top)
|
|
34
|
+
.map(([word, count]) => ({ word, count }));
|
|
35
|
+
|
|
36
|
+
process.stdout.write(`${JSON.stringify({ totalWords, top: topWords })}\n`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "003-word-report",
|
|
3
|
+
"title": "实现词频统计 CLI",
|
|
4
|
+
"tags": ["greenfield", "cli", "output-contract"],
|
|
5
|
+
"instruction": "当前目录 data/notes.txt 已提供输入样本。新建 bin/report.mjs(纯 Node 实现,禁止第三方依赖,package.json 已是 type=module)。\n\n功能:词频统计 CLI。用法:node bin/report.mjs [--input <路径>] [--top N] [--min-len M]\n- --input:输入文件路径,默认 data/notes.txt(相对当前工作目录解析)。\n- 读取文本,小写化后按空白切分为词。\n- --min-len:过滤掉长度小于 M 的词,默认 1。\n- --top:输出频次最高的前 N 个词,默认 5;频次相同时按词的字典序升序排列。\n- stdout 输出且仅输出一行 JSON:{\"totalWords\":<过滤后的总词数>,\"top\":[{\"word\":<词>,\"count\":<次数>}, ...]}\n- 输入文件不存在或不可读时:向 stderr 输出错误信息,退出码 1。"
|
|
6
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
|
|
2
|
+
// 期望值基于 data/notes.txt 的固定内容硬编码,保证判分独立于被测实现。
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { cwd } from 'node:process';
|
|
7
|
+
|
|
8
|
+
const reportCli = join(cwd(), 'bin', 'report.mjs');
|
|
9
|
+
const run = (args) => spawnSync('node', [reportCli, ...args], { encoding: 'utf8' });
|
|
10
|
+
|
|
11
|
+
const run1 = run(['--top', '3']);
|
|
12
|
+
assert.equal(run1.status, 0, `CLI 应成功运行: ${run1.stderr}`);
|
|
13
|
+
assert.ok(run1.stdout.trim().startsWith('{'), 'stdout 应为 JSON');
|
|
14
|
+
assert.equal(run1.stdout.trim().split('\n').length, 1, '只允许输出一行 JSON');
|
|
15
|
+
assert.deepEqual(JSON.parse(run1.stdout), {
|
|
16
|
+
totalWords: 29,
|
|
17
|
+
top: [
|
|
18
|
+
{ word: 'the', count: 4 },
|
|
19
|
+
{ word: 'apple', count: 3 },
|
|
20
|
+
{ word: 'quick', count: 3 },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const run2 = run(['--top', '2', '--min-len', '5']);
|
|
25
|
+
assert.equal(run2.status, 0);
|
|
26
|
+
assert.deepEqual(JSON.parse(run2.stdout), {
|
|
27
|
+
totalWords: 9,
|
|
28
|
+
top: [
|
|
29
|
+
{ word: 'apple', count: 3 },
|
|
30
|
+
{ word: 'quick', count: 3 },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const run3 = run(['--input', 'no-such-file.txt']);
|
|
35
|
+
assert.notEqual(run3.status, 0, '文件不存在应非零退出');
|
|
36
|
+
assert.ok((run3.stderr ?? '').trim().length > 0, '应向 stderr 输出错误信息');
|
|
37
|
+
|
|
38
|
+
console.log('003-word-report verify OK');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sillyspec",
|
|
3
|
-
"version": "3.27.
|
|
3
|
+
"version": "3.27.5",
|
|
4
4
|
"description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
|
|
5
5
|
"icon": "logo.jpg",
|
|
6
6
|
"homepage": "https://sillyspec.ppdmq.top/",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"scripts": {
|
|
27
27
|
"test": "node test/run-tests.mjs",
|
|
28
|
-
"lint": "node test/check-syntax.mjs"
|
|
28
|
+
"lint": "node test/check-syntax.mjs",
|
|
29
|
+
"eval:dry": "node eval/run.mjs --dry-run",
|
|
30
|
+
"eval:pilot": "node eval/run.mjs --pilot"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"@inquirer/prompts": "^7.10.1",
|
package/src/agent-session-log.js
CHANGED
|
@@ -71,10 +71,14 @@ function toPosix(p) {
|
|
|
71
71
|
return String(p).replace(/\\/g, '/');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/** cwd 等值比较口径:正斜杠归一 + Windows
|
|
74
|
+
/** cwd 等值比较口径:正斜杠归一 + Windows 形态路径大小写不敏感。
|
|
75
|
+
* 按数据形态判而非宿主平台——平台 hub 场景 daemon 跑在任意平台,都要能与 Windows
|
|
76
|
+
* agent 上报的 cwd(盘符 `c:/x` 或 UNC `\\server\share`)等值比较;toPosix 在前,
|
|
77
|
+
* UNC 的反斜杠此时已成 `//`。POSIX 形态路径保持大小写敏感(文件系统语义如此)。 */
|
|
75
78
|
function normCwd(p) {
|
|
76
79
|
const s = toPosix(p);
|
|
77
|
-
|
|
80
|
+
const winForm = /^[a-zA-Z]:\//.test(s) || s.startsWith('//');
|
|
81
|
+
return winForm ? s.toLowerCase() : s;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function cwdsMatch(a, b) {
|
|
@@ -85,6 +89,17 @@ function statSafe(p) {
|
|
|
85
89
|
try { return statSync(p); } catch { return null; }
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 会话化上下文 id 消毒(2026-08-23-agent-activity-sessions):非空字符串 trim 后返回,
|
|
94
|
+
* 其余(undefined/null/空白)返回 null——best-effort 语义:ctx 解析失败不带、不抛错。
|
|
95
|
+
* 只用于标识类值(hubSessionId / changeKey / quickId),非 flag 值。
|
|
96
|
+
*/
|
|
97
|
+
function pickCtxId(v) {
|
|
98
|
+
if (typeof v !== 'string') return null;
|
|
99
|
+
const t = v.trim();
|
|
100
|
+
return t || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* 读文件首行并 JSON.parse(只读前 maxBytes 字节,防大日志整读)。
|
|
90
105
|
* codex rollout 首行是 session_meta(含 cwd/originator),这是 cwd 精确匹配的数据源。
|
|
@@ -574,7 +589,8 @@ export function readAgentLogArtifact(runtimeRoot) {
|
|
|
574
589
|
* 登记 agent 会话日志路径进 <runtimeRoot>/agent-session-log.json(run 命令入口调用)。
|
|
575
590
|
*
|
|
576
591
|
* 合并语义:按 log_path 去重——同路径 invocations+1 / 刷新 last_seen_at 与文件 stat;
|
|
577
|
-
* 新路径追加;entries 按 last_seen_at
|
|
592
|
+
* 新路径追加;entries 按 last_seen_at 新→旧排序(同毫秒平局按 seq 登记序决胜),
|
|
593
|
+
* 上限 10 条(超出淘汰最旧)。
|
|
578
594
|
* 探测不到(非 agent 环境)不写盘不上报。全程 best-effort:失败 warn 一行返回 null 不抛。
|
|
579
595
|
* 落盘后立即 REST 上报平台(POST /api/agent-logs,主通道),上报失败本地产物兜底。
|
|
580
596
|
*
|
|
@@ -584,6 +600,10 @@ export function readAgentLogArtifact(runtimeRoot) {
|
|
|
584
600
|
* @param {object} [p.platformOpts] - 平台参数(workspaceId/scanRunId 进产物元信息)
|
|
585
601
|
* @param {string} p.specBase - 规范基路径(本地模式 cwd/.sillyspec;平台模式 specRoot)
|
|
586
602
|
* @param {string} [p.command] - 触发登记的命令描述(flag 名级,不含 flag 值)
|
|
603
|
+
* @param {object} [p.context] - 会话化上下文(2026-08-23-agent-activity-sessions,可选):
|
|
604
|
+
* {hubSessionId, changeKey, quickId}——hubSessionId 为 body 级(run 所属平台会话唯一,
|
|
605
|
+
* daemon 注入 env SILLYHUB_SESSION_ID 的兜底也在此读取);changeKey/quickId 为 entry 级
|
|
606
|
+
* (本次 run 检出/更新的 entry 随 run 持久化,quick 会话 quickId 优先互斥)。缺省全不带。
|
|
587
607
|
* @param {object} [p.env] - 探测用 env(默认 process.env;测试注入用)
|
|
588
608
|
* @param {string} [p.homeDir] - 探测用 home(默认 homedir();测试注入用)
|
|
589
609
|
* @param {number} [p.now] - 探测用时钟(默认 Date.now();测试注入用)
|
|
@@ -591,13 +611,20 @@ export function readAgentLogArtifact(runtimeRoot) {
|
|
|
591
611
|
* @returns {Promise<{artifactPath:string, detected:number, latestLogPath:string|null, isNew:boolean, pushed:boolean|null}|null>}
|
|
592
612
|
* null = 未探测到 / 失败(调用方无需区分)
|
|
593
613
|
*/
|
|
594
|
-
export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts = {}, specBase, command, env, homeDir, now, windowMs } = {}) {
|
|
614
|
+
export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts = {}, specBase, command, context = {}, env, homeDir, now, windowMs } = {}) {
|
|
595
615
|
try {
|
|
596
616
|
if (!cwd || !specBase) return null;
|
|
597
617
|
const cwdCandidates = [...new Set([invokedCwd, cwd].filter(Boolean))];
|
|
598
618
|
const detected = detectAgentLogEntries({ cwdCandidates, env, homeDir, now, windowMs });
|
|
599
619
|
if (detected.length === 0) return null;
|
|
600
620
|
|
|
621
|
+
// 会话化上下文(best-effort 消毒,解析失败不带):entry 级 ctx(changeKey/quickId)随
|
|
622
|
+
// 检出/更新该 entry 的本次 run 持久化;hubSessionId 为 body 级——context 显式传入优先,
|
|
623
|
+
// 缺省回落 env SILLYHUB_SESSION_ID(daemon 派发平台会话时的注入通道,见 protocol 文档 §1)。
|
|
624
|
+
const hubSessionId = pickCtxId(context.hubSessionId) || pickCtxId((env || process.env).SILLYHUB_SESSION_ID);
|
|
625
|
+
const ctxChangeKey = pickCtxId(context.changeKey);
|
|
626
|
+
const ctxQuickId = pickCtxId(context.quickId);
|
|
627
|
+
|
|
601
628
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase);
|
|
602
629
|
const artifactPath = join(runtimeRoot, AGENT_LOG_ARTIFACT_FILENAME);
|
|
603
630
|
const nowIso = new Date().toISOString();
|
|
@@ -607,6 +634,11 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
607
634
|
const existing = readAgentLogArtifact(runtimeRoot);
|
|
608
635
|
const byPath = new Map((existing?.entries || []).map(e => [e.log_path, e]));
|
|
609
636
|
|
|
637
|
+
// 平局决胜序号:last_seen_at 是毫秒精度 ISO,快机器上连续登记落在同一毫秒打平,
|
|
638
|
+
// 稳定排序会让旧条目留在首位(latestLogPath 假"未更新")。seq 按登记触及顺序单调
|
|
639
|
+
// 递增并随条目持久化,平局时后触及者胜(存量产物无 seq 视为 0,老条目间次序不变)。
|
|
640
|
+
let seq = (existing?.entries || []).reduce((m, e) => Math.max(m, Number(e.seq) || 0), 0);
|
|
641
|
+
|
|
610
642
|
for (const det of detected) {
|
|
611
643
|
const st = statSafe(det.log_path);
|
|
612
644
|
const prev = byPath.get(det.log_path);
|
|
@@ -619,6 +651,10 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
619
651
|
agent_cwd: det.agent_cwd,
|
|
620
652
|
session_id: det.session_id ?? prev?.session_id ?? null,
|
|
621
653
|
originator: det.originator ?? prev?.originator ?? null,
|
|
654
|
+
// entry 级 ctx(D-009):本次 run 非空者写入;context 未带(如无 --change 的 run)
|
|
655
|
+
// 保留原值——未被本次 run 触及的存量 entry 不进此循环,展开即保留原 ctx 不追新
|
|
656
|
+
change_key: ctxChangeKey ?? prev?.change_key ?? null,
|
|
657
|
+
quick_id: ctxQuickId ?? prev?.quick_id ?? null,
|
|
622
658
|
exists: Boolean(st),
|
|
623
659
|
size_bytes: st ? st.size : null,
|
|
624
660
|
mtime_ms: st ? st.mtimeMs : (det.mtime_ms ?? null),
|
|
@@ -626,11 +662,12 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
626
662
|
last_seen_at: nowIso,
|
|
627
663
|
invocations: (prev?.invocations || 0) + 1,
|
|
628
664
|
last_command: command || null,
|
|
665
|
+
seq: ++seq,
|
|
629
666
|
});
|
|
630
667
|
}
|
|
631
668
|
|
|
632
669
|
const entries = [...byPath.values()]
|
|
633
|
-
.sort((a, b) => (b.last_seen_at || '').localeCompare(a.last_seen_at || ''))
|
|
670
|
+
.sort((a, b) => (b.last_seen_at || '').localeCompare(a.last_seen_at || '') || ((b.seq || 0) - (a.seq || 0)))
|
|
634
671
|
.slice(0, MAX_ENTRIES);
|
|
635
672
|
|
|
636
673
|
const artifact = {
|
|
@@ -660,6 +697,9 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
660
697
|
agent_cwd: toPosix(invokedCwd || cwd),
|
|
661
698
|
workspace_id: platformOpts.workspaceId ?? null,
|
|
662
699
|
scan_run_id: platformOpts.scanRunId ?? null,
|
|
700
|
+
// body 级会话关联键(run 所属平台会话唯一;非空才带,schema_version 保持 1 纯可选增量)
|
|
701
|
+
...(hubSessionId ? { hub_session_id: hubSessionId } : {}),
|
|
702
|
+
// entries 各自带 entry 级 ctx(change_key/quick_id,随 entry 持久化的原值或本次 run 的新值)
|
|
663
703
|
entries: merged.entries,
|
|
664
704
|
},
|
|
665
705
|
});
|
|
@@ -122,7 +122,8 @@ export const RISK_LEVEL_CAUSES = {
|
|
|
122
122
|
'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
|
|
123
123
|
'注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design.md frontmatter 用 risk_level 显式声明真实等级(如 unit-sufficient)覆盖——不要靠删措辞绕门控,危险链路该有真实启动证据。',
|
|
124
124
|
'integration-critical':
|
|
125
|
-
'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。'
|
|
125
|
+
'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。' +
|
|
126
|
+
'关键词前方同句有否定提示(不/未/无/避免…)的命中已被抑制不参与判级——若本变更实际涉及跨进程/状态机改动,请在 design.md frontmatter 用 risk_level 声明真实等级。',
|
|
126
127
|
'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
|
|
127
128
|
'explicit': 'design.md frontmatter 的 risk_level 显式声明(覆盖关键词判级)。',
|
|
128
129
|
}
|
|
@@ -130,6 +131,69 @@ export const RISK_LEVEL_CAUSES = {
|
|
|
130
131
|
/** design.md frontmatter 可显式声明的合法 risk_level 值(与 detectChangeRisk 判级结果同集合) */
|
|
131
132
|
const RISK_LEVELS = ['doc-only', 'unit-sufficient', 'contract-required', 'integration-critical', 'deployment-critical']
|
|
132
133
|
|
|
134
|
+
// ============ 同句否定抑制(坑 risk-negation-blindness) ============
|
|
135
|
+
//
|
|
136
|
+
// detectChangeRisk 原为全文 blob 字面命中:design 写「本次不新增 daemon 协议」仍判
|
|
137
|
+
// integration-critical、强制全套集成证据,对无状态后端变更误伤(用户实证 2026-08-24)。
|
|
138
|
+
// 机械抑制规则(不做事语义理解,三段判定):
|
|
139
|
+
// ① 子句切分:按 句号/分号/逗号/换行 切子句,否定只在同子句内生效——「不改动 A。新增 B」
|
|
140
|
+
// 的 B 不继承否定;
|
|
141
|
+
// ② 直接否定:命中位置前方 NEGATION_WINDOW 字符内出现否定提示 → 抑制;
|
|
142
|
+
// ③ 枚举继承:「不改动 daemon / session / lifecycle」中后位关键词与前位被抑制关键词之间
|
|
143
|
+
// 只隔枚举连接符(空格 / 、 , 和 与 及 and or)→ 继承抑制——纯距离窗口盖不住长枚举,
|
|
144
|
+
// 而「不涉及 daemon 的情况下重构 session」的间隔是普通文字,不继承(session 正常命中)。
|
|
145
|
+
// 同一关键词的**全部**命中都被抑制才不参与判级(任一处未抑制命中仍算数)。降级不静默:
|
|
146
|
+
// design/verify 两道 gate 会把被抑制词打进 warning(可审计),design.md frontmatter
|
|
147
|
+
// risk_level 仍是双向权威覆盖通道(声明优先级最高,行为不变)。
|
|
148
|
+
//
|
|
149
|
+
// 否定提示词排除假阳:不同/不断/不久/不时/不仅(「不」的非否定合成词)、无状态/无法
|
|
150
|
+
// (「无状态后端」是常见肯定表述)、非常/非空/非法(「非空校验 session」是在改 session)。
|
|
151
|
+
const NEGATION_CUES = /不(?!同|断|久|时|仅|妨)|未|无(?!状态|法)|非(?!常|空|法)|避免|排除|无需|不再|no\s+new|not\b|don'?t|without|avoid/i
|
|
152
|
+
const NEGATION_WINDOW = 16
|
|
153
|
+
const CLAUSE_SPLIT_RE = /[。;;,,\n]/
|
|
154
|
+
const ENUM_GAP_RE = /^[\s/|、,,]*(?:(?:和|与|及|and|or)[\s/|、,,]*)*$/i
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 单行内统计各关键词命中(同子句否定窗口 + 枚举继承)。
|
|
158
|
+
* @returns {Map<string, {kept: number, suppressed: number}>} trigger 原文 → 命中统计
|
|
159
|
+
*/
|
|
160
|
+
function collectLineTriggerStats(line, patterns, stats = new Map()) {
|
|
161
|
+
for (const pattern of patterns) {
|
|
162
|
+
const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g')
|
|
163
|
+
let m
|
|
164
|
+
while ((m = re.exec(line)) !== null) {
|
|
165
|
+
if (m[0].length === 0) { re.lastIndex++; continue }
|
|
166
|
+
// 行内绝对偏移 → 子句内相对偏移(子句内可能有多命中,逐一判定)
|
|
167
|
+
const clauseStart = line.slice(0, m.index).split(CLAUSE_SPLIT_RE).pop().length
|
|
168
|
+
const clauseStartAbs = m.index - clauseStart
|
|
169
|
+
const clause = line.slice(clauseStartAbs).split(CLAUSE_SPLIT_RE)[0]
|
|
170
|
+
const relIndex = m.index - clauseStartAbs
|
|
171
|
+
const prefix = clause.slice(0, relIndex)
|
|
172
|
+
let negated = NEGATION_CUES.test(prefix.slice(-NEGATION_WINDOW))
|
|
173
|
+
if (!negated && prefix.length > 0) {
|
|
174
|
+
// 枚举继承:查同子句内更早的关键词命中,且间隔只有枚举连接符
|
|
175
|
+
for (const other of patterns) {
|
|
176
|
+
const ore = new RegExp(other.source, other.flags.includes('g') ? other.flags : other.flags + 'g')
|
|
177
|
+
let om
|
|
178
|
+
while ((om = ore.exec(clause)) !== null) {
|
|
179
|
+
if (om[0].length === 0) { ore.lastIndex++; continue }
|
|
180
|
+
if (om.index >= relIndex) break
|
|
181
|
+
const gap = clause.slice(om.index + om[0].length, relIndex)
|
|
182
|
+
const prevSuppressed = NEGATION_CUES.test(clause.slice(0, om.index).slice(-NEGATION_WINDOW))
|
|
183
|
+
if (prevSuppressed && ENUM_GAP_RE.test(gap)) { negated = true; break }
|
|
184
|
+
}
|
|
185
|
+
if (negated) break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const stat = stats.get(m[0]) || { kept: 0, suppressed: 0 }
|
|
189
|
+
if (negated) stat.suppressed++
|
|
190
|
+
else stat.kept++
|
|
191
|
+
stats.set(m[0], stat)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return stats
|
|
195
|
+
}
|
|
196
|
+
|
|
133
197
|
/**
|
|
134
198
|
* 从 design.md 顶部 frontmatter 提取显式 risk_level 声明。
|
|
135
199
|
* 只认文档最开头 `---\n...\n---` 块内的 `risk_level: <level>` 单行(与 plan.md 的 plan_level 同款解析),
|
|
@@ -148,10 +212,11 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
148
212
|
const triggers = []
|
|
149
213
|
|
|
150
214
|
// ── 显式豁免优先:design.md frontmatter 声明 risk_level → 以声明为准,跳过关键词判级 ──
|
|
151
|
-
// 历史教训:detectChangeRisk
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
215
|
+
// 历史教训:detectChangeRisk 是机械字面匹配——design 写「本次不改动 daemon/session」曾命中关键词
|
|
216
|
+
// 被误判 integration/deployment-critical,强制要全套集成证据,对无状态后端变更误伤。现给两条通道:
|
|
217
|
+
// ① 同句否定抑制(上方 NEGATION_CUES,机械可审计、只认「关键词前方否定窗口」不做语义理解);
|
|
218
|
+
// ② frontmatter 显式声明(显式、诚实、可审计,落在 design frontmatter + verify-result)——本分支。
|
|
219
|
+
// 声明仍走门控(结论 FAIL 仍拦),但豁免级(unit-sufficient 等)不再强制集成证据。
|
|
155
220
|
const explicit = extractExplicitRiskLevel(designContent)
|
|
156
221
|
if (explicit) {
|
|
157
222
|
const requiredVerification =
|
|
@@ -160,17 +225,20 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
160
225
|
explicit === 'contract-required' ? ['unit_tests', 'contract_tests'] :
|
|
161
226
|
explicit === 'doc-only' ? ['static_check'] :
|
|
162
227
|
['unit_tests']
|
|
163
|
-
return { level: explicit, triggers: ['risk_level (explicit)'], requiredVerification, explicit: true }
|
|
228
|
+
return { level: explicit, triggers: ['risk_level (explicit)'], suppressedTriggers: [], requiredVerification, explicit: true }
|
|
164
229
|
}
|
|
165
230
|
|
|
166
231
|
const combined = [designContent, planContent].join('\n')
|
|
167
232
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
// 逐行收集命中:同句否定窗口内的命中进 suppressed(全部命中均被抑制才剔除该 trigger)
|
|
234
|
+
const stats = new Map()
|
|
235
|
+
for (const line of combined.split('\n')) {
|
|
236
|
+
collectLineTriggerStats(line, INTEGRATION_CRITICAL_PATTERNS, stats)
|
|
237
|
+
}
|
|
238
|
+
const suppressedTriggers = []
|
|
239
|
+
for (const [t, stat] of stats) {
|
|
240
|
+
if (stat.kept > 0) triggers.push(t)
|
|
241
|
+
else suppressedTriggers.push(t)
|
|
174
242
|
}
|
|
175
243
|
|
|
176
244
|
for (const file of changedFiles) {
|
|
@@ -184,7 +252,8 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
184
252
|
}
|
|
185
253
|
|
|
186
254
|
if (triggers.length === 0) {
|
|
187
|
-
|
|
255
|
+
// 关键词命中全被否定语境抑制(或本就无命中)→ doc-only;抑制词随 suppressedTriggers 上报可审计
|
|
256
|
+
return { level: 'doc-only', triggers: [], suppressedTriggers, requiredVerification: ['static_check'] }
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
const deploymentTrigger = triggers.some(t => /cli\.ts|main\.ts|server\.(js|ts)|bootstrap|entrypoint/i.test(t))
|
|
@@ -207,7 +276,7 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
207
276
|
level = 'unit-sufficient'
|
|
208
277
|
}
|
|
209
278
|
|
|
210
|
-
return { level, triggers, requiredVerification }
|
|
279
|
+
return { level, triggers, suppressedTriggers, requiredVerification }
|
|
211
280
|
}
|
|
212
281
|
|
|
213
282
|
/**
|
package/src/config-schema.js
CHANGED
|
@@ -115,9 +115,9 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
115
115
|
{
|
|
116
116
|
id: 'test_strategy',
|
|
117
117
|
title: '测试策略',
|
|
118
|
-
note: 'verify 阶段 CLI
|
|
118
|
+
note: 'verify 阶段 CLI 对账的收窄策略(D-005@v2:skip 接线兑现 + evidence-auto 新增,full/module 语义不变)。',
|
|
119
119
|
keys: [
|
|
120
|
-
{ path: 'test_strategy', type: 'enum', values: ['full', 'module'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules
|
|
120
|
+
{ path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules);skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹,R-07);evidence-auto=按变更目录 module-impact.md 影响类型推荐检查组合(行为→module 聚焦测试、文档/prompt→docs-check、门禁契约→gate;缺失/不可解析降级 module 并注记)。缺省=全量。', example: 'full' },
|
|
121
121
|
],
|
|
122
122
|
},
|
|
123
123
|
{
|
|
@@ -157,6 +157,14 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
157
157
|
{ path: 'docs-check.cross_repo_roots', type: 'object', optional: true, status: 'live', readers: ['readDocsCheckConfig (src/docs-check.js)'], desc: 'repo://<仓库名> 跨仓引用 → 本机仓库根目录映射。文档里 repo://name/src/x.js:12 形态的引用:未配映射默认跳过(不同设备仓库位置不同,防跨设备误报);配了映射走与本地引用相同的行号+关键词校验。local.yaml 是 gitignored,绝对路径不入库,每台设备各自配。', example: 'sillyspec: /c/Users/you/IdeaProjects/sillyspec' },
|
|
158
158
|
],
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
id: 'decisions',
|
|
162
|
+
title: '决策库复核阈值',
|
|
163
|
+
note: 'docs-check 决策规则(W1.1 behind 复核)的消费配置——决策锚定模块源码在「最近确认」后前进超阈值 → doctor 报「决策待复核」(advisory 不阻断)。读键按「存在则读、不存在用缺省」容错。',
|
|
164
|
+
keys: [
|
|
165
|
+
{ path: 'decisions.behind_threshold', type: 'integer', optional: true, status: 'live', readers: ['readDecisionRulesConfig (src/docs-check.js)'], desc: '决策 behind 复核阈值(锚定模块源码在「最近确认」commit 后的前进数),缺省 10;超阈报「决策待复核」提示。', example: '10' },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
160
168
|
],
|
|
161
169
|
};
|
|
162
170
|
|
|
@@ -276,9 +284,16 @@ modules:
|
|
|
276
284
|
frontend: { path: "frontend/", test: "cd frontend && pnpm test" }
|
|
277
285
|
backend: { path: "backend/", test: "cd backend && npm test" }
|
|
278
286
|
|
|
279
|
-
# ──
|
|
287
|
+
# ── 测试策略(verify 实测收窄;full/module 语义不变,skip/evidence-auto 为 2026-08-23 新增)──
|
|
288
|
+
# full=全量 commands.test | module=按命中模块收窄(需配 modules)
|
|
289
|
+
# skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹)
|
|
290
|
+
# evidence-auto=按变更 module-impact.md 影响面推荐检查组合(行为→module 聚焦测试、文档→docs-check、门禁→gate;缺失降级 module)
|
|
280
291
|
test_strategy: full
|
|
281
292
|
|
|
293
|
+
# ── 决策库 behind 复核阈值(docs-check 决策规则:源码在「最近确认」后前进超阈值 → 待复核提示;缺省 10)──
|
|
294
|
+
decisions:
|
|
295
|
+
behind_threshold: 10
|
|
296
|
+
|
|
282
297
|
# ── 预存失败豁免清单(变更前就失败的测试行;务必定期复核,防误豁免真实失败)──
|
|
283
298
|
known_failures:
|
|
284
299
|
- tests/some-old-stable-failure
|