svharness 0.14.5 → 0.14.7
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 +43 -0
- package/dist/commands/apply.js +9 -2
- package/dist/commands/convert.js +39 -4
- package/dist/commands/init.js +27 -4
- package/dist/config/merge-options.js +5 -0
- package/dist/config/normalize.js +5 -1
- package/dist/core/apply-project-entry.js +8 -1
- package/dist/core/harness-yaml-baseline.js +68 -0
- package/dist/core/markdown-sheet-split.js +109 -0
- package/dist/core/markdown-table-cleanup.js +151 -0
- package/dist/core/next-steps.js +15 -2
- package/dist/core/repomix-apply-hint.js +68 -0
- package/dist/core/repomix-pack.js +5 -0
- package/dist/index.js +9 -0
- package/package.json +1 -1
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +2 -0
- package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +1 -0
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +1 -1
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +10 -3
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +5 -0
- package/templates/_shared/meta/harness.yaml.ejs +1 -0
- package/templates/_shared/skeleton/requirements/yaml/schema.json +5 -1
- package/templates/svharness.config.example.yaml +3 -0
package/README.md
CHANGED
|
@@ -116,6 +116,7 @@ Harness 是一个 **项目本地的知识层**,由两大部分组成:
|
|
|
116
116
|
│ └── yaml/
|
|
117
117
|
└── baseline/
|
|
118
118
|
├── code/ # 参考代码快照
|
|
119
|
+
├── repomix/ # 可选:`--repomix` 时生成的单文件 XML 快照
|
|
119
120
|
└── wiki/ # 架构 wiki
|
|
120
121
|
│
|
|
121
122
|
├── agent-env/ # Agent 运行期环境
|
|
@@ -274,6 +275,7 @@ svharness build \
|
|
|
274
275
|
| `--extra-skills <path...>` | 可选 | 额外运行期资源输入(文件/目录/glob,可混放 skills/rules);build 阶段先拷贝到 `agent-env/_incoming/skills/` 并生成 `agent-env/_incoming/manifest.yaml`,S65 再分流写入 `skills/` 与 `rules/` | — |
|
|
275
276
|
| `--baseline-branch <name>` | 可选 | git 基线的分支名(仅 git 模式有效) | `main` |
|
|
276
277
|
| `--baseline-max-file-kb <kb>` | 可选 | 基线拷贝单文件大小上限(KB) | `1024` |
|
|
278
|
+
| `--repomix` | 可选 flag | 将 `baseline/code` 打成**单文件** Repomix XML;**须同时提供 `--baseline`**。适用场景见下文 [Repomix](#repomix-repomix) | `false` |
|
|
277
279
|
| `--convert-endpoint <url>` | 可选 | build 自动 convert 使用的 markitdown 服务基址;省略时读环境变量 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
|
|
278
280
|
| `--convert-concurrency <n>` | 可选 | build 自动 convert 并发上传数 | `3` |
|
|
279
281
|
| `--convert-max-file-mb <n>` | 可选 | build 自动 convert 单文件大小上限(MB) | `50` |
|
|
@@ -307,6 +309,36 @@ svharness build \
|
|
|
307
309
|
|
|
308
310
|
> Wiki 生成失败不会回滚 harness 骨架,仅输出警告。
|
|
309
311
|
|
|
312
|
+
#### Repomix(`--repomix`){#repomix-repomix}
|
|
313
|
+
|
|
314
|
+
**产物**:`baseline/repomix/repomix-pack.xml` —— 用 [Repomix](https://github.com/yamadashy/repomix) 把 `baseline/code/` 目录树压缩成**一份带行号的 XML**,便于整包喂给上下文窗口有限的模型,或离线归档/比对。
|
|
315
|
+
|
|
316
|
+
**默认关闭的原因**:harness 主流程已依赖 `baseline/code/`(逐文件引用、wiki-writer、knowledge-builder、S61 自动提取等)。Repomix 是**辅助快照**,不是 specs / requirements 的契约来源,且大仓库打包耗时长、产物体积大。
|
|
317
|
+
|
|
318
|
+
**建议开启 `--repomix` 的场景**:
|
|
319
|
+
|
|
320
|
+
| 场景 | 说明 |
|
|
321
|
+
|------|------|
|
|
322
|
+
| 基线体量较大,需要「整库一览」 | 希望 Agent **一次 attach 单文件** 做架构鸟瞰、模块边界梳理、依赖关系初扫,而不是在对话里反复 `@` 上百个路径 |
|
|
323
|
+
| 外部工具只吃单文件上下文 | 使用只支持粘贴/上传**单个**上下文文件的分析器、审计脚本或二次 LLM 流水线 |
|
|
324
|
+
| 离线交付或版本留档 | 需要把某次 `--baseline` 快照固化为**可 diff 的单文件**(例如随 harness 封存、发给评审方) |
|
|
325
|
+
| 与 `baseline/wiki/` 互补 | wiki 偏「说明文档」;Repomix 偏「源码字面快照」。做反向提取前的**代码面**快速摸底时可同时保留两者 |
|
|
326
|
+
|
|
327
|
+
**通常不必开启的场景**:
|
|
328
|
+
|
|
329
|
+
- 基线很小,或 Agent 已能稳定按路径阅读 `baseline/code/`(**默认路径即可**)
|
|
330
|
+
- 只关心 harness-build 标准阶段(S40/S50/S85):规格与审查以 `requirements/`、`specs/`、`baseline/code` 为准,**不依赖** Repomix XML
|
|
331
|
+
- 基线含大量二进制/生成物:Repomix 对图片等帮助有限,且会拉长 build 时间(失败仅告警,不回滚骨架)
|
|
332
|
+
|
|
333
|
+
**用法**:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
svharness build --harness-name my-app --baseline ./src --repomix
|
|
337
|
+
# 或配置:build.repomix: true(须同时配置 build.baseline)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
> Repomix 步骤在 baseline 拷贝**之后**执行;打包失败不会回滚 harness 骨架。已构建的 harness 可带 `--force` 重新 `build` 并补上 `--repomix`。
|
|
341
|
+
|
|
310
342
|
### Agent 适配器
|
|
311
343
|
|
|
312
344
|
| Agent | skill 目录 | 扩展名 | 额外处理 |
|
|
@@ -341,6 +373,8 @@ svharness apply --harness ../my-app-harness --target ./to-apply-project --clone
|
|
|
341
373
|
- `--inject-build-main-bridge` 开启时:在 `<target>/<adapter.skillsDir>/harness-build-skills-bridge/` 写入桥接 skill(仅用于构建流二次改造)
|
|
342
374
|
- 目标 `.gitignore` 会在文件尾部 append 注入路径(幂等去重)
|
|
343
375
|
|
|
376
|
+
**Repomix 与 apply**:若 build 时启用了 `--repomix` 且产物存在,CLI 会回写 `harness.yaml` → `baseline.repomix_pack`,并在 `apply` 时向 `harness-apply-skills-main` 与项目根 `AGENTS.md`/`CLAUDE.md` 注入 Repomix 路径与用法(权威参考实现仍为 `baseline/code/`)。详见上文 [Repomix(`--repomix`)](#repomix-repomix)。
|
|
377
|
+
|
|
344
378
|
#### references 内容引用 → apply_skill_registry(S60,由 Agent 写入)
|
|
345
379
|
|
|
346
380
|
S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某份 references 属于**内容引用**(apply 时要按 `references/md/` 原文指导开发),Agent 应:
|
|
@@ -435,6 +469,10 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
|
|
|
435
469
|
|
|
436
470
|
把本地原始需求文档(`.pdf / .docx / .pptx / .xlsx / .html / .epub / .txt / .csv / .json / ...`)通过**云端部署**的 `markitdown_serve`(FastAPI + Microsoft MarkItDown)批量转为 Markdown,产物统一落到 `<harness>/<type>/md/`(`type` 为 `requirements` 或 `references`),直接喂给下游 `S40_extract_requirements` 条目化流程,显著提升 specs 生成质量的稳定性。
|
|
437
471
|
|
|
472
|
+
> **表格清洗(xlsx/xls/csv)**:源文件为 `.xlsx`、`.xls` 或 `.csv` 时,转换完成后自动清理 Markdown 表格中的 `NaN`、全空行、以及数据全空且表头为 `Unnamed: N` 的列(CLI 与服务端双端生效)。对已存在的产物 md 可执行:`node scripts/cleanup-spreadsheet-md.js <dirOrFile>`。
|
|
473
|
+
|
|
474
|
+
> **Sheet 拆分(xlsx/xls)**:多 sheet 工作簿转换后,MarkItDown 以 `## SheetName` 分隔各 sheet。CLI 默认在保留合并 md 的同时,将各 sheet 写入 `<basename>_split/`(含 `README.md` 索引);`--no-split-sheets` 可关闭。已有合并 md 可执行:`node scripts/split-spreadsheet-md.js <dirOrFile>`。
|
|
475
|
+
|
|
438
476
|
> **架构约束**:CLI 只是 HTTP 客户端,**不 spawn / 不装 Python / 不管进程**。服务端代码集中在 `svharnessbuild/markitdown_serve/` 便于仓内维护,但**不随 npm 包分发**,部署方式详见 [markitdown_serve/README.md](./markitdown_serve/README.md)。
|
|
439
477
|
|
|
440
478
|
```bash
|
|
@@ -464,6 +502,8 @@ svharness convert --verbose
|
|
|
464
502
|
| `--timeout-sec <n>` | 单请求超时秒数 | `120`(2 分钟) |
|
|
465
503
|
| `--type <type>` | 目标子目录:`requirements`(正式需求)\| `references`(参考资料) | `requirements` |
|
|
466
504
|
| `--force` | flag,覆盖已存在同名 `.md`(默认自动追加 `-1`、`-2` 后缀) | `false` |
|
|
505
|
+
| `--split-sheets-suffix <suffix>` | xlsx/xls 按 sheet 拆分的子目录后缀 | `_split` |
|
|
506
|
+
| `--no-split-sheets` | flag,不将 xlsx/xls 合并 md 按 `##` 拆分为多文件 | `false`(默认拆分) |
|
|
467
507
|
| `-y, --yes` | flag,跳过交互确认 | `false` |
|
|
468
508
|
| `--verbose` | flag,显示详细日志 | `false` |
|
|
469
509
|
|
|
@@ -703,6 +743,9 @@ node bin\cli.js build --harness-name demo --arch android-compose --agent codecha
|
|
|
703
743
|
### 发布
|
|
704
744
|
|
|
705
745
|
```bash
|
|
746
|
+
npm version 0.14.6
|
|
747
|
+
npm run build
|
|
748
|
+
|
|
706
749
|
npm login
|
|
707
750
|
npm publish --access public # prepublishOnly 自动执行 build
|
|
708
751
|
```
|
package/dist/commands/apply.js
CHANGED
|
@@ -10,6 +10,7 @@ const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
|
10
10
|
const prompts_1 = __importDefault(require("prompts"));
|
|
11
11
|
const adapters_1 = require("../adapters");
|
|
12
12
|
const apply_project_entry_1 = require("../core/apply-project-entry");
|
|
13
|
+
const repomix_apply_hint_1 = require("../core/repomix-apply-hint");
|
|
13
14
|
const validate_args_1 = require("../utils/validate-args");
|
|
14
15
|
const logger_1 = require("../utils/logger");
|
|
15
16
|
const version_1 = require("../utils/version");
|
|
@@ -322,9 +323,14 @@ async function injectThinMainSkill(input) {
|
|
|
322
323
|
await fs_extra_1.default.remove(dstDir);
|
|
323
324
|
}
|
|
324
325
|
const raw = await fs_extra_1.default.readFile(templatePath, 'utf8');
|
|
325
|
-
const
|
|
326
|
+
const repomixHint = await (0, repomix_apply_hint_1.buildRepomixApplyHintReplacement)({
|
|
327
|
+
harnessRoot: input.harnessRoot,
|
|
328
|
+
harnessDirName: input.harnessDirName,
|
|
329
|
+
blockquote: false,
|
|
330
|
+
});
|
|
331
|
+
const rendered = (0, repomix_apply_hint_1.applyRepomixHintPlaceholder)(raw
|
|
326
332
|
.replace(/__HARNESS_ROOT_REL__/g, `./${input.harnessDirName}`)
|
|
327
|
-
.replace(/__HARNESS_DIR_NAME__/g, input.harnessDirName);
|
|
333
|
+
.replace(/__HARNESS_DIR_NAME__/g, input.harnessDirName), repomixHint, repomix_apply_hint_1.REPOMIX_APPLY_HINT_PLACEHOLDER);
|
|
328
334
|
const content = input.adapter.transform
|
|
329
335
|
? input.adapter.transform(rendered, `${DISPATCHER_SKILL_NAME}.md`)
|
|
330
336
|
: rendered;
|
|
@@ -707,6 +713,7 @@ async function runApply(opts) {
|
|
|
707
713
|
const thinMainPath = await injectThinMainSkill({
|
|
708
714
|
templatesRoot,
|
|
709
715
|
targetRoot,
|
|
716
|
+
harnessRoot: effectiveHarnessRoot,
|
|
710
717
|
adapter,
|
|
711
718
|
harnessDirName,
|
|
712
719
|
force: !!opts.force,
|
package/dist/commands/convert.js
CHANGED
|
@@ -27,6 +27,8 @@ const logger_1 = require("../utils/logger");
|
|
|
27
27
|
const validate_args_1 = require("../utils/validate-args");
|
|
28
28
|
const markitdown_client_1 = require("../core/markitdown-client");
|
|
29
29
|
const doc_intake_paths_1 = require("../core/doc-intake-paths");
|
|
30
|
+
const markdown_table_cleanup_1 = require("../core/markdown-table-cleanup");
|
|
31
|
+
const markdown_sheet_split_1 = require("../core/markdown-sheet-split");
|
|
30
32
|
/**
|
|
31
33
|
* Extensions recognised by Microsoft MarkItDown (v0.x). Anything outside this
|
|
32
34
|
* whitelist is skipped client-side to avoid a round-trip that is guaranteed to
|
|
@@ -85,7 +87,12 @@ async function runConvert(opts) {
|
|
|
85
87
|
configRows.push({ label: 'harness ', value: v.harnessRoot });
|
|
86
88
|
configRows.push({ label: 'type ', value: v.type });
|
|
87
89
|
}
|
|
88
|
-
configRows.push({ label: 'output dir ', value: outDir }, { label: 'endpoint ', value: v.endpoint }, { label: 'concurrency', value: String(v.concurrency) }, { label: 'max file ', value: `${v.maxFileMB} MB` }, { label: 'timeout ', value: `${v.timeoutSec}s` }, { label: 'force ', value: opts.force ? 'yes' : 'no' }
|
|
90
|
+
configRows.push({ label: 'output dir ', value: outDir }, { label: 'endpoint ', value: v.endpoint }, { label: 'concurrency', value: String(v.concurrency) }, { label: 'max file ', value: `${v.maxFileMB} MB` }, { label: 'timeout ', value: `${v.timeoutSec}s` }, { label: 'force ', value: opts.force ? 'yes' : 'no' }, {
|
|
91
|
+
label: 'split sheets',
|
|
92
|
+
value: opts.splitSheets === false
|
|
93
|
+
? 'no'
|
|
94
|
+
: `yes (${opts.splitSheetsSuffix ?? '_split'})`,
|
|
95
|
+
});
|
|
89
96
|
logger_1.logger.configBox('svharness convert', configRows);
|
|
90
97
|
// 1. Collect candidate files.
|
|
91
98
|
const candidates = await collectFiles(v.input, cwd);
|
|
@@ -321,13 +328,41 @@ async function uploadOne(source, outDir, used, v, opts) {
|
|
|
321
328
|
const basename = node_path_1.default.basename(source, node_path_1.default.extname(source));
|
|
322
329
|
const outPath = await resolveOutputPath(outDir, basename, used, !!opts.force);
|
|
323
330
|
used.add(outPath);
|
|
331
|
+
const splitSheets = opts.splitSheets !== false;
|
|
332
|
+
const splitSuffix = opts.splitSheetsSuffix ?? '_split';
|
|
324
333
|
logger_1.logger.debug(`upload ${source} -> ${outPath}`);
|
|
325
334
|
try {
|
|
326
335
|
const resp = await (0, markitdown_client_1.postConvertWithRetry)(v.endpoint, source, v.timeoutSec * 1000);
|
|
327
|
-
|
|
328
|
-
|
|
336
|
+
const ext = node_path_1.default.extname(source).toLowerCase();
|
|
337
|
+
let markdown = resp.markdown;
|
|
338
|
+
if (markdown_table_cleanup_1.XLSX_CONVERT_EXTS.has(ext)) {
|
|
339
|
+
const cleaned = (0, markdown_table_cleanup_1.cleanupSpreadsheetMarkdown)(markdown);
|
|
340
|
+
markdown = cleaned.markdown;
|
|
341
|
+
const { stats } = cleaned;
|
|
342
|
+
if (stats.nanCellsReplaced > 0 ||
|
|
343
|
+
stats.rowsRemoved > 0 ||
|
|
344
|
+
stats.columnsRemoved > 0) {
|
|
345
|
+
logger_1.logger.debug(`spreadsheet cleanup ${node_path_1.default.basename(source)}: ` +
|
|
346
|
+
`NaN=${stats.nanCellsReplaced} rows-=${stats.rowsRemoved} cols-=${stats.columnsRemoved} ` +
|
|
347
|
+
`bytes ${stats.charsBefore}->${stats.charsAfter}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await fs_extra_1.default.writeFile(outPath, markdown, 'utf8');
|
|
351
|
+
const bytes = Buffer.byteLength(markdown, 'utf8');
|
|
352
|
+
const outputs = [outPath];
|
|
353
|
+
if (splitSheets && markdown_sheet_split_1.XLSX_SPLIT_EXTS.has(ext)) {
|
|
354
|
+
const split = (0, markdown_sheet_split_1.splitSpreadsheetMarkdownBySheet)(markdown);
|
|
355
|
+
if (split.sections.length >= 2) {
|
|
356
|
+
const splitDir = node_path_1.default.join(outDir, `${basename}${splitSuffix}`);
|
|
357
|
+
const splitWritten = await (0, markdown_sheet_split_1.writeSheetSplitOutput)(split.sections, splitDir, node_path_1.default.basename(source), { writeIndex: true, force: !!opts.force });
|
|
358
|
+
outputs.push(...splitWritten);
|
|
359
|
+
logger_1.logger.debug(`sheet split ${node_path_1.default.basename(source)}: ${split.sections.length} sections -> ${splitDir}`);
|
|
360
|
+
logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} + ${split.sections.length} sheets in ${node_path_1.default.basename(splitDir)}/ (${bytes}B)`);
|
|
361
|
+
return { source, output: outPath, outputs, status: 'ok', bytes };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
329
364
|
logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)`);
|
|
330
|
-
return { source, output: outPath, status: 'ok', bytes };
|
|
365
|
+
return { source, output: outPath, outputs, status: 'ok', bytes };
|
|
331
366
|
}
|
|
332
367
|
catch (err) {
|
|
333
368
|
const e = err;
|
package/dist/commands/init.js
CHANGED
|
@@ -20,6 +20,8 @@ const build_project_entry_1 = require("../core/build-project-entry");
|
|
|
20
20
|
const project_ignore_1 = require("../core/project-ignore");
|
|
21
21
|
const harness_name_1 = require("../utils/harness-name");
|
|
22
22
|
const yaml_safe_path_1 = require("../utils/yaml-safe-path");
|
|
23
|
+
const harness_yaml_baseline_1 = require("../core/harness-yaml-baseline");
|
|
24
|
+
const repomix_apply_hint_1 = require("../core/repomix-apply-hint");
|
|
23
25
|
const repomix_pack_1 = require("../core/repomix-pack");
|
|
24
26
|
const extra_assets_intake_1 = require("../core/extra-assets-intake");
|
|
25
27
|
const baseline_copy_1 = require("../utils/baseline-copy");
|
|
@@ -100,6 +102,10 @@ async function runInit(opts) {
|
|
|
100
102
|
// modes. Wiki generation consumes this directory by default so it always
|
|
101
103
|
// reflects the baseline the user asked for (not the caller's cwd).
|
|
102
104
|
const hasBaseline = !!validated.baseline;
|
|
105
|
+
const enableRepomix = !!opts.repomix && hasBaseline;
|
|
106
|
+
if (opts.repomix && !hasBaseline) {
|
|
107
|
+
logger_1.logger.warn('--repomix 需要同时提供 --baseline,已忽略 Repomix 打包');
|
|
108
|
+
}
|
|
103
109
|
const baselineCodeDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
|
|
104
110
|
const wikiSourceRoot = node_path_1.default.resolve(opts.wikiSource ?? (hasBaseline ? baselineCodeDir : cwd));
|
|
105
111
|
// Derive the final wiki mode:
|
|
@@ -182,7 +188,9 @@ async function runInit(opts) {
|
|
|
182
188
|
if (hasBaseline) {
|
|
183
189
|
configItems.push({
|
|
184
190
|
label: 'Repomix 基线包',
|
|
185
|
-
value:
|
|
191
|
+
value: enableRepomix
|
|
192
|
+
? `启用(${(0, repomix_pack_1.repomixPackRelFile)()})`
|
|
193
|
+
: '关闭(默认;大基线整库快照/单文件工具/留档时再 --repomix)',
|
|
186
194
|
});
|
|
187
195
|
}
|
|
188
196
|
logger_1.logger.configBox('配置确认', configItems);
|
|
@@ -247,7 +255,7 @@ async function runInit(opts) {
|
|
|
247
255
|
const hasExtraAssets = (opts.extraSkills?.length ?? 0) > 0;
|
|
248
256
|
const stepExtraAssets = hasExtraAssets ? ++stepCursor : 0;
|
|
249
257
|
const stepBaseline = hasBaseline ? ++stepCursor : 0;
|
|
250
|
-
const stepRepomix =
|
|
258
|
+
const stepRepomix = enableRepomix ? ++stepCursor : 0;
|
|
251
259
|
const stepWiki = wikiEnabled ? ++stepCursor : 0;
|
|
252
260
|
const stepState = ++stepCursor;
|
|
253
261
|
const totalSteps = stepCursor;
|
|
@@ -362,8 +370,9 @@ async function runInit(opts) {
|
|
|
362
370
|
logger_1.logger.info('项目 ignore 文件已包含 build 注入路径,未追加新行');
|
|
363
371
|
}
|
|
364
372
|
}
|
|
365
|
-
// 6a. Repomix XML pack of baseline code (
|
|
366
|
-
|
|
373
|
+
// 6a. Repomix XML pack of baseline code (opt-in via --repomix; non-fatal on failure).
|
|
374
|
+
let repomixGenerated = false;
|
|
375
|
+
if (enableRepomix) {
|
|
367
376
|
logger_1.logger.section(`步骤 ${stepRepomix}/${totalSteps} - 生成 baseline Repomix 包`);
|
|
368
377
|
try {
|
|
369
378
|
const packRootFs = wikiSourceRoot;
|
|
@@ -381,6 +390,19 @@ async function runInit(opts) {
|
|
|
381
390
|
onLog: (m) => logger_1.logger.info(` ${m}`),
|
|
382
391
|
});
|
|
383
392
|
logger_1.logger.success(`baseline Repomix 已生成:${node_path_1.default.relative(targetRoot, outAbs)}`);
|
|
393
|
+
repomixGenerated = true;
|
|
394
|
+
await (0, harness_yaml_baseline_1.setBaselineRepomixPack)(targetRoot, (0, repomix_pack_1.repomixPackRelFile)());
|
|
395
|
+
logger_1.logger.info(`已更新 harness.yaml:baseline.repomix_pack`);
|
|
396
|
+
const agentsApplyPath = node_path_1.default.join(targetRoot, 'AGENTS_APPLY.md');
|
|
397
|
+
if (await fs_extra_1.default.pathExists(agentsApplyPath)) {
|
|
398
|
+
const repomixHint = await (0, repomix_apply_hint_1.buildRepomixApplyHintReplacement)({
|
|
399
|
+
harnessRoot: targetRoot,
|
|
400
|
+
harnessDirName,
|
|
401
|
+
blockquote: true,
|
|
402
|
+
});
|
|
403
|
+
const agentsApplyRaw = await fs_extra_1.default.readFile(agentsApplyPath, 'utf8');
|
|
404
|
+
await fs_extra_1.default.outputFile(agentsApplyPath, (0, repomix_apply_hint_1.applyRepomixHintPlaceholder)(agentsApplyRaw, repomixHint, repomix_apply_hint_1.REPOMIX_APPLY_HINT_PLACEHOLDER), 'utf8');
|
|
405
|
+
}
|
|
384
406
|
}
|
|
385
407
|
catch (err) {
|
|
386
408
|
logger_1.logger.warn(`Repomix 生成失败(不回滚 harness 骨架):${err.message}`);
|
|
@@ -563,6 +585,7 @@ async function runInit(opts) {
|
|
|
563
585
|
adapter: adapterForNext,
|
|
564
586
|
agent: validated.agent,
|
|
565
587
|
hasSource: !!validated.baseline,
|
|
588
|
+
repomixGenerated,
|
|
566
589
|
wikiMode,
|
|
567
590
|
});
|
|
568
591
|
}
|
|
@@ -77,6 +77,7 @@ function mergeBuildOptions(cli, configSection, defaults, cmd) {
|
|
|
77
77
|
force: pickBool('force', cli.force, cfg.force, defaults?.force, cmd),
|
|
78
78
|
yes: pickBool('yes', cli.yes, cfg.yes, defaults?.yes, cmd),
|
|
79
79
|
verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
|
|
80
|
+
repomix: pickBool('repomix', cli.repomix, cfg.repomix, undefined, cmd),
|
|
80
81
|
generateWiki: pickBool('generateWiki', cli.generateWiki, cfg.generateWiki, undefined, cmd),
|
|
81
82
|
wikiTasksOnly: pickBool('wikiTasksOnly', cli.wikiTasksOnly, cfg.wikiTasksOnly, undefined, cmd),
|
|
82
83
|
wikiLang: pickString('wikiLang', cli.wikiLang, cfg.wikiLang, cmd),
|
|
@@ -113,6 +114,10 @@ function mergeConvertOptions(cli, configSection, defaults, cmd) {
|
|
|
113
114
|
timeoutSec: pickNumber('timeoutSec', cli.timeoutSec, cfg.timeoutSec, cmd),
|
|
114
115
|
type: pickString('type', cli.type, cfg.type, cmd),
|
|
115
116
|
force: pickBool('force', cli.force, cfg.force, defaults?.force, cmd),
|
|
117
|
+
splitSheets: cli.noSplitSheets
|
|
118
|
+
? false
|
|
119
|
+
: pickBool('splitSheets', cli.splitSheets, cfg.splitSheets, undefined, cmd),
|
|
120
|
+
splitSheetsSuffix: pickString('splitSheetsSuffix', cli.splitSheetsSuffix, cfg.splitSheetsSuffix, cmd),
|
|
116
121
|
yes: pickBool('yes', cli.yes, cfg.yes, defaults?.yes, cmd),
|
|
117
122
|
verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
|
|
118
123
|
};
|
package/dist/config/normalize.js
CHANGED
|
@@ -71,6 +71,7 @@ function pickBuildSection(raw) {
|
|
|
71
71
|
'force',
|
|
72
72
|
'yes',
|
|
73
73
|
'verbose',
|
|
74
|
+
'repomix',
|
|
74
75
|
'generateWiki',
|
|
75
76
|
'wikiTasksOnly',
|
|
76
77
|
]) {
|
|
@@ -107,10 +108,13 @@ function pickConvertSection(raw) {
|
|
|
107
108
|
if (raw[k] !== undefined)
|
|
108
109
|
s[k] = Number(raw[k]);
|
|
109
110
|
}
|
|
110
|
-
for (const k of ['force', 'yes', 'verbose']) {
|
|
111
|
+
for (const k of ['force', 'yes', 'verbose', 'splitSheets']) {
|
|
111
112
|
if (raw[k] !== undefined)
|
|
112
113
|
s[k] = Boolean(raw[k]);
|
|
113
114
|
}
|
|
115
|
+
if (raw.splitSheetsSuffix !== undefined && raw.splitSheetsSuffix !== null) {
|
|
116
|
+
s.splitSheetsSuffix = String(raw.splitSheetsSuffix).trim();
|
|
117
|
+
}
|
|
114
118
|
return s;
|
|
115
119
|
}
|
|
116
120
|
function pickDefaults(raw) {
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.writeApplyProjectEntry = writeApplyProjectEntry;
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const repomix_apply_hint_1 = require("./repomix-apply-hint");
|
|
9
10
|
const logger_1 = require("../utils/logger");
|
|
10
11
|
function toPosix(p) {
|
|
11
12
|
return p.replace(/\\/g, '/');
|
|
@@ -81,7 +82,13 @@ async function writeApplyProjectEntry(input) {
|
|
|
81
82
|
: bridgeHint.length > 0
|
|
82
83
|
? `${raw.trimEnd()}\n\n${bridgeHint}`
|
|
83
84
|
: raw;
|
|
84
|
-
const
|
|
85
|
+
const repomixHint = await (0, repomix_apply_hint_1.buildRepomixApplyHintReplacement)({
|
|
86
|
+
harnessRoot: input.harnessRoot,
|
|
87
|
+
harnessDirName: input.harnessDirName,
|
|
88
|
+
blockquote: true,
|
|
89
|
+
});
|
|
90
|
+
const withRepomixHint = (0, repomix_apply_hint_1.applyRepomixHintPlaceholder)(withBridgeHint, repomixHint, repomix_apply_hint_1.REPOMIX_APPLY_HINT_PLACEHOLDER);
|
|
91
|
+
const rewritten = rewriteEntryReferences(withRepomixHint, input.harnessDirName);
|
|
85
92
|
await fs_extra_1.default.outputFile(dest, rewritten, 'utf8');
|
|
86
93
|
logger_1.logger.success(`已写入项目根 AI 入口:${rel}(由 AGENTS_APPLY.md 重命名)`);
|
|
87
94
|
return rel;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readHarnessYamlDoc = readHarnessYamlDoc;
|
|
7
|
+
exports.getBaselineRepomixPackFromDoc = getBaselineRepomixPackFromDoc;
|
|
8
|
+
exports.setBaselineRepomixPack = setBaselineRepomixPack;
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
|
+
const yaml_safe_path_1 = require("../utils/yaml-safe-path");
|
|
13
|
+
const REPOMIX_PACK_KEY = 'repomix_pack';
|
|
14
|
+
const REPOMIX_PACK_COMMENT = ' # repomix_pack: baseline/repomix/repomix-pack.xml # 仅 --repomix 成功后由 CLI 写入';
|
|
15
|
+
async function readHarnessYamlDoc(harnessRoot) {
|
|
16
|
+
const yamlPath = node_path_1.default.join(harnessRoot, 'harness.yaml');
|
|
17
|
+
if (!(await fs_extra_1.default.pathExists(yamlPath)))
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs_extra_1.default.readFile(yamlPath, 'utf8');
|
|
21
|
+
return js_yaml_1.default.load((0, yaml_safe_path_1.preprocessHarnessYamlText)(raw));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getBaselineRepomixPackFromDoc(doc) {
|
|
28
|
+
if (!doc || typeof doc.baseline !== 'object' || doc.baseline === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const baseline = doc.baseline;
|
|
31
|
+
const v = baseline[REPOMIX_PACK_KEY];
|
|
32
|
+
if (typeof v !== 'string' || !v.trim())
|
|
33
|
+
return undefined;
|
|
34
|
+
return v.trim().replace(/\\/g, '/');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Set or clear `baseline.repomix_pack` in harness.yaml (line edit to preserve comments).
|
|
38
|
+
*/
|
|
39
|
+
async function setBaselineRepomixPack(harnessRoot, repomixPackRel) {
|
|
40
|
+
const yamlPath = node_path_1.default.join(harnessRoot, 'harness.yaml');
|
|
41
|
+
if (!(await fs_extra_1.default.pathExists(yamlPath))) {
|
|
42
|
+
throw new Error(`harness.yaml 不存在:${yamlPath}`);
|
|
43
|
+
}
|
|
44
|
+
let text = await fs_extra_1.default.readFile(yamlPath, 'utf8');
|
|
45
|
+
const rel = repomixPackRel?.trim().replace(/\\/g, '/') ?? null;
|
|
46
|
+
const activeLine = rel ? ` repomix_pack: ${rel}` : REPOMIX_PACK_COMMENT;
|
|
47
|
+
const activePattern = /^\s*repomix_pack:\s*.+$/m;
|
|
48
|
+
const commentPattern = /^\s*#\s*repomix_pack:.*$/m;
|
|
49
|
+
if (activePattern.test(text)) {
|
|
50
|
+
text = text.replace(activePattern, activeLine);
|
|
51
|
+
}
|
|
52
|
+
else if (commentPattern.test(text)) {
|
|
53
|
+
text = text.replace(commentPattern, activeLine);
|
|
54
|
+
}
|
|
55
|
+
else if (rel) {
|
|
56
|
+
const wikiLine = /(^\s*wiki:\s*baseline\/wiki\/.*$)/m;
|
|
57
|
+
if (wikiLine.test(text)) {
|
|
58
|
+
text = text.replace(wikiLine, `$1\n${activeLine}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const baselineBlock = /(^baseline:\s*\n(?:^\s+.+\n)*)/m;
|
|
62
|
+
if (baselineBlock.test(text)) {
|
|
63
|
+
text = text.replace(baselineBlock, (block) => `${block}${activeLine}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await fs_extra_1.default.outputFile(yamlPath, text, 'utf8');
|
|
68
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.XLSX_SPLIT_EXTS = void 0;
|
|
7
|
+
exports.slugifySheetTitle = slugifySheetTitle;
|
|
8
|
+
exports.splitSpreadsheetMarkdownBySheet = splitSpreadsheetMarkdownBySheet;
|
|
9
|
+
exports.writeSheetSplitOutput = writeSheetSplitOutput;
|
|
10
|
+
/**
|
|
11
|
+
* Split spreadsheet-derived Markdown by level-2 headings (`##`).
|
|
12
|
+
*
|
|
13
|
+
* MarkItDown emits one `## SheetName` block per Excel worksheet. This pass
|
|
14
|
+
* writes each block to its own `.md` file under `<basename><suffix>/`.
|
|
15
|
+
*/
|
|
16
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
/** Source extensions that trigger sheet split during `svharness convert`. */
|
|
19
|
+
exports.XLSX_SPLIT_EXTS = new Set(['.xlsx', '.xls']);
|
|
20
|
+
const INVALID_FILENAME_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
|
|
21
|
+
/**
|
|
22
|
+
* Turn a sheet title into a safe filename: `{index}_{slug}.md`.
|
|
23
|
+
*/
|
|
24
|
+
function slugifySheetTitle(title, index) {
|
|
25
|
+
let name = title.trim();
|
|
26
|
+
name = name.replace(INVALID_FILENAME_CHARS, '_');
|
|
27
|
+
name = name.replace(/\s+/g, '_');
|
|
28
|
+
name = name.replace(/^[._]+|[._]+$/g, '');
|
|
29
|
+
if (!name) {
|
|
30
|
+
name = 'section';
|
|
31
|
+
}
|
|
32
|
+
return `${String(index).padStart(2, '0')}_${name}.md`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Split markdown at `##` headings (one section per sheet).
|
|
36
|
+
*/
|
|
37
|
+
function splitSpreadsheetMarkdownBySheet(markdown) {
|
|
38
|
+
const charsBefore = Buffer.byteLength(markdown, 'utf8');
|
|
39
|
+
const lines = markdown.split(/\r?\n/);
|
|
40
|
+
const anchors = [];
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
const match = /^## (.+)$/.exec(lines[i]);
|
|
43
|
+
if (match) {
|
|
44
|
+
anchors.push({ title: match[1].trim(), startLine: i });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (anchors.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
sections: [],
|
|
50
|
+
stats: { sectionsFound: 0, charsBefore, charsAfter: charsBefore },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const sections = [];
|
|
54
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
55
|
+
const endLine = i + 1 < anchors.length ? anchors[i + 1].startLine : lines.length;
|
|
56
|
+
const body = lines.slice(anchors[i].startLine, endLine).join('\n').trimEnd() + '\n';
|
|
57
|
+
const filename = slugifySheetTitle(anchors[i].title, i + 1);
|
|
58
|
+
sections.push({
|
|
59
|
+
title: anchors[i].title,
|
|
60
|
+
body,
|
|
61
|
+
filename,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const charsAfter = sections.reduce((sum, s) => sum + Buffer.byteLength(s.body, 'utf8'), 0);
|
|
65
|
+
return {
|
|
66
|
+
sections,
|
|
67
|
+
stats: {
|
|
68
|
+
sectionsFound: sections.length,
|
|
69
|
+
charsBefore,
|
|
70
|
+
charsAfter,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function buildSplitIndex(sourceName, sections) {
|
|
75
|
+
const lines = [
|
|
76
|
+
`# Split index: ${sourceName}\n`,
|
|
77
|
+
`\nTotal sections: ${sections.length}\n\n`,
|
|
78
|
+
];
|
|
79
|
+
for (const section of sections) {
|
|
80
|
+
lines.push(`- [${section.title}](${section.filename})\n`);
|
|
81
|
+
}
|
|
82
|
+
return lines.join('');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Persist split sections to `outputDir`. Returns absolute paths written.
|
|
86
|
+
*/
|
|
87
|
+
async function writeSheetSplitOutput(sections, outputDir, sourceName, opts) {
|
|
88
|
+
const writeIndex = opts?.writeIndex !== false;
|
|
89
|
+
const force = !!opts?.force;
|
|
90
|
+
await fs_extra_1.default.ensureDir(outputDir);
|
|
91
|
+
const written = [];
|
|
92
|
+
for (const section of sections) {
|
|
93
|
+
const filePath = node_path_1.default.join(outputDir, section.filename);
|
|
94
|
+
if (!force && (await fs_extra_1.default.pathExists(filePath))) {
|
|
95
|
+
throw new Error(`sheet split output exists (pass --force): ${filePath}`);
|
|
96
|
+
}
|
|
97
|
+
await fs_extra_1.default.writeFile(filePath, section.body, 'utf8');
|
|
98
|
+
written.push(filePath);
|
|
99
|
+
}
|
|
100
|
+
if (writeIndex) {
|
|
101
|
+
const indexPath = node_path_1.default.join(outputDir, 'README.md');
|
|
102
|
+
if (!force && (await fs_extra_1.default.pathExists(indexPath))) {
|
|
103
|
+
throw new Error(`sheet split index exists (pass --force): ${indexPath}`);
|
|
104
|
+
}
|
|
105
|
+
await fs_extra_1.default.writeFile(indexPath, buildSplitIndex(sourceName, sections), 'utf8');
|
|
106
|
+
written.push(indexPath);
|
|
107
|
+
}
|
|
108
|
+
return written;
|
|
109
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Post-process Markdown emitted from spreadsheet conversions (`.xlsx` / `.xls` / `.csv`).
|
|
4
|
+
*
|
|
5
|
+
* MarkItDown / pandas represent empty Excel cells as the literal "NaN" in pipe
|
|
6
|
+
* tables. This pass:
|
|
7
|
+
* 1. Replaces NaN cells with empty cells
|
|
8
|
+
* 2. Drops data rows that are entirely empty or only contain a numeric index
|
|
9
|
+
* 3. Drops columns whose data rows are all empty and whose header is blank or
|
|
10
|
+
* "Unnamed: N" (pandas default for empty header cells)
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.XLSX_CONVERT_EXTS = void 0;
|
|
14
|
+
exports.cleanupSpreadsheetMarkdown = cleanupSpreadsheetMarkdown;
|
|
15
|
+
/** Source extensions that trigger table cleanup during `svharness convert`. */
|
|
16
|
+
exports.XLSX_CONVERT_EXTS = new Set([
|
|
17
|
+
'.xlsx',
|
|
18
|
+
'.xls',
|
|
19
|
+
'.csv',
|
|
20
|
+
]);
|
|
21
|
+
const UNNAMED_HEADER = /^Unnamed: \d+$/;
|
|
22
|
+
function isTableRow(line) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
return trimmed.startsWith('|') && trimmed.endsWith('|');
|
|
25
|
+
}
|
|
26
|
+
function parseTableRow(line) {
|
|
27
|
+
const inner = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
28
|
+
return inner.split('|').map((cell) => cell.trim());
|
|
29
|
+
}
|
|
30
|
+
function formatTableRow(cells) {
|
|
31
|
+
return `| ${cells.join(' | ')} |`;
|
|
32
|
+
}
|
|
33
|
+
function normalizeCell(cell) {
|
|
34
|
+
if (cell === 'NaN' || cell === 'nan') {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
return cell;
|
|
38
|
+
}
|
|
39
|
+
function isSparseTemplateRow(row) {
|
|
40
|
+
const nonEmpty = row.filter((cell) => cell !== '');
|
|
41
|
+
if (nonEmpty.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// Excel 预留行常在首列保留序号(2、3、…),其余单元格为空。
|
|
45
|
+
if (nonEmpty.length === 1 && /^\d+\.?\d*$/.test(nonEmpty[0])) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function isSeparatorRow(cells) {
|
|
51
|
+
return (cells.length > 0 &&
|
|
52
|
+
cells.every((cell) => {
|
|
53
|
+
const c = cell.trim();
|
|
54
|
+
return c === '' || /^:?-{3,}:?$/.test(c);
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
function padRow(row, width) {
|
|
58
|
+
const out = row.slice();
|
|
59
|
+
while (out.length < width) {
|
|
60
|
+
out.push('');
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function cleanupTableBlock(lines) {
|
|
65
|
+
if (lines.length === 0) {
|
|
66
|
+
return { lines, nanCellsReplaced: 0, rowsRemoved: 0, columnsRemoved: 0 };
|
|
67
|
+
}
|
|
68
|
+
let nanCellsReplaced = 0;
|
|
69
|
+
const rawRows = lines.map(parseTableRow);
|
|
70
|
+
const colCount = Math.max(...rawRows.map((row) => row.length), 0);
|
|
71
|
+
if (colCount === 0) {
|
|
72
|
+
return { lines, nanCellsReplaced: 0, rowsRemoved: 0, columnsRemoved: 0 };
|
|
73
|
+
}
|
|
74
|
+
const rows = rawRows.map((row) => padRow(row, colCount).map((cell) => {
|
|
75
|
+
const normalized = normalizeCell(cell);
|
|
76
|
+
if (normalized !== cell) {
|
|
77
|
+
nanCellsReplaced += 1;
|
|
78
|
+
}
|
|
79
|
+
return normalized;
|
|
80
|
+
}));
|
|
81
|
+
const hasSeparator = rows.length > 1 && isSeparatorRow(rows[1]);
|
|
82
|
+
const header = rows[0];
|
|
83
|
+
const dataStart = hasSeparator ? 2 : 1;
|
|
84
|
+
const dataRowsBefore = rows.slice(dataStart);
|
|
85
|
+
const dataRows = dataRowsBefore.filter((row) => !isSparseTemplateRow(row));
|
|
86
|
+
const rowsRemoved = dataRowsBefore.length - dataRows.length;
|
|
87
|
+
const keepCols = [];
|
|
88
|
+
for (let col = 0; col < colCount; col += 1) {
|
|
89
|
+
const headerCell = header[col] ?? '';
|
|
90
|
+
const dataEmpty = dataRows.every((row) => (row[col] ?? '') === '');
|
|
91
|
+
if (dataEmpty && (headerCell === '' || UNNAMED_HEADER.test(headerCell))) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
keepCols.push(col);
|
|
95
|
+
}
|
|
96
|
+
if (keepCols.length === 0) {
|
|
97
|
+
keepCols.push(0);
|
|
98
|
+
}
|
|
99
|
+
const columnsRemoved = colCount - keepCols.length;
|
|
100
|
+
const pickCols = (row) => keepCols.map((col) => padRow(row, colCount)[col] ?? '');
|
|
101
|
+
const out = [];
|
|
102
|
+
out.push(formatTableRow(pickCols(header)));
|
|
103
|
+
if (hasSeparator || dataRows.length > 0) {
|
|
104
|
+
out.push(formatTableRow(keepCols.map(() => '---')));
|
|
105
|
+
}
|
|
106
|
+
for (const row of dataRows) {
|
|
107
|
+
out.push(formatTableRow(pickCols(row)));
|
|
108
|
+
}
|
|
109
|
+
return { lines: out, nanCellsReplaced, rowsRemoved, columnsRemoved };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Clean spreadsheet-derived Markdown document-wide (table blocks only).
|
|
113
|
+
*/
|
|
114
|
+
function cleanupSpreadsheetMarkdown(markdown) {
|
|
115
|
+
const charsBefore = Buffer.byteLength(markdown, 'utf8');
|
|
116
|
+
const inputLines = markdown.split(/\r?\n/);
|
|
117
|
+
const outputLines = [];
|
|
118
|
+
let nanCellsReplaced = 0;
|
|
119
|
+
let rowsRemoved = 0;
|
|
120
|
+
let columnsRemoved = 0;
|
|
121
|
+
let index = 0;
|
|
122
|
+
while (index < inputLines.length) {
|
|
123
|
+
const line = inputLines[index];
|
|
124
|
+
if (!isTableRow(line)) {
|
|
125
|
+
outputLines.push(line);
|
|
126
|
+
index += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const block = [];
|
|
130
|
+
while (index < inputLines.length && isTableRow(inputLines[index])) {
|
|
131
|
+
block.push(inputLines[index]);
|
|
132
|
+
index += 1;
|
|
133
|
+
}
|
|
134
|
+
const cleaned = cleanupTableBlock(block);
|
|
135
|
+
nanCellsReplaced += cleaned.nanCellsReplaced;
|
|
136
|
+
rowsRemoved += cleaned.rowsRemoved;
|
|
137
|
+
columnsRemoved += cleaned.columnsRemoved;
|
|
138
|
+
outputLines.push(...cleaned.lines);
|
|
139
|
+
}
|
|
140
|
+
const result = outputLines.join('\n');
|
|
141
|
+
return {
|
|
142
|
+
markdown: result,
|
|
143
|
+
stats: {
|
|
144
|
+
nanCellsReplaced,
|
|
145
|
+
rowsRemoved,
|
|
146
|
+
columnsRemoved,
|
|
147
|
+
charsBefore,
|
|
148
|
+
charsAfter: Buffer.byteLength(result, 'utf8'),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
package/dist/core/next-steps.js
CHANGED
|
@@ -60,11 +60,24 @@ function printNextSteps(input) {
|
|
|
60
60
|
lines.push('4. 随时用下面的命令查看进度: ' +
|
|
61
61
|
picocolors_1.default.cyan('cat .harness-build-state.yaml'));
|
|
62
62
|
lines.push('');
|
|
63
|
-
if (input.
|
|
64
|
-
lines.push(picocolors_1.default.bold('Baseline Repomix(XML
|
|
63
|
+
if (input.repomixGenerated) {
|
|
64
|
+
lines.push(picocolors_1.default.bold('Baseline Repomix(XML)'));
|
|
65
65
|
lines.push(' 输出文件: ' + picocolors_1.default.cyan((0, repomix_pack_1.repomixPackRelFile)()));
|
|
66
66
|
lines.push('');
|
|
67
67
|
}
|
|
68
|
+
else if (input.hasSource) {
|
|
69
|
+
lines.push(picocolors_1.default.bold('Baseline Repomix(可选,默认关闭)'));
|
|
70
|
+
lines.push(' 适用:大基线整库鸟瞰、只接受单文件上下文的工具、封存留档;' +
|
|
71
|
+
'日常 S40/S50 以 ' +
|
|
72
|
+
picocolors_1.default.cyan('baseline/code/') +
|
|
73
|
+
' 为准即可');
|
|
74
|
+
lines.push(' 启用:重新 build 并加 ' +
|
|
75
|
+
picocolors_1.default.cyan('--repomix') +
|
|
76
|
+
'(须保留 --baseline)→ ' +
|
|
77
|
+
picocolors_1.default.cyan((0, repomix_pack_1.repomixPackRelFile)()));
|
|
78
|
+
lines.push(' 说明:svharnessbuild README「Repomix」');
|
|
79
|
+
lines.push('');
|
|
80
|
+
}
|
|
68
81
|
if (wikiMode === 'tasks') {
|
|
69
82
|
lines.push(picocolors_1.default.bold('Baseline wiki 任务清单已生成(默认 tasks-only 模式)'));
|
|
70
83
|
lines.push(' 清单位置: ' + picocolors_1.default.cyan('baseline/wiki/TASKS.md'));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.REPOMIX_APPLY_HINT_PLACEHOLDER = void 0;
|
|
7
|
+
exports.resolveRepomixPackRel = resolveRepomixPackRel;
|
|
8
|
+
exports.renderRepomixApplyHintMarkdown = renderRepomixApplyHintMarkdown;
|
|
9
|
+
exports.buildRepomixApplyHintReplacement = buildRepomixApplyHintReplacement;
|
|
10
|
+
exports.applyRepomixHintPlaceholder = applyRepomixHintPlaceholder;
|
|
11
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const harness_yaml_baseline_1 = require("./harness-yaml-baseline");
|
|
14
|
+
const repomix_pack_1 = require("./repomix-pack");
|
|
15
|
+
exports.REPOMIX_APPLY_HINT_PLACEHOLDER = '__REPOMIX_APPLY_HINT__';
|
|
16
|
+
/**
|
|
17
|
+
* Resolve Repomix pack path relative to harness root: harness.yaml field first, then file existence.
|
|
18
|
+
*/
|
|
19
|
+
async function resolveRepomixPackRel(harnessRoot) {
|
|
20
|
+
const doc = await (0, harness_yaml_baseline_1.readHarnessYamlDoc)(harnessRoot);
|
|
21
|
+
const fromYaml = (0, harness_yaml_baseline_1.getBaselineRepomixPackFromDoc)(doc);
|
|
22
|
+
if (fromYaml) {
|
|
23
|
+
const abs = node_path_1.default.join(harnessRoot, fromYaml);
|
|
24
|
+
if (await fs_extra_1.default.pathExists(abs))
|
|
25
|
+
return fromYaml;
|
|
26
|
+
}
|
|
27
|
+
const defaultRel = (0, repomix_pack_1.repomixPackRelFile)();
|
|
28
|
+
const defaultAbs = node_path_1.default.join(harnessRoot, defaultRel);
|
|
29
|
+
if (await fs_extra_1.default.pathExists(defaultAbs))
|
|
30
|
+
return defaultRel;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function renderRepomixApplyHintMarkdown(input) {
|
|
34
|
+
const harnessScoped = `./${input.harnessDirName}/${input.packRel.replace(/\\/g, '/')}`;
|
|
35
|
+
const codeScoped = `./${input.harnessDirName}/baseline/code/`;
|
|
36
|
+
const body = [
|
|
37
|
+
'**Baseline Repomix(可选快照)**',
|
|
38
|
+
'',
|
|
39
|
+
`- **路径**:\`${input.packRel}\`(项目根视角:\`${harnessScoped}\`)`,
|
|
40
|
+
'- **用途**:将 `baseline/code/` 打成单文件 XML,便于整库鸟瞰、单文件 attach;与 `baseline/wiki/` 互补。',
|
|
41
|
+
'- **优先级**:**权威参考实现仍为** `baseline/code/`(行级引用、specs 追溯、守则不变);Repomix **不得**替代对具体文件的精读,也不得作为 specs 契约来源。',
|
|
42
|
+
`- **读取**:优先 Read 上述 XML;上下文过大时只读相关片段,或退回 \`${codeScoped}<repo-relative>\`。`,
|
|
43
|
+
'- **典型场景**:架构摸底、模块边界初扫;实现细节与交付校验仍以 specs + `baseline/code/` 为准。',
|
|
44
|
+
];
|
|
45
|
+
if (input.blockquote) {
|
|
46
|
+
return ['> Repomix 快照(build 已启用 `--repomix`)', ...body.map((l) => (l ? `> ${l}` : '>'))].join('\n');
|
|
47
|
+
}
|
|
48
|
+
return ['## Baseline Repomix(可选快照)', '', ...body].join('\n');
|
|
49
|
+
}
|
|
50
|
+
async function buildRepomixApplyHintReplacement(input) {
|
|
51
|
+
const packRel = await resolveRepomixPackRel(input.harnessRoot);
|
|
52
|
+
if (!packRel)
|
|
53
|
+
return '';
|
|
54
|
+
const block = renderRepomixApplyHintMarkdown({
|
|
55
|
+
harnessDirName: input.harnessDirName,
|
|
56
|
+
packRel,
|
|
57
|
+
blockquote: input.blockquote,
|
|
58
|
+
});
|
|
59
|
+
return `${block}\n\n`;
|
|
60
|
+
}
|
|
61
|
+
function applyRepomixHintPlaceholder(content, hint, placeholder = exports.REPOMIX_APPLY_HINT_PLACEHOLDER) {
|
|
62
|
+
if (content.includes(placeholder)) {
|
|
63
|
+
return content.replace(new RegExp(placeholder, 'g'), hint.trimEnd() ? hint : '');
|
|
64
|
+
}
|
|
65
|
+
if (!hint.trim())
|
|
66
|
+
return content;
|
|
67
|
+
return `${content.trimEnd()}\n\n${hint}`;
|
|
68
|
+
}
|
|
@@ -6,6 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.REPOMIX_PACK_FILENAME = exports.REPOMIX_BASELINE_REL_DIR = void 0;
|
|
7
7
|
exports.repomixPackRelFile = repomixPackRelFile;
|
|
8
8
|
exports.runRepomixPackBaseline = runRepomixPackBaseline;
|
|
9
|
+
/**
|
|
10
|
+
* Optional baseline snapshot via Repomix (single XML under `baseline/repomix/`).
|
|
11
|
+
* Enabled only when `svharness build --repomix` is passed with `--baseline`.
|
|
12
|
+
* Complements `baseline/code/` (per-file references); see svharnessbuild README § Repomix.
|
|
13
|
+
*/
|
|
9
14
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
16
|
const repomix_1 = require("repomix");
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ function buildSectionFromOpts(opts, harnessName) {
|
|
|
55
55
|
force: opts.force,
|
|
56
56
|
yes: opts.yes,
|
|
57
57
|
verbose: opts.verbose,
|
|
58
|
+
repomix: opts.repomix,
|
|
58
59
|
generateWiki: opts.generateWiki,
|
|
59
60
|
wikiTasksOnly: opts.wikiTasksOnly,
|
|
60
61
|
wikiLang: opts.wikiLang === 'en' ? 'en' : opts.wikiLang === 'zh' ? 'zh' : undefined,
|
|
@@ -87,6 +88,7 @@ async function runBuildAction(opts, cmd) {
|
|
|
87
88
|
force: !!merged.force,
|
|
88
89
|
yes: !!merged.yes,
|
|
89
90
|
verbose: !!merged.verbose,
|
|
91
|
+
repomix: !!merged.repomix,
|
|
90
92
|
generateWiki: !!merged.generateWiki,
|
|
91
93
|
wikiTasksOnly: !!merged.wikiTasksOnly,
|
|
92
94
|
wikiLang: merged.wikiLang === 'en' ? 'en' : merged.wikiLang === 'zh' ? 'zh' : undefined,
|
|
@@ -120,6 +122,7 @@ function attachBuildOptions(cmd) {
|
|
|
120
122
|
.option('--arch <arch>', '架构模板:' + (0, validate_args_1.listSupportedArches)().join(' | '), DEFAULT_ARCH)
|
|
121
123
|
.option('--agent <agent>', '目标 Agent:' + (0, validate_args_1.listSupportedAgents)().join(' | '), DEFAULT_AGENT)
|
|
122
124
|
.option('--baseline <path|url>', '【可选】基线源码路径或 Git 仓库地址')
|
|
125
|
+
.option('--repomix', '【可选,需 --baseline】生成 baseline/repomix/repomix-pack.xml:大基线整库鸟瞰、单文件上下文工具、封存留档;日常 build 默认关闭,详见 README「Repomix」')
|
|
123
126
|
.option('--requirements <path>', '【可选】需求文档输入路径(文件或目录)')
|
|
124
127
|
.option('--references <path>', '【可选】参考资料输入路径(文件或目录)')
|
|
125
128
|
.option('--extra-skills <path...>', '【可选】额外运行期资源(skills/rules 混放,先入 _incoming)')
|
|
@@ -231,6 +234,8 @@ function main() {
|
|
|
231
234
|
.option('--timeout-sec <n>', '【默认 120】超时秒数', (v) => Number(v))
|
|
232
235
|
.option('--type <type>', 'requirements | references')
|
|
233
236
|
.option('--force', '覆盖已存在同名 .md')
|
|
237
|
+
.option('--split-sheets-suffix <suffix>', 'xlsx/xls 按 sheet(##)拆分时的子目录后缀', '_split')
|
|
238
|
+
.option('--no-split-sheets', '不将 xlsx/xls 合并 md 按 ## 拆分为多文件')
|
|
234
239
|
.option('-y, --yes', '跳过交互确认')
|
|
235
240
|
.option('--verbose', '显示详细日志')
|
|
236
241
|
.action(async (opts, cmd) => {
|
|
@@ -246,6 +251,8 @@ function main() {
|
|
|
246
251
|
timeoutSec: opts.timeoutSec,
|
|
247
252
|
type: opts.type,
|
|
248
253
|
force: opts.force,
|
|
254
|
+
splitSheetsSuffix: opts.splitSheetsSuffix,
|
|
255
|
+
noSplitSheets: opts.noSplitSheets,
|
|
249
256
|
yes: opts.yes,
|
|
250
257
|
verbose: opts.verbose,
|
|
251
258
|
}, loaded?.config.convert, loaded?.config.defaults, cmd);
|
|
@@ -259,6 +266,8 @@ function main() {
|
|
|
259
266
|
timeoutSec: merged.timeoutSec,
|
|
260
267
|
type: merged.type,
|
|
261
268
|
force: !!merged.force,
|
|
269
|
+
splitSheets: merged.noSplitSheets ? false : merged.splitSheets,
|
|
270
|
+
splitSheetsSuffix: merged.splitSheetsSuffix,
|
|
262
271
|
yes: !!merged.yes,
|
|
263
272
|
verbose: !!merged.verbose,
|
|
264
273
|
});
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- `description` 必须覆盖 `source_excerpt` 里的条件、数值、枚举、时序,不得退化为标题短语。
|
|
29
29
|
- 推荐使用长度启发式审查:`description` 明显短于 `source_excerpt`(如 < 50%)时标记风险并复核。
|
|
30
30
|
- `title` 仅用于索引,禁止用 `title` 替代完整需求描述。
|
|
31
|
+
- `source_file` 必须指向 Agent 可读文本:优先 `requirements/md/<同名>.md`(convert 产物),或 `requirements/raw/` 下的 md/txt/代码文件;禁止引用 xlsx/pdf/docx 等难解析格式。
|
|
31
32
|
|
|
32
33
|
5. **覆盖率报告是 S40 必备产物**
|
|
33
34
|
- 必须生成 `requirements/coverage-report.yaml`。
|
|
@@ -28,7 +28,7 @@ specs/
|
|
|
28
28
|
- `source_excerpt`: 源文档原文摘录(可多行)
|
|
29
29
|
- `description`: 完整需求陈述(不得弱于 source_excerpt)
|
|
30
30
|
- `source_anchor`: 可追踪源锚点(如 `SyRD-*`、章节+行号)
|
|
31
|
-
- `source_file`:
|
|
31
|
+
- `source_file`: Agent 可读的文本源路径——`requirements/md/<同名>.md`(convert 产物)或 `requirements/raw/` 下的 md/txt/代码文件;禁止引用 xlsx/pdf/docx 等难解析格式
|
|
32
32
|
- `source_section`: 源章节或表格区域
|
|
33
33
|
- `aggregates`: 聚合锚点列表(仅在显式聚合时)
|
|
34
34
|
- `aggregation_reason`: 聚合原因(聚合时必填)
|
|
@@ -53,7 +53,9 @@ items:
|
|
|
53
53
|
允许整理语序,但信息集合不得缩小。
|
|
54
54
|
acceptance: "如何验证该需求满足"
|
|
55
55
|
source_anchor: "SyRD-..."
|
|
56
|
-
|
|
56
|
+
# AI 可读文本源(二选一,禁止引用 xlsx/pdf/docx 等难解析格式):
|
|
57
|
+
source_file: "requirements/md/<file>.md" # 原始为 pdf/xlsx/docx 等 → 引用 convert 产物
|
|
58
|
+
# source_file: "requirements/raw/<file>.md" # 或 raw 下本身可读的 md/txt/代码文件
|
|
57
59
|
source_section: "01_Function Spec"
|
|
58
60
|
aggregates: []
|
|
59
61
|
waived: false
|
|
@@ -68,7 +70,11 @@ items:
|
|
|
68
70
|
- `source_excerpt` 必须来自源文档原文;禁止写成「见上文」「同前」等替代语。
|
|
69
71
|
- `description` 必须覆盖 `source_excerpt` 的关键约束;不得删减条件、数值、枚举、时序。
|
|
70
72
|
- `traces_to` 初始可为空,阶段 B 生成 specs 后回填。
|
|
71
|
-
- `source_doc`
|
|
73
|
+
- `source_doc` 记录原始入库路径(`requirements/raw/<原始文件名>`),用于资产追溯。
|
|
74
|
+
- `source_file` 必须指向 **Agent 可直接读取的文本源**,路径二选一:
|
|
75
|
+
- `requirements/md/<同名>.md` —— 原始为 pdf/xlsx/docx 等时,引用 S30 convert 后的 Markdown;
|
|
76
|
+
- `requirements/raw/<file>` —— 原始本身即为 `.md`、`.txt` 或源代码文件(如 `.kt`、`.xml`、`.proto` 等)。
|
|
77
|
+
- **禁止**将 `requirements/raw/` 下的 xlsx、pdf、docx、图片等 AI 难处理格式写入 `source_file`。
|
|
72
78
|
- 聚合仅在用户确认后允许,且必须带 `aggregates + aggregation_reason`。
|
|
73
79
|
- 产出后必须校验 `requirements/yaml/schema.json`(若存在);失败不得进入 S50。
|
|
74
80
|
|
|
@@ -93,7 +99,8 @@ items:
|
|
|
93
99
|
extraction_strategy: md_primary
|
|
94
100
|
generated_at: "..."
|
|
95
101
|
sources:
|
|
96
|
-
- file: "requirements/
|
|
102
|
+
- file: "requirements/md/<file>.md" # 或 requirements/raw/<可读文本文件>
|
|
103
|
+
source_doc: "requirements/raw/<原始文件>" # 可选,保留原始资产路径
|
|
97
104
|
anchors_total: 100
|
|
98
105
|
anchors_mapped: 96
|
|
99
106
|
anchors_waived: 4
|
|
@@ -100,6 +100,7 @@ __BUILD_MAIN_BRIDGE_HINT__
|
|
|
100
100
|
|------|------|------|
|
|
101
101
|
| `baseline/wiki/` | 项目知识摘要,优先建立上下文 | 只读 |
|
|
102
102
|
| `baseline/code/` | 权威参考实现 | 只读 |
|
|
103
|
+
| `baseline/repomix/repomix-pack.xml` | 可选:Repomix 整库 XML 快照(仅 build 启用 `--repomix` 时存在;见 `harness.yaml` → `baseline.repomix_pack`) | 只读 |
|
|
103
104
|
| `specs/signals/` | 信号/协议规格 | 只读 |
|
|
104
105
|
| `specs/ui/` | UI 规格 | 只读 |
|
|
105
106
|
| `specs/behavior/` | 行为规格 | 只读 |
|
|
@@ -108,6 +109,10 @@ __BUILD_MAIN_BRIDGE_HINT__
|
|
|
108
109
|
|
|
109
110
|
> `baseline/code/` 是“参考实现”的权威来源。
|
|
110
111
|
> 你仍需阅读并修改目标项目业务源码来完成交付,但不得把 `baseline/code/` 之外的路径当作规范样本来源。
|
|
112
|
+
>
|
|
113
|
+
> 若存在 `baseline/repomix/repomix-pack.xml`,清单字段为 `harness.yaml` → `baseline.repomix_pack`;`svharness apply` 会在项目根入口与本 skill 中注入详细用法。
|
|
114
|
+
|
|
115
|
+
__REPOMIX_APPLY_HINT__
|
|
111
116
|
|
|
112
117
|
### 4.3 编码执行
|
|
113
118
|
|
|
@@ -43,6 +43,7 @@ references: # 参考资料(不参与条目化)
|
|
|
43
43
|
baseline:
|
|
44
44
|
code: baseline/code/ # 基线代码快照(参考样本)
|
|
45
45
|
wiki: baseline/wiki/ # 基线工程 wiki
|
|
46
|
+
# repomix_pack: baseline/repomix/repomix-pack.xml # 仅 --repomix 成功后由 CLI 写入
|
|
46
47
|
|
|
47
48
|
# Agent 运行时环境(规则、技能、工具、记忆)。
|
|
48
49
|
# incoming_* 仅构建期:`svharness build --extra-skills` 将资源暂存于此并生成 manifest,S65 确认后分流写入 rules/skills。
|
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
"description": { "type": "string", "minLength": 1 },
|
|
50
50
|
"acceptance": { "type": "string", "minLength": 1 },
|
|
51
51
|
"source_anchor": { "type": "string", "minLength": 1 },
|
|
52
|
-
"source_file": {
|
|
52
|
+
"source_file": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 1,
|
|
55
|
+
"description": "Agent 可读文本源:requirements/md/<同名>.md(convert 产物)或 requirements/raw/ 下的 md/txt/代码文件;禁止 xlsx/pdf/docx 等"
|
|
56
|
+
},
|
|
53
57
|
"source_section": { "type": "string", "minLength": 1 },
|
|
54
58
|
"aggregates": {
|
|
55
59
|
"type": "array",
|
|
@@ -20,6 +20,9 @@ build:
|
|
|
20
20
|
referencesNote: 设计稿与接口文档
|
|
21
21
|
extraSkills:
|
|
22
22
|
- ./team-skills/custom-skill
|
|
23
|
+
# Repomix:将 baseline/code 打成单文件 baseline/repomix/repomix-pack.xml(默认 false)
|
|
24
|
+
# 适合:大基线整库鸟瞰、外部单文件上下文工具、封存留档;日常 harness-build 不必开
|
|
25
|
+
repomix: false
|
|
23
26
|
generateWiki: false
|
|
24
27
|
wikiLang: zh
|
|
25
28
|
force: false
|