@xulthekl/team-flow 0.56.1 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/marketplace.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +37 -17
- package/CHANGELOG.md +123 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +8 -7
- package/agents/code-reviewer.md +3 -0
- package/agents/cross-change-consistency-checker.md +34 -6
- package/agents/release-archivist.md +6 -0
- package/docs/README_en.md +1 -1
- package/docs/release-checklist.md +1 -1
- package/docs/solutions/INDEX.md +3 -3
- package/docs/usage-guide.md +2 -2
- package/gemini-extension.json +2 -2
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/plugin.json +2 -2
- package/scripts/lib/cmd-doctor.mjs +140 -1
- package/scripts/lib/cmd-solutions.mjs +14 -4
- package/scripts/lib/md-normalize.mjs +39 -0
- package/scripts/lib/solutions-backfill.mjs +116 -0
- package/scripts/lib/solutions-capture.mjs +67 -20
- package/scripts/lib/solutions-entry.mjs +185 -0
- package/scripts/lib/solutions-index-gen.mjs +117 -57
- package/scripts/lib/solutions-inject.mjs +102 -24
- package/scripts/lib/solutions-promote.mjs +147 -95
- package/scripts/lib/test-merge.mjs +73 -12
- package/scripts/team-flow.mjs +7 -3
- package/skills/architecture-design/SKILL.md +2 -2
- package/skills/architecture-design/references/s3.5-product-architecture.md +1 -1
- package/skills/architecture-design/templates/conventions/frontend-patterns.md +7 -0
- package/skills/build-executor/SKILL.md +7 -1
- package/skills/build-executor/implementer-prompt.md +19 -0
- package/skills/build-executor/task-reviewer-prompt.md +51 -5
- package/skills/ce-brainstorm/references/grounding.md +2 -2
- package/skills/ce-compound/references/promotion-rules.md +26 -9
- package/skills/ce-compound/references/schema.yaml +4 -2
- package/skills/ce-compound/references/three-tier-index.md +10 -7
- package/skills/ce-compound/references/write-flow.md +22 -10
- package/skills/ce-ideate/references/agents/learnings-researcher.md +9 -2
- package/skills/ce-ideate/references/grounding.md +1 -1
- package/skills/ce-plan/references/agents/learnings-researcher.md +9 -2
- package/skills/ce-plan/references/research-workflow.md +2 -2
- package/skills/clean-code/SKILL.md +116 -0
- package/skills/clean-code/references/judgement-cases.md +83 -0
- package/skills/clean-code/references/shared-layer-rules.md +50 -0
- package/skills/code-reviewer/SKILL.md +17 -0
- package/skills/code-reviewer/code-reviewer-prompt.md +74 -2
- package/skills/contract-builder/SKILL.md +9 -0
- package/skills/release-archivist/SKILL.md +2 -2
- package/skills/release-archivist/references/closing-procedures.md +3 -1
- package/skills/spec-writer/SKILL.md +1 -1
- package/skills/workflow-orchestrator/SKILL.md +2 -2
- package/skills/workflow-orchestrator/references/s1-path-router.md +4 -2
- package/skills/workflow-orchestrator/references/s3-plan-pipeline.md +1 -1
- package/skills/workflow-orchestrator/references/s5-monitoring.md +8 -4
- package/skills/workflow-start/SKILL.md +4 -0
- package/templates/conventions/glaf4-compliant/java-testing.md +3 -3
- package/templates/conventions/js-testing.md +1 -1
- package/templates/conventions/python-testing.md +1 -1
- package/templates/learnings.md +17 -5
package/docs/solutions/INDEX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# Solutions Index
|
|
2
|
-
<!--
|
|
3
|
-
| date | phase | domain | type | severity | summary | file |
|
|
4
|
-
|
|
2
|
+
<!-- 每条一行,排序 severity 降序 → date 降序 → file 兜底,≤150 条上限;summary/source 内的 | 转义为 \| -->
|
|
3
|
+
| date | phase | domain | type | severity | summary | file | source |
|
|
4
|
+
|------|-------|--------|------|----------|---------|------|--------|
|
package/docs/usage-guide.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# team-flow 使用说明(研发团队版)
|
|
2
2
|
|
|
3
|
-
> 版本锚点:v0.
|
|
3
|
+
> 版本锚点:v0.58.0(27 skills + 17 agents)· 更新日期:2026-09-12
|
|
4
4
|
> 读者:使用 team-flow 做日常研发的工程师。不需要你懂插件内部实现,只需要照着路径走。
|
|
5
5
|
> 配套文档:安装细节见 [INSTALL.md](../INSTALL.md);状态机细节见 [state-machine.md](state-machine.md);决策点细节见 [decision-points.md](decision-points.md);平台差异见 [platform-matrix.md](platform-matrix.md)。
|
|
6
6
|
|
|
@@ -239,7 +239,7 @@ tf arch-merge(架构增量 → docs/architecture/ 全局当前态)
|
|
|
239
239
|
→ tf deisolate --merge(worktree 代码合并回主分支,Step 5c Code Landing)
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
这就是"改动不烂尾、知识不流失"的机制保障。经验沉淀后,下一个 change 的对应阶段会通过 `tf solutions inject`
|
|
242
|
+
这就是"改动不烂尾、知识不流失"的机制保障。经验沉淀后,下一个 change 的对应阶段会通过 `tf solutions inject` 自动注入相关经验(默认 top-5;调用点须显式传 `--limit`,否则 cross-phase 条目满载时本阶段新增条目永不可达)。
|
|
243
243
|
|
|
244
244
|
---
|
|
245
245
|
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"description": "Unified workflow plugin: team-flow (spec-driven dev) + compound-engineering core subset + architecture-design (4A/DDD) + prototype (local HTML) + business-analysis (independent requirement/scenario artifact).
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Unified workflow plugin: team-flow (spec-driven dev) + compound-engineering core subset + architecture-design (4A/DDD) + prototype (local HTML) + business-analysis (independent requirement/scenario artifact). 27 skills, one install.",
|
|
4
|
+
"version": "0.58.0",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/session-start
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# v0.
|
|
2
|
+
# v0.58.0: auto-sync CLI version with plugin version
|
|
3
3
|
set -e
|
|
4
4
|
|
|
5
5
|
# ═══════════════════════════════════════════════════════════════
|
|
6
6
|
# Plugin version (update this when releasing new versions)
|
|
7
7
|
# ═══════════════════════════════════════════════════════════════
|
|
8
|
-
PLUGIN_VERSION="0.
|
|
8
|
+
PLUGIN_VERSION="0.58.0"
|
|
9
9
|
|
|
10
10
|
# ═══════════════════════════════════════════════════════════════
|
|
11
11
|
# Step 1: Auto-sync CLI version with plugin version
|
package/llms.txt
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Overview
|
|
4
4
|
spec-superflow is a self-contained workflow integration plugin for Claude Code, Cursor, OpenAI Codex CLI/App, GitHub Copilot CLI, Gemini CLI, OpenCode, WorkBuddy, and Trae. It merges spec-driven planning artifacts (proposal, specs, design, tasks) with disciplined execution guardrails (TDD, review gates, controlled handoff) into one unified workflow.
|
|
5
5
|
|
|
6
|
-
Current version: v0.
|
|
6
|
+
Current version: v0.58.0.
|
|
7
7
|
|
|
8
8
|
## Key Documents
|
|
9
9
|
- README.md: Chinese homepage with full usage guide and FAQ
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xulthekl/team-flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified plugin (
|
|
3
|
+
"version": "0.58.0",
|
|
4
|
+
"description": "Unified plugin (27 skills + 17 agents) integrating team-flow, compound-engineering, architecture-design, prototype, design-system, workflow-orchestrator, workflow-bootstrap, e2e, session-handoff, workflow-feedback, business-analysis for multi-agent coding tools.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact) + design-system (design tokens + component contract + AI primer + showcase) + test-strategy (test strategy design) + project-initialize (new service onboarding).
|
|
3
|
+
"version": "0.58.0",
|
|
4
|
+
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact) + design-system (design tokens + component contract + AI primer + showcase) + test-strategy (test strategy design) + project-initialize (new service onboarding). 27 skills + 17 agents, one install.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LT"
|
|
7
7
|
},
|
|
@@ -7,6 +7,13 @@ import { PLATFORM_RUNTIME_INVENTORY } from './platform-runtime-inventory.mjs';
|
|
|
7
7
|
import { readState, VALID_STATES } from './state-loader.mjs';
|
|
8
8
|
// v0.22 §85:tasks_skipped 豁免判定复用 guard 侧唯一真相源,避免同一规则两处实现漂移。
|
|
9
9
|
import { hasExplicitTasksSkip } from '../guard/checks/tasks-gate-exemptions.mjs';
|
|
10
|
+
// v0.57.0 §4.4:复利库健康检查(漂移/截断/白名单外布局/未晋升)复用既有共享层
|
|
11
|
+
import { parseTableRow } from './md-normalize.mjs';
|
|
12
|
+
import { parseFrontmatter, MAX_INDEX_ENTRIES } from './solutions-index-gen.mjs';
|
|
13
|
+
// 条目枚举与 index-gen **同源**——含判据(不一致会报出"重跑 index-gen 也消不掉"的告警)
|
|
14
|
+
// 与防御(非普通文件不抛异常;本维度无逐维度隔离,抛出会中止整个 doctor)
|
|
15
|
+
import { listSolutionEntries } from './solutions-entry.mjs';
|
|
16
|
+
import { SOLUTION_PHASES } from './solutions-phases.mjs';
|
|
10
17
|
|
|
11
18
|
const RUNTIME_SKILLS = new Set([
|
|
12
19
|
'workflow-start', 'need-explorer', 'spec-writer', 'contract-builder',
|
|
@@ -332,6 +339,137 @@ function checkArchState(root) {
|
|
|
332
339
|
return { pass: warnings.length === 0, message: warnings.length ? warnings.join('; ') : 'arch-state/iterations 一致' };
|
|
333
340
|
}
|
|
334
341
|
|
|
342
|
+
/**
|
|
343
|
+
* v0.57.0 §4.4:复利库健康检查(设计文档 compound-lifecycle-governance-design v2.2 §5 P1#6)。
|
|
344
|
+
*
|
|
345
|
+
* 四个维度合并为一条 doctor 结论(doctor 每维度输出一行):
|
|
346
|
+
* A. **INDEX ↔ 文件漂移** —— 注入只读 INDEX(`solutions-inject.mjs`),任何失配都会静默影响 top-5。
|
|
347
|
+
* 本项是 §4.2「文件是唯一真相源、INDEX 只做投影」的**防复发**手段。
|
|
348
|
+
* B. **条目数 / 截断** —— `refreshIndex` 收进 capture/promote 后,截断从"从未触发"变为
|
|
349
|
+
* "过 150 即每次静默淘汰",须有可见的临近预警。
|
|
350
|
+
* C. **白名单外目录与平铺文件** —— 不被 `SOLUTION_PHASES` 收录的目录(如 ce-compound 的
|
|
351
|
+
* category 目录、`workflow-patterns/`)与直接放在 `docs/solutions/` 下的条目文件,
|
|
352
|
+
* 对 CLI 通道**永久不可见**(不入 INDEX、不可注入)。
|
|
353
|
+
* D. **未 promote 的 change** —— 有 `learnings.md` 但 solutions 中无任何条目引用它的 change
|
|
354
|
+
* (emp-auth 实测:C1 有 2 条 critical 因旧 bug 从未晋升)。
|
|
355
|
+
*
|
|
356
|
+
* advisory:与 doctor 其余维度一致,不阻断。
|
|
357
|
+
*/
|
|
358
|
+
function checkSolutions(root) {
|
|
359
|
+
const solutionsDir = join(root, 'docs', 'solutions');
|
|
360
|
+
if (!existsSync(solutionsDir)) {
|
|
361
|
+
return { pass: true, message: '未使用复利库(docs/solutions/ 不存在)' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const problems = [];
|
|
365
|
+
|
|
366
|
+
// 收集条目文件(限白名单 phase 目录)
|
|
367
|
+
const entryFiles = [];
|
|
368
|
+
for (const phase of SOLUTION_PHASES) {
|
|
369
|
+
// v0.57.0 P4 五/六轮:条目枚举走**共享层**——它同时保证 ①与 `index-gen` 同判据
|
|
370
|
+
//(判据不一致会报出"重跑 index-gen 也消不掉"的告警)②**非普通文件不抛异常**。
|
|
371
|
+
// 后者对本维度尤为关键:`checkSolutions` 内联在 `checks` 数组字面量中、**无逐维度隔离**,
|
|
372
|
+
// 一次抛出会中止**整个 doctor**(12 个维度全丢),而它本来只是 advisory 巡检。
|
|
373
|
+
for (const e of listSolutionEntries(join(solutionsDir, phase))) {
|
|
374
|
+
entryFiles.push({ phase, file: e.file, rel: e.rel, content: e.content });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// A. INDEX ↔ 文件漂移
|
|
379
|
+
const indexPath = join(solutionsDir, 'INDEX.md');
|
|
380
|
+
if (existsSync(indexPath)) {
|
|
381
|
+
const indexRows = new Map();
|
|
382
|
+
for (const line of readFileSync(indexPath, 'utf-8').split('\n')) {
|
|
383
|
+
if (!line.startsWith('|') || line.startsWith('| date') || line.startsWith('|--')) continue;
|
|
384
|
+
const cells = parseTableRow(line);
|
|
385
|
+
// v0.57.0 P4:`source` 列新增后列数为 7(存量 INDEX)或 8(本版重建);
|
|
386
|
+
// 另校验 `file` 列形状——列数在界内仍可能因未转义的 `|` 而错位。
|
|
387
|
+
if (cells.length < 7 || cells.length > 8) continue;
|
|
388
|
+
if (!/^[a-z][a-z-]*\/.+\.md$/.test(cells[6])) continue;
|
|
389
|
+
indexRows.set(cells[6], { date: cells[0], phase: cells[1], domain: cells[2], type: cells[3], severity: cells[4] });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const drifts = [];
|
|
393
|
+
const FIELDS = ['date', 'phase', 'domain', 'type', 'severity'];
|
|
394
|
+
for (const [rel, row] of indexRows) {
|
|
395
|
+
const fp = join(solutionsDir, rel);
|
|
396
|
+
if (!existsSync(fp)) { drifts.push(`${rel}(INDEX 有行、文件缺失)`); continue; }
|
|
397
|
+
// 防御同 `listSolutionEntries`(v0.57.0 P4 收尾):INDEX 行指向的路径可能是目录 /
|
|
398
|
+
// 断链 symlink / 无读权限 / **FIFO**。前三类让本维度抛异常,而 `checkSolutions`
|
|
399
|
+
// 内联在 `checks` 字面量中、**无逐维度隔离** ⇒ 会**中止整个 doctor**(12 维度全丢);
|
|
400
|
+
// FIFO 更会让 `readFileSync` **阻塞挂死**(不是异常,try/catch 也挡不住)——故先 `statSync` 判类型。
|
|
401
|
+
// INDEX 由 index-gen 生成、只对可枚举的普通文件出行,故现实实例为 0;但兜底成本是几行。
|
|
402
|
+
let raw;
|
|
403
|
+
try {
|
|
404
|
+
if (!statSync(fp).isFile()) { drifts.push(`${rel}(INDEX 有行、但该路径不是普通文件)`); continue; }
|
|
405
|
+
raw = readFileSync(fp, 'utf-8');
|
|
406
|
+
} catch {
|
|
407
|
+
drifts.push(`${rel}(INDEX 有行、但无法读取)`); continue;
|
|
408
|
+
}
|
|
409
|
+
const fm = parseFrontmatter(raw);
|
|
410
|
+
if (!fm) { drifts.push(`${rel}(无 frontmatter)`); continue; }
|
|
411
|
+
for (const k of FIELDS) {
|
|
412
|
+
if (fm[k] && fm[k] !== row[k]) { drifts.push(`${rel} ${k}: INDEX=${row[k]} 文件=${fm[k]}`); break; }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const e of entryFiles) {
|
|
416
|
+
if (!indexRows.has(e.rel)) drifts.push(`${e.rel}(文件存在、INDEX 无行)`);
|
|
417
|
+
}
|
|
418
|
+
if (drifts.length > 0) {
|
|
419
|
+
problems.push(`${drifts.length} 处 INDEX/文件漂移(${drifts.slice(0, 3).join(';')}${drifts.length > 3 ? ' …' : ''})→ 运行 tf solutions index-gen 重建`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// B. 条目数 / 截断
|
|
424
|
+
const total = entryFiles.length;
|
|
425
|
+
if (total > MAX_INDEX_ENTRIES) {
|
|
426
|
+
problems.push(`条目数 ${total} 已超索引上限 ${MAX_INDEX_ENTRIES},超出部分不被索引(详见 index-gen 的 dropped 提示)`);
|
|
427
|
+
} else if (total > Math.floor(MAX_INDEX_ENTRIES * 0.8)) {
|
|
428
|
+
problems.push(`条目数 ${total} 接近索引上限 ${MAX_INDEX_ENTRIES}(>80%)`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// C. 白名单外目录与平铺文件
|
|
432
|
+
const strayDirs = readdirSync(solutionsDir, { withFileTypes: true })
|
|
433
|
+
.filter(e => e.isDirectory() && !SOLUTION_PHASES.includes(e.name))
|
|
434
|
+
.map(e => `${e.name}/`);
|
|
435
|
+
const strayFiles = readdirSync(solutionsDir, { withFileTypes: true })
|
|
436
|
+
.filter(e => e.isFile() && e.name.endsWith('.md') && e.name !== 'INDEX.md')
|
|
437
|
+
.map(e => e.name);
|
|
438
|
+
if (strayDirs.length > 0) {
|
|
439
|
+
problems.push(`非白名单目录 ${strayDirs.join(', ')}(不被索引:CLI 通道不可见)`);
|
|
440
|
+
}
|
|
441
|
+
if (strayFiles.length > 0) {
|
|
442
|
+
problems.push(`平铺条目文件 ${strayFiles.join(', ')}(须移入 phase 子目录才可被索引)`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// D. 未 promote 的 change
|
|
446
|
+
const changesDir = join(root, 'changes');
|
|
447
|
+
if (existsSync(changesDir)) {
|
|
448
|
+
const referenced = new Set();
|
|
449
|
+
for (const e of entryFiles) {
|
|
450
|
+
const fm = parseFrontmatter(readFileSync(join(solutionsDir, e.rel), 'utf-8'));
|
|
451
|
+
if (!fm) continue;
|
|
452
|
+
if (fm.source) referenced.add(fm.source.trim());
|
|
453
|
+
const confirmations = fm.confirmations || '';
|
|
454
|
+
for (const c of confirmations.replace(/^\[|\]$/g, '').split(',')) {
|
|
455
|
+
if (c.trim()) referenced.add(c.trim());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const unpromoted = readdirSync(changesDir, { withFileTypes: true })
|
|
459
|
+
.filter(e => e.isDirectory() && existsSync(join(changesDir, e.name, 'learnings.md')))
|
|
460
|
+
.map(e => e.name)
|
|
461
|
+
.filter(name => !referenced.has(name));
|
|
462
|
+
if (unpromoted.length > 0) {
|
|
463
|
+
problems.push(`${unpromoted.length} 个 change 有 learnings.md 但未被晋升(${unpromoted.slice(0, 3).join(', ')}${unpromoted.length > 3 ? ' …' : ''})`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
pass: problems.length === 0,
|
|
469
|
+
message: problems.length ? problems.join(' | ') : `${total} 条经验,索引与文件一致`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
335
473
|
export async function run(args) {
|
|
336
474
|
const root = process.cwd();
|
|
337
475
|
const config = loadConfig(root);
|
|
@@ -351,6 +489,7 @@ export async function run(args) {
|
|
|
351
489
|
['Change states', checkChangeStates(root)],
|
|
352
490
|
['Change test gates', checkChangeTestGates(root)],
|
|
353
491
|
['Arch state', checkArchState(root)],
|
|
492
|
+
['Solutions', checkSolutions(root)],
|
|
354
493
|
];
|
|
355
494
|
|
|
356
495
|
// Config check
|
|
@@ -379,4 +518,4 @@ export async function run(args) {
|
|
|
379
518
|
}
|
|
380
519
|
}
|
|
381
520
|
|
|
382
|
-
export { checkVersionConsistency, checkHooks, checkCodexManifest, checkSkills, checkRuntimeDistribution, checkDist, checkRootPluginAuthor, checkNodeVersion, checkDocs, checkChangeStates, checkChangeTestGates };
|
|
521
|
+
export { checkVersionConsistency, checkHooks, checkCodexManifest, checkSkills, checkRuntimeDistribution, checkDist, checkRootPluginAuthor, checkNodeVersion, checkDocs, checkChangeStates, checkChangeTestGates, checkSolutions };
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 设计说明(v0.5 / 2026-07-21):
|
|
4
4
|
// - 与 checkpoint / handoff / execution 一致,采用「单 COMMANDS key + 内部子命令分发」模式。
|
|
5
|
-
// -
|
|
5
|
+
// - 底层脚本 solutions-{index-gen,inject,capture,promote,backfill}.mjs 的 run(args) 接收「对象」参数,
|
|
6
6
|
// 而 CLI 入口传入的是「字符串数组」。本分发器用 parseArgs 把数组解析成各脚本需要的对象后再调用,
|
|
7
7
|
// 完成 数组 → 对象 的桥接。
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
|
|
10
|
-
const USAGE = 'Usage: tf solutions <index-gen|inject|capture|promote> [options]';
|
|
10
|
+
const USAGE = 'Usage: tf solutions <index-gen|inject|capture|promote|backfill> [options]';
|
|
11
11
|
|
|
12
12
|
export async function run(args) {
|
|
13
13
|
const { positionals, values } = parseArgs({
|
|
@@ -21,6 +21,10 @@ export async function run(args) {
|
|
|
21
21
|
severity: { type: 'string' },
|
|
22
22
|
summary: { type: 'string' },
|
|
23
23
|
source: { type: 'string' },
|
|
24
|
+
// v0.57.0 §4.1:inject 的展示条数控制与 backfill 的预演
|
|
25
|
+
limit: { type: 'string' },
|
|
26
|
+
all: { type: 'boolean' },
|
|
27
|
+
'dry-run': { type: 'boolean' },
|
|
24
28
|
},
|
|
25
29
|
});
|
|
26
30
|
|
|
@@ -35,7 +39,7 @@ export async function run(args) {
|
|
|
35
39
|
|
|
36
40
|
if (subcommand === 'inject') {
|
|
37
41
|
const mod = await import('./solutions-inject.mjs');
|
|
38
|
-
mod.run({ phase: values.phase, domain: values.domain, dir: values.dir });
|
|
42
|
+
mod.run({ phase: values.phase, domain: values.domain, dir: values.dir, limit: values.limit, all: values.all });
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -61,7 +65,13 @@ export async function run(args) {
|
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
if (subcommand === 'backfill') {
|
|
69
|
+
const mod = await import('./solutions-backfill.mjs');
|
|
70
|
+
mod.run({ dir: values.dir, 'dry-run': values['dry-run'] });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.error(`Unknown solutions subcommand: ${subcommand}. Valid: index-gen, inject, capture, promote, backfill`);
|
|
65
75
|
process.exit(2);
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -106,3 +106,42 @@ export function parseTaskLine(line) {
|
|
|
106
106
|
if (!m) return null;
|
|
107
107
|
return { indent: m[1].length, checked: m[3].toLowerCase() === 'x', text: m[4] };
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 解析 Markdown 表格行 → 单元格字符串数组(**保留空列**)。
|
|
112
|
+
*
|
|
113
|
+
* v0.57.0 §4.4 抽为共享层。背景:`solutions-inject.mjs` 此前用
|
|
114
|
+
* `split('|').map(c => c.trim()).filter(Boolean)` —— `filter(Boolean)` **会丢空列**,
|
|
115
|
+
* 而守卫只有下界(`cols.length < 7`)⇒ 某单元格内容含 `|` 时列数变多、**不被拦截**,
|
|
116
|
+
* `file` 列取到错误值(与 `test-merge.mjs` 在 v0.38.0 修过的同型缺陷一致,当时未横展)。
|
|
117
|
+
*
|
|
118
|
+
* 约定:
|
|
119
|
+
* - `split('|')` 对 `| a | b |` 产出 `['', ' a ', ' b ', '']`,故取 `slice(1, -1)` 去首尾空串
|
|
120
|
+
* - 支持 `\|` 转义(v0.57.0 P4):生成侧 `escapeCell` 把内容里的 `|` 写成 `\|`,
|
|
121
|
+
* 此处先换占位符再切分,否则转义符形同虚设(`\|` 仍会被 `split` 切开)。
|
|
122
|
+
* 无 `\|` 时与裸 `split('|').slice(1,-1)` **逐格一致**(含行尾缺竖线的非良构行——
|
|
123
|
+
* 两者对 `| a | b | c` 都返回 2 格)。与**本模块替换掉的旧版 `filter(Boolean)` 实现**
|
|
124
|
+
* 的差异才是实质的:旧版无 `slice`、且 `filter(Boolean)` 丢空列 ⇒ 同样的
|
|
125
|
+
* `| a | b | c` 返回 3 格、而 `| a | | c |` 返回 2 格(空列消失导致后续列全部左移)
|
|
126
|
+
* - **不做**列数校验——期望列数由调用方决定(INDEX 为 7/8 列,test-matrix 为 12 列)
|
|
127
|
+
* - 调用方 MUST 同时校验**上界与下界**(只查下界无法发现"多出的 `|` 导致的错位")
|
|
128
|
+
*
|
|
129
|
+
* ⚠ 同名函数差异矩阵(v0.57.0 P4 评审补充,避免按名字误挑):
|
|
130
|
+
* | 位置 | 非法输入 | 清洗 |
|
|
131
|
+
* |---|---|---|
|
|
132
|
+
* | 本函数(md-normalize) | **永不返回 null**,非表格行也产出数组 | 仅 trim + 还原 `\|` |
|
|
133
|
+
* | `arch-parse.mjs` | 返回 `null` | strip 反引号与强调标记 |
|
|
134
|
+
* | `ds-parse.mjs` | 返回 `null` | 走 `normalizeInline` |
|
|
135
|
+
* 本函数是最"裸"的一层 —— 调用方须自行判断行是否合法(如 `startsWith('|')`)。
|
|
136
|
+
*
|
|
137
|
+
* @param {string} line
|
|
138
|
+
* @returns {string[]}
|
|
139
|
+
*/
|
|
140
|
+
export function parseTableRow(line) {
|
|
141
|
+
const PIPE = '\u0001'; // 控制字符占位:正常 markdown 内容不含
|
|
142
|
+
return String(line)
|
|
143
|
+
.replace(/\\\|/g, PIPE)
|
|
144
|
+
.split('|')
|
|
145
|
+
.slice(1, -1)
|
|
146
|
+
.map(cell => cell.trim().replaceAll(PIPE, '|'));
|
|
147
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* solutions-backfill — 为存量复利条目补写 `title:` 字段
|
|
4
|
+
*
|
|
5
|
+
* v0.57.0 §4.1(设计文档 compound-lifecycle-governance-design v2.2)
|
|
6
|
+
* 用法:tf solutions backfill [--dir <solutions-dir>] [--dry-run]
|
|
7
|
+
*
|
|
8
|
+
* 为什么需要:
|
|
9
|
+
* `ce-plan`/`ce-ideate` 的 `learnings-researcher` 子代理按
|
|
10
|
+
* `title:` / `tags:` / `module:` / `problem_type:` 四个字段 grep 检索
|
|
11
|
+
* (`skills/ce-plan/references/agents/learnings-researcher.md:77-80`),
|
|
12
|
+
* 而 CLI 通道(promote/capture)此前写入的 frontmatter 只有
|
|
13
|
+
* `phase/domain/type/severity/date/source` —— **四个模式一个都命中不了**。
|
|
14
|
+
* 实测:emp-auth 21 个条目文件中,这四个模式**各只命中 1 个**(那条 ce-compound 通道写的条目)。
|
|
15
|
+
*
|
|
16
|
+
* v0.57.0 起 promote/capture 会写 `title:`,但**只改写入模板等于没改**——
|
|
17
|
+
* 存量条目仍检索不到(这正是 P-1 自己批评过的"修 bug 未回填",v0.48.0→v0.51.0 同型)。
|
|
18
|
+
* 本命令补齐存量。
|
|
19
|
+
*
|
|
20
|
+
* title 推导口径(与 `solutions-index-gen` 的 `extractSummary` 同源):
|
|
21
|
+
* ① 正文中的 `## <标题>` 行(promote 写入的形态)
|
|
22
|
+
* ② 回退:首个非空且非 `#` 开头的行
|
|
23
|
+
* 两者均经 `cleanTitle` 去 learnings 的 `N. ` 序号前缀(该序号只在其源文件内有意义)。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { pathToFileURL } from 'node:url';
|
|
29
|
+
import { SOLUTION_PHASES } from './solutions-phases.mjs';
|
|
30
|
+
import { refreshIndex } from './solutions-index-gen.mjs';
|
|
31
|
+
import { listSolutionEntries } from './solutions-entry.mjs';
|
|
32
|
+
|
|
33
|
+
/** 与 solutions-promote 的 cleanTitle 同口径:去 `N. ` / `N、` 序号前缀 */
|
|
34
|
+
function cleanTitle(raw) {
|
|
35
|
+
return String(raw ?? '').replace(/^\d+[.、]\s*/, '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 从条目正文推导 title */
|
|
39
|
+
export function deriveTitle(content) {
|
|
40
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
|
41
|
+
|
|
42
|
+
const heading = body.match(/^##\s+(.+)$/m);
|
|
43
|
+
if (heading) return cleanTitle(heading[1]);
|
|
44
|
+
|
|
45
|
+
const line = body.split('\n').map(l => l.trim()).find(l => l && !l.startsWith('#'));
|
|
46
|
+
return line ? cleanTitle(line).slice(0, 80) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function run(args = {}) {
|
|
50
|
+
const dir = args.dir || 'docs/solutions';
|
|
51
|
+
const dryRun = args['dry-run'] === true || args['dry-run'] === 'true';
|
|
52
|
+
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
console.error(`Solutions directory not found: ${dir}`);
|
|
55
|
+
return { filled: 0, already: 0, skipped: 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let filled = 0;
|
|
59
|
+
let already = 0;
|
|
60
|
+
const skipped = [];
|
|
61
|
+
|
|
62
|
+
for (const phase of SOLUTION_PHASES) {
|
|
63
|
+
// 条目枚举走**共享层**(与 index-gen / doctor 同源):含条目判据与"非普通文件不抛异常"防御。
|
|
64
|
+
// 副作用(有意):阶段目录下的**非条目文档**(README / 格式说明)不再出现在 skipped 里——
|
|
65
|
+
// 它们本就不是"待补 title 的条目",列出来只是噪音。
|
|
66
|
+
//
|
|
67
|
+
// ⚠️ 但下方**刻意**不复用共享层的 `parseFrontmatter`:本脚本只做**键存在性检测**
|
|
68
|
+
// 与**整块替换**(需要带 `---` 的完整匹配),不读取任何 frontmatter 值,
|
|
69
|
+
// 故不涉及"行尾注释剥离"那一类静默错判(P4 二轮 N1)。若日后需要读值,请改走共享层。
|
|
70
|
+
for (const { file, filePath, content } of listSolutionEntries(join(dir, phase))) {
|
|
71
|
+
const block = content.match(/^---\n([\s\S]*?)\n---/);
|
|
72
|
+
if (!block) {
|
|
73
|
+
skipped.push(`${phase}/${file}(无 frontmatter)`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// 容忍全角冒号与加粗(沿用 FB-4 的容忍面)
|
|
77
|
+
if (/^\**title\**\s*[::]/m.test(block[1])) {
|
|
78
|
+
already += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const title = deriveTitle(content);
|
|
83
|
+
if (!title) {
|
|
84
|
+
skipped.push(`${phase}/${file}(无法推导标题)`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!dryRun) {
|
|
89
|
+
writeFileSync(filePath, content.replace(block[0], `---\n${block[1]}\ntitle: ${title}\n---`), 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
filled += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// INDEX 由共享入口派生(title 会成为新的摘要来源)
|
|
96
|
+
if (!dryRun && filled > 0) refreshIndex(dir, { quiet: true });
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`Backfill${dryRun ? '(dry-run)' : ''}: ${filled} filled, ${already} already had title, ${skipped.length} skipped`,
|
|
100
|
+
);
|
|
101
|
+
if (skipped.length > 0) {
|
|
102
|
+
console.log('Skipped:');
|
|
103
|
+
for (const item of skipped) console.log(` - ${item}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { filled, already, skipped: skipped.length };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
110
|
+
const args = {};
|
|
111
|
+
for (let i = 2; i < process.argv.length; i += 1) {
|
|
112
|
+
if (process.argv[i] === '--dry-run') { args['dry-run'] = true; continue; }
|
|
113
|
+
if (process.argv[i] === '--dir' && process.argv[i + 1]) { args.dir = process.argv[++i]; }
|
|
114
|
+
}
|
|
115
|
+
run(args);
|
|
116
|
+
}
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* solutions-capture — 写入一条经验到
|
|
3
|
+
* solutions-capture — 写入一条经验到 docs/solutions/<phase>/ + 刷新 INDEX
|
|
4
4
|
*
|
|
5
5
|
* v0.5 复利贯穿机制脚本
|
|
6
6
|
* 用法:tf solutions capture --phase <p> --domain <d> --type <t> --severity <s> --summary "<text>"
|
|
7
7
|
* --severity 合法取值:critical | high | medium | low(v0.23 §91.3.4 起校验,非法值报错退出)
|
|
8
8
|
*
|
|
9
9
|
* 功能:
|
|
10
|
-
* 1.
|
|
11
|
-
* 2.
|
|
12
|
-
*
|
|
10
|
+
* 1. 在对应阶段目录下创建经验文件(含 `title:` 字段,v0.57.0 起)
|
|
11
|
+
* 2. 调用共享入口 `refreshIndex()` 重建 INDEX.md(本脚本**不再直接写 INDEX**)
|
|
12
|
+
*
|
|
13
|
+
* v0.57.0 §4.2(设计文档 compound-lifecycle-governance-design v2.2):
|
|
14
|
+
* - 原实现以 `appendFileSync` 追加 INDEX 行,且是"INDEX 首次创建"(含表头)的**唯一来源**。
|
|
15
|
+
* 将写入权收归 `refreshIndex` 后,由它统一负责首建与重建——否则 capture 路径写下的条目
|
|
16
|
+
* 在下次 promote 前不在 INDEX 中(注入不可见),且无人再负责首次建索引。
|
|
17
|
+
* - 新增 `title:` 字段:`learnings-researcher` 按 `title:`/`tags:`/`module:`/`problem_type:`
|
|
18
|
+
* 检索,而 CLI 条目此前只有 `phase/domain/type/...` ⇒ 实测 4 个模式在 21 条中**各只命中 1 条**。
|
|
19
|
+
* - 删除本地的容量自查(原按"150 行"打印提示,**口径有误**——上限实为 150 **条**,文件另含 4 行头部)
|
|
20
|
+
* ——容量由 `refreshIndex` 统一处理并 WARN。
|
|
13
21
|
*/
|
|
14
22
|
|
|
15
|
-
import {
|
|
23
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
16
24
|
import { join } from 'node:path';
|
|
17
25
|
import { pathToFileURL } from 'node:url';
|
|
18
|
-
import { SEVERITY_VALUES, isSeverity } from './severity.mjs';
|
|
26
|
+
import { SEVERITY_VALUES, isSeverity, severityRank } from './severity.mjs';
|
|
19
27
|
import { SOLUTION_PHASES } from './solutions-phases.mjs';
|
|
20
28
|
import { slugify } from './slug.mjs';
|
|
29
|
+
import { refreshIndex } from './solutions-index-gen.mjs';
|
|
30
|
+
// 条目身份与碰撞定位——与 promote 共用的**共享层**(v0.57.0 P4)
|
|
31
|
+
import { entryBodySignature, resolveEntryPath, readFrontmatterValue, MAX_COLLISION_SUFFIX } from './solutions-entry.mjs';
|
|
21
32
|
|
|
22
|
-
const MAX_INDEX_LINES = 150;
|
|
23
33
|
const SLUG_MAX_LENGTH = 40;
|
|
24
34
|
|
|
25
35
|
export function run(args = {}) {
|
|
@@ -60,6 +70,8 @@ export function run(args = {}) {
|
|
|
60
70
|
const filePath = join(phaseDir, fileName);
|
|
61
71
|
|
|
62
72
|
// 写经验文件
|
|
73
|
+
// `title` 须为单行(它是 INDEX 摘要与 `learnings-researcher` 的检索字段)
|
|
74
|
+
const title = String(summary).replace(/\s*\n\s*/g, ' ').trim();
|
|
63
75
|
const content = `---
|
|
64
76
|
phase: ${phase}
|
|
65
77
|
domain: ${domain}
|
|
@@ -67,6 +79,7 @@ type: ${type}
|
|
|
67
79
|
severity: ${severity}
|
|
68
80
|
date: ${date}
|
|
69
81
|
source: ${source}
|
|
82
|
+
title: ${title}
|
|
70
83
|
---
|
|
71
84
|
|
|
72
85
|
## 问题描述
|
|
@@ -78,24 +91,58 @@ ${summary}
|
|
|
78
91
|
## 预防措施/应用方式
|
|
79
92
|
(待补充)
|
|
80
93
|
`;
|
|
81
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
82
94
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
// v0.57.0 P4:碰撞保护 —— 原实现无条件 `writeFileSync`,同日同 slug 会**静默覆盖**
|
|
96
|
+
// (丢正文、丢已升的 severity、丢 confirmations),使"内容零丢失"在本通道为假。
|
|
97
|
+
// 身份判据与 promote 共用共享层(`solutions-entry.mjs`),避免两条通道各写一份实现。
|
|
98
|
+
const resolved = resolveEntryPath(phaseDir, date, slug, entryBodySignature(content));
|
|
99
|
+
if (!resolved) {
|
|
100
|
+
console.error(`✗ 同标题条目已达上限 ${MAX_COLLISION_SUFFIX} 条,未写入:${phase}/${slug}-*.md`);
|
|
101
|
+
process.exit(1);
|
|
87
102
|
}
|
|
88
|
-
|
|
103
|
+
if (!resolved.isNew) {
|
|
104
|
+
// 签名相同 = 同一条经验(重复 capture)⇒ 幂等,不重复写入、不覆盖。
|
|
105
|
+
// 但 severity **只升不降**:两次 capture 的差异可能**全在 frontmatter**(正文同、severity 不同),
|
|
106
|
+
// 而签名刻意剔除 frontmatter ⇒ 若在此直接返回,用户"提高等级"的意图会被静默吞掉
|
|
107
|
+
// (v0.57.0 P4 实测:`severity: critical` 重复 capture 后条目仍是 `high`)。
|
|
108
|
+
const existing = readFileSync(resolved.filePath, 'utf-8');
|
|
109
|
+
// 读 severity 必须走共享层(**剥行尾注释**)——裸读会把 `high # 注释` 当未知值,
|
|
110
|
+
// 使下面的"只升不降"判据把这次 `medium` 误判为升级,日志打 Upgraded 实为**降级**
|
|
111
|
+
const current = readFrontmatterValue(existing, 'severity') || '';
|
|
112
|
+
const rel = `${phase}/${resolved.fileName}`;
|
|
113
|
+
|
|
114
|
+
// ⚠ `severityRank` 是**越小越严重**(critical=0 … low=3,见 severity.mjs)。
|
|
115
|
+
// 注入/索引侧用它做升序排序(critical 排最前),故此处的"更严重"判据是 `<` 而非 `>`
|
|
116
|
+
// ——写反会让 critical 被判为降级、medium 被判为升级(v0.57.0 P4 实测踩过)。
|
|
117
|
+
if (!current) {
|
|
118
|
+
// 既有条目**没有 severity 行**(手工条目):无从比较档位。
|
|
119
|
+
// 不能把 null 兜成 `''` 后去比较 —— `severityRank('')` 落未知值档(4),
|
|
120
|
+
// 任何合法 severity 都会被判为"升级",于是**日志与返回值谎报升级而文件未变**
|
|
121
|
+
//(v0.57.0 P4 三轮实测:返回 `upgradedFrom: ""` 且文件无 severity 行)。
|
|
122
|
+
console.log(`Already captured: ${rel}(内容一致,未重复写入;既有条目无 severity 行,未比较档位)`);
|
|
123
|
+
return { file: rel, phase, domain, type, severity, alreadyExisted: true };
|
|
124
|
+
}
|
|
89
125
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
if (severityRank(severity) < severityRank(current)) {
|
|
127
|
+
writeFileSync(resolved.filePath, existing.replace(/^severity\s*:\s*.+$/m, `severity: ${severity}`), 'utf-8');
|
|
128
|
+
refreshIndex(dir, { quiet: true });
|
|
129
|
+
console.log(`Upgraded: ${rel} severity ${current} → ${severity}(同一条经验,只升不降)`);
|
|
130
|
+
return { file: rel, phase, domain, type, severity, alreadyExisted: true, upgradedFrom: current };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const downgradedNote = severityRank(severity) > severityRank(current)
|
|
134
|
+
? `;本次 ${severity} 低于既有 ${current},按"只升不降"忽略`
|
|
135
|
+
: '';
|
|
136
|
+
console.log(`Already captured: ${rel}(内容一致,未重复写入${downgradedNote})`);
|
|
137
|
+
return { file: rel, phase, domain, type, severity, alreadyExisted: true };
|
|
95
138
|
}
|
|
139
|
+
writeFileSync(resolved.filePath, content, 'utf-8');
|
|
140
|
+
|
|
141
|
+
// v0.57.0 §4.2:INDEX 由共享入口派生(capture 不再直接写 INDEX)
|
|
142
|
+
refreshIndex(dir, { quiet: true });
|
|
96
143
|
|
|
97
|
-
console.log(`Captured: ${phase}/${fileName} (${severity}/${type})`);
|
|
98
|
-
return { file: `${phase}/${fileName}`, phase, domain, type, severity };
|
|
144
|
+
console.log(`Captured: ${phase}/${resolved.fileName} (${severity}/${type})`);
|
|
145
|
+
return { file: `${phase}/${resolved.fileName}`, phase, domain, type, severity };
|
|
99
146
|
}
|
|
100
147
|
|
|
101
148
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|