svharness 0.14.2 → 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 +50 -3
- package/dist/commands/apply.js +9 -2
- package/dist/commands/convert.js +39 -4
- package/dist/commands/doctor/check-requirements-fidelity.js +96 -0
- package/dist/commands/doctor/check-requirements.js +33 -1
- package/dist/commands/doctor/check-specs-depth.js +165 -0
- package/dist/commands/doctor/index.js +4 -0
- 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-chinese-only.md +2 -2
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +5 -2
- package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +7 -1
- package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +10 -1
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +8 -4
- package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +3 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +48 -7
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +5 -0
- package/templates/_shared/meta/harness.yaml.ejs +2 -0
- package/templates/_shared/skeleton/requirements/yaml/schema.json +75 -0
- package/templates/_shared/skeleton/specs/behavior/schema.json +8 -8
- package/templates/_shared/skeleton/specs/interfaces/schema.json +4 -4
- package/templates/_shared/skeleton/specs/signals/schema.json +12 -1
- package/templates/_shared/skeleton/specs/ui/schema.json +5 -4
- package/templates/svharness.config.example.yaml +3 -0
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
以下产出**必须**使用中文撰写:
|
|
8
8
|
|
|
9
|
-
- `requirements/yaml/*.requirements.yaml` 中 `description`、`acceptance` 字段
|
|
9
|
+
- `requirements/yaml/*.requirements.yaml` 中 `title`、`source_excerpt`、`description`、`acceptance` 字段
|
|
10
10
|
- `specs/**/*.yaml` 中所有描述性字段(如 `description`、`note`、`summary`、`purpose`)
|
|
11
11
|
- `agent-env/rules/**/*.md` 或 `*.mdc` 中正文段落、正反例注释、检测说明
|
|
12
12
|
- `agent-env/skills/*/SKILL.md` 正文及 description
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
## 正反例
|
|
33
33
|
|
|
34
34
|
正例:
|
|
35
|
-
> `description: "
|
|
35
|
+
> `description: "当 P_TMRAVA=1 且车辆处于驻车状态时显示预约充电入口;当 P_TMRAVA=0 或车速>0 时隐藏入口。"`
|
|
36
36
|
|
|
37
37
|
反例(严禁):
|
|
38
38
|
> `description: "Show or hide the scheduled charging settings entry based on P_TMRAVA signal"`
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
3. **S30_convert_docs** — 将 `requirements/raw/` 和 `references/raw/` 中的非 Markdown 原始文档统一转为 Markdown,产物落到对应 `md/` 目录。准入:S20_collect_inputs DONE;退出:每份源文档在对应 `md/` 目录中有同基名的 `.md` 文件。
|
|
15
15
|
4. **S40_extract_requirements** — 由 `harness-build-skill-spec-builder` 将 raw 条目化到 `requirements/yaml/`。退出:
|
|
16
16
|
- 已生成 `requirements/coverage-report.yaml`;
|
|
17
|
+
- `requirements/yaml/schema.json`(若存在)校验通过;
|
|
17
18
|
- `unmapped` 为空,或已全部完成 waiver 与用户确认;
|
|
18
|
-
- 每条需求具备稳定 id
|
|
19
|
+
- 每条需求具备稳定 id 与可追溯锚点(允许有备案聚合,但不得无依据粗化);
|
|
20
|
+
- 每条需求包含 `title`、`source_excerpt`、`description`,且不存在明显缩略风险(门禁策略可由 doctor/审查报告提供证据)。
|
|
19
21
|
5. **S50_generate_specs** — 生成 `specs/{signals,ui,behavior,interfaces}/*.yaml`。退出:
|
|
20
22
|
- 全部通过 `specs/*/schema.json` 校验;
|
|
21
23
|
- 已产出 spec 覆盖率结果(建议 `specs/coverage-report.yaml`);
|
|
22
|
-
- 每条 REQ 有 spec 映射或显式 N/A
|
|
24
|
+
- 每条 REQ 有 spec 映射或显式 N/A 理由;
|
|
25
|
+
- 深度启发式无 Critical(如 index-only signals、空 guard、断裂 bound_* 引用)。
|
|
23
26
|
6. **S60_process_references**(阶段叙事:**references 处理**)— 对 S20 中的 references 执行处理与结构化:
|
|
24
27
|
- 必须执行 `svharness convert`,不得跳过转换直接抽取;
|
|
25
28
|
- 必须产出结构化索引(规制约束 / skills 候选 / signals / manuals,缺项需显式标注);
|
|
@@ -21,10 +21,16 @@
|
|
|
21
21
|
|
|
22
22
|
1. **需求可追溯**(若存在 requirements/yaml):每条在已声明 spec 层有映射或 waiver。
|
|
23
23
|
2. **覆盖率完整**:`requirements/coverage-report.yaml` 与(若存在)`specs/coverage-report.yaml` 可解析;`unmapped` 闭环。
|
|
24
|
-
3. **描述完整性**:无 TBD/placeholder
|
|
24
|
+
3. **描述完整性**:无 TBD/placeholder 占位语义,且 `description` 不得明显弱于 `source_excerpt`。
|
|
25
25
|
4. **references / wiki / agent-env**(若目录存在):与 S60、baseline 策略一致。
|
|
26
26
|
5. **流程合规**、**中文与 agent-agnostic** 扫尾。
|
|
27
27
|
|
|
28
|
+
补充抽检要求(至少随机 N=5 条或按项目规模调整):
|
|
29
|
+
|
|
30
|
+
- 抽检 `requirements/yaml`:确认 `source_excerpt` 保留原文约束(条件/数值/枚举),`description` 未删减关键语义。
|
|
31
|
+
- 抽检 `specs/signals`:识别 index-only 信号(仅 id/name/type 且缺少协议/枚举语义)。
|
|
32
|
+
- 抽检 `specs/behavior`:`transitions[].guard`/`action` 为空或占位语义应计入风险。
|
|
33
|
+
|
|
28
34
|
### Part B — 深度质量(按 specs.layers 启用)
|
|
29
35
|
|
|
30
36
|
- 加载 `agent-env/review-profiles/<arch>.yaml` 或 `_default.yaml`。
|
|
@@ -23,7 +23,14 @@
|
|
|
23
23
|
- `aggregation_reason`
|
|
24
24
|
- 用户表单确认
|
|
25
25
|
|
|
26
|
-
4.
|
|
26
|
+
4. **原文优先,禁止缩略**
|
|
27
|
+
- `source_excerpt` 必须是可追溯锚点对应的原文摘录,禁止写成「见上文」「同前」「TBD」。
|
|
28
|
+
- `description` 必须覆盖 `source_excerpt` 里的条件、数值、枚举、时序,不得退化为标题短语。
|
|
29
|
+
- 推荐使用长度启发式审查:`description` 明显短于 `source_excerpt`(如 < 50%)时标记风险并复核。
|
|
30
|
+
- `title` 仅用于索引,禁止用 `title` 替代完整需求描述。
|
|
31
|
+
- `source_file` 必须指向 Agent 可读文本:优先 `requirements/md/<同名>.md`(convert 产物),或 `requirements/raw/` 下的 md/txt/代码文件;禁止引用 xlsx/pdf/docx 等难解析格式。
|
|
32
|
+
|
|
33
|
+
5. **覆盖率报告是 S40 必备产物**
|
|
27
34
|
- 必须生成 `requirements/coverage-report.yaml`。
|
|
28
35
|
- 报告至少包含:
|
|
29
36
|
- `extraction_strategy`
|
|
@@ -41,3 +48,5 @@
|
|
|
41
48
|
- 禁止跳过源清点(inventory)直接写 `requirements/yaml`。
|
|
42
49
|
- 禁止仅凭少量摘要条目宣称「提取完成」。
|
|
43
50
|
- 禁止在未说明依据的情况下选择提取策略。
|
|
51
|
+
- 禁止 `source_excerpt` 留空或使用占位语。
|
|
52
|
+
- 禁止将多行表格仅提取第一列后声称完成条目化。
|