agent-project-sdlc 0.1.12 → 0.1.14
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 +11 -1
- package/assets/agents/AGENTS_CORE.md +3 -3
- package/assets/docs/README.md +10 -2
- package/assets/make/sdlc-harness.mk +4 -6
- package/assets/policies/allowed_paths.yaml +1 -0
- package/assets/policies/gates.yaml +2 -2
- package/assets/policies/phase_contracts.yaml +10 -7
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +20 -14
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +6 -2
- package/assets/skills/pjsdlc_manager/SKILL.md +2 -2
- package/assets/skills/pjsdlc_release_manager/SKILL.md +16 -16
- package/assets/skills/pjsdlc_reviewer/SKILL.md +8 -2
- package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +7 -2
- package/assets/skills/pjsdlc_tester/SKILL.md +31 -16
- package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +13 -5
- package/assets/templates/RELEASE_TEMPLATE.md +8 -1
- package/assets/templates/REVIEW_TEMPLATE.md +9 -1
- package/assets/templates/RFC_TEMPLATE.md +8 -1
- package/assets/templates/TEST_CASES_TEMPLATE.md +24 -0
- package/assets/templates/TEST_REPORT_TEMPLATE.md +41 -0
- package/assets/templates/TEST_STRATEGY_TEMPLATE.md +27 -0
- package/dist/lib/sync-engine.js +1 -1
- package/dist/lib/validators.js +262 -17
- package/package.json +1 -1
- package/assets/templates/TEST_PLAN_TEMPLATE.md +0 -29
package/dist/lib/validators.js
CHANGED
|
@@ -27,6 +27,62 @@ const DESIGN_CATEGORIES = [
|
|
|
27
27
|
architectureTerms: ["compliance", "permission", "authorization", "audit", "合规", "权限", "审计", "授权", "客户确认", "回执归档"]
|
|
28
28
|
}
|
|
29
29
|
];
|
|
30
|
+
const TESTING_DISALLOWED_ALLOWED_PATHS = [
|
|
31
|
+
"package.json",
|
|
32
|
+
"**/package.json",
|
|
33
|
+
"package-lock.json",
|
|
34
|
+
"**/package-lock.json",
|
|
35
|
+
"npm-shrinkwrap.json",
|
|
36
|
+
"**/npm-shrinkwrap.json",
|
|
37
|
+
"pnpm-lock.yaml",
|
|
38
|
+
"**/pnpm-lock.yaml",
|
|
39
|
+
"yarn.lock",
|
|
40
|
+
"**/yarn.lock",
|
|
41
|
+
"bun.lock",
|
|
42
|
+
"**/bun.lock",
|
|
43
|
+
"bun.lockb",
|
|
44
|
+
"**/bun.lockb",
|
|
45
|
+
"src/**",
|
|
46
|
+
"app/**",
|
|
47
|
+
"lib/**",
|
|
48
|
+
"server/**",
|
|
49
|
+
"bin/**",
|
|
50
|
+
"cli/**",
|
|
51
|
+
"runtime/**",
|
|
52
|
+
"scripts/**",
|
|
53
|
+
"tools/**",
|
|
54
|
+
"deploy/**",
|
|
55
|
+
"deployment/**",
|
|
56
|
+
"infra/**",
|
|
57
|
+
"ops/**",
|
|
58
|
+
"systemd/**",
|
|
59
|
+
".github/workflows/**",
|
|
60
|
+
"dockerfile",
|
|
61
|
+
"dockerfile.*",
|
|
62
|
+
"docker-compose*.yml",
|
|
63
|
+
"docker-compose*.yaml",
|
|
64
|
+
"*.service",
|
|
65
|
+
"tests/runtime/**",
|
|
66
|
+
"tests/**/runtime/**"
|
|
67
|
+
];
|
|
68
|
+
const TESTING_DISALLOWED_CHANGED_PATHS = [...TESTING_DISALLOWED_ALLOWED_PATHS, "scripts/**", "tools/**"];
|
|
69
|
+
const TESTING_RUNTIME_FILE_TERMS = ["bootstrap", "cloud", "daemon", "poller", "provider", "runtime", "service", "systemd"];
|
|
70
|
+
const TESTING_ALLOWED_TEST_FILE_TERMS = ["assertion", "fixture", "mock", "smoke"];
|
|
71
|
+
const TEST_REPORT_PATH = ".docs/07_test/TEST_REPORT.md";
|
|
72
|
+
const CURRENT_RELEASE_REPORT_PATH = ".docs/08_release/CURRENT_RELEASE.md";
|
|
73
|
+
const TEST_REPORT_PLACEHOLDER_TERMS = ["pending", "tbd", "todo", "待填", "待补", "placeholder"];
|
|
74
|
+
const TEST_FACT_SOURCE_PHASES = new Set(["TESTING", "RFC_RECALIBRATION"]);
|
|
75
|
+
const TEST_FACT_SOURCE_PATTERNS = [".docs/07_test/**", ".docs/07_test/"];
|
|
76
|
+
const TEST_FACT_SOURCE_REF = /\.docs\/07_test\/[^\s`,)]+/g;
|
|
77
|
+
const RUNNABLE_ENTRY_EXIT_TERMS = [
|
|
78
|
+
"runnable entry/exit",
|
|
79
|
+
"entry/exit",
|
|
80
|
+
"entry points",
|
|
81
|
+
"entry point",
|
|
82
|
+
"可运行入口/出口",
|
|
83
|
+
"入口/出口",
|
|
84
|
+
"not applicable"
|
|
85
|
+
];
|
|
30
86
|
const validators = {
|
|
31
87
|
"validate-harness": validateHarness,
|
|
32
88
|
"validate-current": validateCurrent,
|
|
@@ -73,6 +129,9 @@ async function validateCurrent(projectRoot) {
|
|
|
73
129
|
const root = await harnessRoot(projectRoot);
|
|
74
130
|
const lifecycle = await readYamlObject(path.join(projectRoot, root, "state", "lifecycle.yaml"));
|
|
75
131
|
const current = String(lifecycle.current_phase ?? "");
|
|
132
|
+
if (current === "SPRINTING") {
|
|
133
|
+
return validateDevInternal(projectRoot, { phaseExit: true });
|
|
134
|
+
}
|
|
76
135
|
const gateByPhase = {
|
|
77
136
|
REQUIREMENT_GATHERING: "validate-pm",
|
|
78
137
|
ARCHITECTING: "validate-design",
|
|
@@ -213,6 +272,7 @@ function validateDraftTaskShape(task, index, errors) {
|
|
|
213
272
|
if (!hasImplementationDoc && !hasResultDocs) {
|
|
214
273
|
errors.push(`${String(task.id ?? prefix)} must define implementation_doc or result_docs`);
|
|
215
274
|
}
|
|
275
|
+
errors.push(...testFactSourceErrorsForTask(task));
|
|
216
276
|
if (OPEN_TASK_STATUSES.has(String(task.status))) {
|
|
217
277
|
if ("gate_result" in task)
|
|
218
278
|
errors.push(`${String(task.id ?? prefix)} open task must not define gate_result`);
|
|
@@ -262,10 +322,49 @@ async function validateCrossCuttingArchitecture(projectRoot, productFiles, techP
|
|
|
262
322
|
return errors;
|
|
263
323
|
}
|
|
264
324
|
async function validateDev(projectRoot) {
|
|
325
|
+
return validateDevInternal(projectRoot, { phaseExit: false });
|
|
326
|
+
}
|
|
327
|
+
async function validateDevInternal(projectRoot, options) {
|
|
265
328
|
const root = await harnessRoot(projectRoot);
|
|
266
|
-
const
|
|
329
|
+
const lifecycle = await readYamlObject(path.join(projectRoot, root, "state", "lifecycle.yaml"));
|
|
330
|
+
const plan = await validatePlanState(projectRoot, !options.phaseExit);
|
|
331
|
+
const phaseErrors = String(lifecycle.current_phase ?? "") === "SPRINTING" ? [] : ["validate-dev requires lifecycle current_phase SPRINTING"];
|
|
332
|
+
const openTaskErrors = options.phaseExit ? [] : validateDevOpenTaskState(plan.plan);
|
|
333
|
+
const pathErrors = options.phaseExit ? [] : await validateChangedPaths(projectRoot, plan.plan, true);
|
|
267
334
|
const draftErrors = await validateDevDraftConsumed(projectRoot, root);
|
|
268
|
-
|
|
335
|
+
const implementationDocErrors = await validateImplementationDocRunnableEntryExit(projectRoot);
|
|
336
|
+
return {
|
|
337
|
+
info: [`validate-dev checked ${plan.taskCount} task(s)${options.phaseExit ? " for phase exit" : ""}`],
|
|
338
|
+
errors: [...phaseErrors, ...plan.errors, ...openTaskErrors, ...pathErrors, ...draftErrors, ...implementationDocErrors]
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function validateDevOpenTaskState(plan) {
|
|
342
|
+
const errors = [];
|
|
343
|
+
const tasks = Array.isArray(plan.tasks) ? plan.tasks.filter(isRecord) : [];
|
|
344
|
+
const open = tasks.filter((task) => OPEN_TASK_STATUSES.has(String(task.status)));
|
|
345
|
+
if (open.length === 0)
|
|
346
|
+
return errors;
|
|
347
|
+
const currentTaskId = String(plan.current_task_id ?? "");
|
|
348
|
+
if (!currentTaskId) {
|
|
349
|
+
errors.push("validate-dev requires current_task_id when SPRINTING has an open task");
|
|
350
|
+
return errors;
|
|
351
|
+
}
|
|
352
|
+
const currentTask = open.find((task) => String(task.id ?? "") === currentTaskId);
|
|
353
|
+
if (!currentTask) {
|
|
354
|
+
errors.push(`current_task_id does not match an open SPRINTING task: ${currentTaskId}`);
|
|
355
|
+
return errors;
|
|
356
|
+
}
|
|
357
|
+
const otherOpen = open.filter((task) => String(task.id ?? "") !== currentTaskId);
|
|
358
|
+
if (otherOpen.length > 0) {
|
|
359
|
+
errors.push(`validate-dev supports only the current open task during SPRINTING: ${open.map((task) => task.id).join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
if (String(currentTask.phase ?? "") !== "SPRINTING") {
|
|
362
|
+
errors.push(`${currentTaskId} must have phase SPRINTING for validate-dev`);
|
|
363
|
+
}
|
|
364
|
+
if (typeof currentTask.implementation_doc !== "string" || !currentTask.implementation_doc.trim()) {
|
|
365
|
+
errors.push(`${currentTaskId} must define implementation_doc for validate-dev`);
|
|
366
|
+
}
|
|
367
|
+
return errors;
|
|
269
368
|
}
|
|
270
369
|
async function validateDevDraftConsumed(projectRoot, root) {
|
|
271
370
|
const errors = [];
|
|
@@ -291,38 +390,55 @@ async function validateReview(projectRoot) {
|
|
|
291
390
|
errors.push("Review report must include findings or risks");
|
|
292
391
|
if (!containsAny(text, ["test gap", "测试缺口", "coverage"]))
|
|
293
392
|
errors.push("Review report must include test gaps or coverage notes");
|
|
393
|
+
if (!containsAny(text, ["entry/exit", "entrypoint", "入口", "出口", "runnable", "可运行"])) {
|
|
394
|
+
errors.push("Review report must assess runnable entry/exit readiness before TESTING");
|
|
395
|
+
}
|
|
294
396
|
if (!containsAny(text, ["pass", "blocked", "通过", "阻塞"]))
|
|
295
397
|
errors.push("Review report must include PASS/BLOCKED decision");
|
|
296
398
|
return { info: ["validate-review checked review report"], errors };
|
|
297
399
|
}
|
|
298
400
|
async function validateTest(projectRoot) {
|
|
401
|
+
const root = await harnessRoot(projectRoot);
|
|
402
|
+
const lifecycle = await readYamlObject(path.join(projectRoot, root, "state", "lifecycle.yaml"));
|
|
299
403
|
const plan = await validatePlanState(projectRoot, false);
|
|
300
|
-
const text = (await readText(path.join(projectRoot, ".docs/07_test/TEST_PLAN.md"))).toLowerCase();
|
|
301
404
|
const errors = [...plan.errors];
|
|
405
|
+
const report = await readTestReport(projectRoot);
|
|
406
|
+
const text = report ? report.text.toLowerCase() : "";
|
|
407
|
+
if (!report)
|
|
408
|
+
errors.push(`Missing test report: expected executed evidence at ${TEST_REPORT_PATH}`);
|
|
409
|
+
if (containsAny(text, TEST_REPORT_PLACEHOLDER_TERMS)) {
|
|
410
|
+
errors.push("Test report must contain executed evidence, not pending/TBD/TODO/placeholder content");
|
|
411
|
+
}
|
|
302
412
|
if (!containsAny(text, ["matrix", "矩阵"]))
|
|
303
|
-
errors.push("Test
|
|
413
|
+
errors.push("Test report must include a test matrix");
|
|
304
414
|
if (!containsAny(text, ["regression", "回归"]))
|
|
305
|
-
errors.push("Test
|
|
415
|
+
errors.push("Test report must include regression evidence");
|
|
306
416
|
if (!containsAny(text, ["coverage gap", "覆盖缺口", "gap"]))
|
|
307
|
-
errors.push("Test
|
|
417
|
+
errors.push("Test report must include coverage gaps");
|
|
418
|
+
if (!containsAny(text, ["entry/exit", "entrypoint", "入口", "出口", "runnable", "可运行"])) {
|
|
419
|
+
errors.push("Test report must state existing runnable entry/exit coverage or blocker status");
|
|
420
|
+
}
|
|
308
421
|
if (!containsAny(text, ["pass", "blocked", "通过", "阻塞"]))
|
|
309
|
-
errors.push("Test
|
|
310
|
-
|
|
422
|
+
errors.push("Test report must include PASS/BLOCKED decision");
|
|
423
|
+
if (lifecycle.current_phase === "TESTING") {
|
|
424
|
+
errors.push(...testingBoundaryErrorsForChangedFiles(await changedFiles(projectRoot)));
|
|
425
|
+
}
|
|
426
|
+
return { info: [`validate-test checked ${report?.source ?? "missing test report"}`], errors };
|
|
311
427
|
}
|
|
312
428
|
async function validateRelease(projectRoot) {
|
|
313
429
|
const plan = await validatePlanState(projectRoot, false);
|
|
314
|
-
const
|
|
315
|
-
const text =
|
|
430
|
+
const report = await readReleaseReport(projectRoot);
|
|
431
|
+
const text = report?.text ?? "";
|
|
316
432
|
const errors = [...plan.errors];
|
|
317
|
-
if (
|
|
318
|
-
errors.push(
|
|
433
|
+
if (!report)
|
|
434
|
+
errors.push(`Missing current release report: expected ${CURRENT_RELEASE_REPORT_PATH} or legacy .docs/08_release/*.md`);
|
|
319
435
|
if (!containsAny(text, ["release", "发布"]))
|
|
320
|
-
errors.push("
|
|
436
|
+
errors.push("Current release report must include release notes");
|
|
321
437
|
if (!containsAny(text, ["smoke", "冒烟"]))
|
|
322
|
-
errors.push("
|
|
438
|
+
errors.push("Current release report must include smoke test evidence");
|
|
323
439
|
if (!containsAny(text, ["rollback", "回滚"]))
|
|
324
|
-
errors.push("
|
|
325
|
-
return { info: [`validate-release checked ${
|
|
440
|
+
errors.push("Current release report must include rollback plan");
|
|
441
|
+
return { info: [`validate-release checked ${report?.source ?? "missing current release report"}`], errors };
|
|
326
442
|
}
|
|
327
443
|
async function validateRfc(projectRoot) {
|
|
328
444
|
const plan = await validatePlanState(projectRoot, false);
|
|
@@ -339,6 +455,21 @@ async function validateRfc(projectRoot) {
|
|
|
339
455
|
errors.push("RFC must include technical impact candidates");
|
|
340
456
|
if (!containsAny(text, ["regression", "回归"]))
|
|
341
457
|
errors.push("RFC must include regression requirements");
|
|
458
|
+
if (!containsAny(text, ["test fact source impact", "测试事实源影响"])) {
|
|
459
|
+
errors.push("RFC must include Test Fact Source Impact");
|
|
460
|
+
}
|
|
461
|
+
const indexPath = path.join(projectRoot, ".docs/INDEX.md");
|
|
462
|
+
const indexText = (await pathExists(indexPath)) ? await readText(indexPath) : "";
|
|
463
|
+
if (!indexText)
|
|
464
|
+
errors.push("Missing .docs/INDEX.md for RFC test fact source validation");
|
|
465
|
+
for (const superseded of await supersededTestDocs(docs)) {
|
|
466
|
+
if (await pathExists(path.join(projectRoot, superseded))) {
|
|
467
|
+
errors.push(`Superseded test doc still exists in current facts: ${superseded}`);
|
|
468
|
+
}
|
|
469
|
+
if (indexText.includes(superseded)) {
|
|
470
|
+
errors.push(`Superseded test doc still linked from .docs/INDEX.md: ${superseded}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
342
473
|
const statuses = [...text.matchAll(/status:\s*([a-z_]+)/g)].map((match) => match[1].toUpperCase());
|
|
343
474
|
if (statuses.length === 0)
|
|
344
475
|
errors.push("RFC must include a Status line");
|
|
@@ -397,6 +528,7 @@ async function validatePlanState(projectRoot, allowOpen) {
|
|
|
397
528
|
if (match) {
|
|
398
529
|
maxTaskSequence = Math.max(maxTaskSequence, Number(match[1]));
|
|
399
530
|
}
|
|
531
|
+
errors.push(...testFactSourceErrorsForTask(task));
|
|
400
532
|
if (["pending", "in_progress", "blocked", "pending_revision"].includes(String(task.status))) {
|
|
401
533
|
if ("gate_result" in task) {
|
|
402
534
|
errors.push(`Open task ${task.id} must not define gate_result`);
|
|
@@ -417,6 +549,7 @@ async function validatePlanState(projectRoot, allowOpen) {
|
|
|
417
549
|
if (!Array.isArray(task.acceptance_criteria) || task.acceptance_criteria.length === 0) {
|
|
418
550
|
errors.push(`Open task ${task.id} must define acceptance_criteria`);
|
|
419
551
|
}
|
|
552
|
+
errors.push(...testingBoundaryErrorsForAllowedPaths(task));
|
|
420
553
|
}
|
|
421
554
|
else {
|
|
422
555
|
errors.push(`Completed task ${task.id} must not remain in plan.yaml`);
|
|
@@ -564,6 +697,46 @@ async function readYamlObject(filePath) {
|
|
|
564
697
|
return {};
|
|
565
698
|
return (parseYaml(await readText(filePath)) ?? {});
|
|
566
699
|
}
|
|
700
|
+
async function readTestReport(projectRoot) {
|
|
701
|
+
const canonical = path.join(projectRoot, TEST_REPORT_PATH);
|
|
702
|
+
if (await pathExists(canonical)) {
|
|
703
|
+
return { text: await readText(canonical), source: TEST_REPORT_PATH };
|
|
704
|
+
}
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
async function readReleaseReport(projectRoot) {
|
|
708
|
+
const canonical = path.join(projectRoot, CURRENT_RELEASE_REPORT_PATH);
|
|
709
|
+
if (await pathExists(canonical)) {
|
|
710
|
+
return { text: await readText(canonical), source: CURRENT_RELEASE_REPORT_PATH };
|
|
711
|
+
}
|
|
712
|
+
const legacyDocs = await markdownFiles(path.join(projectRoot, ".docs/08_release"));
|
|
713
|
+
if (legacyDocs.length > 0) {
|
|
714
|
+
return { text: await combinedText(legacyDocs), source: `legacy .docs/08_release/*.md (${legacyDocs.length} file(s))` };
|
|
715
|
+
}
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
async function validateImplementationDocRunnableEntryExit(projectRoot) {
|
|
719
|
+
const docs = await markdownFiles(path.join(projectRoot, ".docs/04_implementation"));
|
|
720
|
+
const errors = [];
|
|
721
|
+
const indexPath = path.join(projectRoot, ".docs/INDEX.md");
|
|
722
|
+
const indexText = (await pathExists(indexPath)) ? await readText(indexPath) : "";
|
|
723
|
+
if (docs.length > 0 && !indexText) {
|
|
724
|
+
errors.push("Missing .docs/INDEX.md for implementation doc validation");
|
|
725
|
+
}
|
|
726
|
+
for (const doc of docs) {
|
|
727
|
+
const relative = repoRelative(projectRoot, doc);
|
|
728
|
+
const dotted = relative.startsWith(".") ? relative : `.${relative}`;
|
|
729
|
+
const withoutDocsPrefix = dotted.replace(/^\.docs\//, "");
|
|
730
|
+
if (indexText && !indexText.includes(dotted) && !indexText.includes(withoutDocsPrefix)) {
|
|
731
|
+
errors.push(`.docs/INDEX.md does not link implementation doc: ${dotted}`);
|
|
732
|
+
}
|
|
733
|
+
const text = await readText(doc);
|
|
734
|
+
if (!containsAny(text, RUNNABLE_ENTRY_EXIT_TERMS)) {
|
|
735
|
+
errors.push(`Implementation doc must include Runnable Entry/Exit facts or explicit Not applicable: ${dotted}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return errors;
|
|
739
|
+
}
|
|
567
740
|
async function markdownFiles(root) {
|
|
568
741
|
const files = await listFiles(root);
|
|
569
742
|
return files.filter((file) => {
|
|
@@ -579,6 +752,78 @@ function containsAny(text, needles) {
|
|
|
579
752
|
const lowered = text.toLowerCase();
|
|
580
753
|
return needles.some((needle) => lowered.includes(needle.toLowerCase()));
|
|
581
754
|
}
|
|
755
|
+
function testingBoundaryErrorsForAllowedPaths(task) {
|
|
756
|
+
if (task.phase !== "TESTING")
|
|
757
|
+
return [];
|
|
758
|
+
const allowed = Array.isArray(task.allowed_paths) ? task.allowed_paths.map((item) => String(item)) : [];
|
|
759
|
+
const blocked = allowed.filter((item) => isTestingBoundaryAllowedPath(item));
|
|
760
|
+
if (blocked.length === 0)
|
|
761
|
+
return [];
|
|
762
|
+
return [
|
|
763
|
+
`TESTING task allowed_paths must not include product runtime, package/deploy config, or long-running runtime paths: ${blocked.join(", ")}`
|
|
764
|
+
];
|
|
765
|
+
}
|
|
766
|
+
function testingBoundaryErrorsForChangedFiles(files) {
|
|
767
|
+
const blocked = files.filter((file) => isTestingRuntimeBoundaryChange(file));
|
|
768
|
+
if (blocked.length === 0)
|
|
769
|
+
return [];
|
|
770
|
+
return [
|
|
771
|
+
`TESTING changes must use existing product entrypoints only; move runtime, bootstrap, provider, deploy, or package script changes to SPRINTING/RFC: ${blocked.join(", ")}`
|
|
772
|
+
];
|
|
773
|
+
}
|
|
774
|
+
function testFactSourceErrorsForTask(task) {
|
|
775
|
+
const phase = String(task.phase ?? "");
|
|
776
|
+
if (TEST_FACT_SOURCE_PHASES.has(phase))
|
|
777
|
+
return [];
|
|
778
|
+
const candidates = [...asStringList(task.allowed_paths), ...asStringList(task.result_docs)];
|
|
779
|
+
const blocked = candidates.filter((candidate) => {
|
|
780
|
+
const normalized = candidate.replace(/\\/g, "/");
|
|
781
|
+
return normalized.startsWith(".docs/07_test/") || matchesAny(normalized, TEST_FACT_SOURCE_PATTERNS);
|
|
782
|
+
});
|
|
783
|
+
if (blocked.length === 0)
|
|
784
|
+
return [];
|
|
785
|
+
return [
|
|
786
|
+
`Only TESTING or RFC_RECALIBRATION tasks may target current test fact sources under .docs/07_test/**: ${blocked.join(", ")}`
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
async function supersededTestDocs(docs) {
|
|
790
|
+
const refs = new Set();
|
|
791
|
+
for (const doc of docs) {
|
|
792
|
+
const text = await readText(doc);
|
|
793
|
+
for (const line of text.split("\n")) {
|
|
794
|
+
const lowered = line.toLowerCase();
|
|
795
|
+
if (!lowered.includes("superseded") && !lowered.includes("被替代") && !lowered.includes("失效")) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
for (const match of line.matchAll(TEST_FACT_SOURCE_REF)) {
|
|
799
|
+
refs.add(normalizeDocRef(match[0]).replace(/[.,;:]$/, ""));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return [...refs].filter((ref) => ref.startsWith(".docs/07_test/"));
|
|
804
|
+
}
|
|
805
|
+
function isTestingBoundaryAllowedPath(file) {
|
|
806
|
+
const lowered = file.replace(/\\/g, "/").toLowerCase();
|
|
807
|
+
if (["package.json", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"].includes(lowered)) {
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
return matchesAny(lowered, TESTING_DISALLOWED_ALLOWED_PATHS);
|
|
811
|
+
}
|
|
812
|
+
function isTestingRuntimeBoundaryChange(file) {
|
|
813
|
+
const normalized = file.replace(/\\/g, "/");
|
|
814
|
+
const lowered = normalized.toLowerCase();
|
|
815
|
+
if (isTestingBoundaryAllowedPath(lowered) || matchesAny(lowered, TESTING_DISALLOWED_CHANGED_PATHS)) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
if (lowered.startsWith("tests/")) {
|
|
819
|
+
const name = path.basename(lowered);
|
|
820
|
+
if (TESTING_ALLOWED_TEST_FILE_TERMS.some((term) => name.includes(term))) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
return TESTING_RUNTIME_FILE_TERMS.some((term) => name.includes(term));
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
582
827
|
function isDevelopmentDraft(task) {
|
|
583
828
|
const taskId = String(task.id ?? "");
|
|
584
829
|
return Boolean(task.implementation_doc) || task.phase === "SPRINTING" || taskId.startsWith("DEV-");
|
|
@@ -609,7 +854,7 @@ function taskText(task) {
|
|
|
609
854
|
}
|
|
610
855
|
export async function changedFiles(projectRoot) {
|
|
611
856
|
try {
|
|
612
|
-
const { stdout } = await execFileAsync("git", ["status", "--porcelain"], { cwd: projectRoot });
|
|
857
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: projectRoot });
|
|
613
858
|
return stdout
|
|
614
859
|
.split("\n")
|
|
615
860
|
.map((line) => line.slice(3).trim())
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# Test Plan(测试计划)
|
|
2
|
-
|
|
3
|
-
## 1. Scope(范围)
|
|
4
|
-
|
|
5
|
-
- PRD:
|
|
6
|
-
- Technical design:
|
|
7
|
-
- Implementation docs:
|
|
8
|
-
- Review report:
|
|
9
|
-
|
|
10
|
-
## 2. Test Matrix(测试矩阵)
|
|
11
|
-
|
|
12
|
-
| 需求(Requirement) | 场景(Scenario) | 测试类型(Test Type) | 测试用例(Test Case) | 结果(Result) |
|
|
13
|
-
|---|---|---|---|---|
|
|
14
|
-
| | | unit/integration/e2e/regression | | pending |
|
|
15
|
-
|
|
16
|
-
## 3. Regression Checklist(回归检查清单)
|
|
17
|
-
|
|
18
|
-
- [ ]
|
|
19
|
-
|
|
20
|
-
## 4. Coverage Gaps(覆盖缺口)
|
|
21
|
-
|
|
22
|
-
| 缺口(Gap) | 风险(Risk) | 后续动作(Follow-up) |
|
|
23
|
-
|---|---|---|
|
|
24
|
-
| | | |
|
|
25
|
-
|
|
26
|
-
## 5. Final Result(最终结论)
|
|
27
|
-
|
|
28
|
-
- Decision: `PASS` / `BLOCKED`
|
|
29
|
-
- Evidence:
|