coding-agent-harness 1.0.1 → 1.0.2
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/CHANGELOG.md +19 -0
- package/README.en-US.md +14 -0
- package/README.md +111 -86
- package/README.zh-CN.md +270 -0
- package/SKILL.md +116 -189
- package/docs-release/README.md +72 -5
- package/docs-release/architecture/overview.md +286 -28
- package/docs-release/architecture/overview.zh-CN.md +288 -0
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/docs-release/assets/harness-architecture.svg +163 -0
- package/docs-release/assets/harness-workflow.svg +64 -0
- package/docs-release/guides/agent-installation.en-US.md +214 -0
- package/docs-release/guides/agent-installation.md +123 -26
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
- package/docs-release/guides/document-audience-and-surfaces.md +112 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
- package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
- package/docs-release/guides/migration-playbook.en-US.md +325 -0
- package/docs-release/guides/migration-playbook.md +329 -0
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
- package/docs-release/guides/parent-control-repository-pattern.md +252 -0
- package/docs-release/guides/repository-operating-models.en-US.md +196 -0
- package/docs-release/guides/repository-operating-models.md +196 -0
- package/docs-release/intl/README.md +15 -0
- package/docs-release/intl/de-DE.md +18 -0
- package/docs-release/intl/en-US.md +18 -0
- package/docs-release/intl/es-ES.md +18 -0
- package/docs-release/intl/fr-FR.md +18 -0
- package/docs-release/intl/ja-JP.md +18 -0
- package/docs-release/intl/ko-KR.md +18 -0
- package/docs-release/intl/zh-CN.md +18 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
- package/package.json +3 -1
- package/references/agents-md-pattern.md +3 -3
- package/references/docs-directory-standard.md +47 -3
- package/references/external-source-intake-standard.md +75 -0
- package/references/harness-ledger.md +5 -3
- package/references/legacy-12-phase-bootstrap.md +41 -0
- package/references/lessons-governance.md +23 -6
- package/references/planning-loop.md +41 -3
- package/references/project-onboarding-audit.md +10 -0
- package/references/repo-governance-standard.md +2 -0
- package/references/testing-standard.md +50 -0
- package/references/walkthrough-closeout.md +6 -5
- package/scripts/check-harness.mjs +76 -35
- package/scripts/harness.mjs +303 -12
- package/scripts/lib/capability-registry.mjs +533 -0
- package/scripts/lib/check-profiles.mjs +510 -0
- package/scripts/lib/core-shared.mjs +186 -0
- package/scripts/lib/dashboard-data.mjs +389 -0
- package/scripts/lib/dashboard-workbench.mjs +217 -0
- package/scripts/lib/dashboard-writer.mjs +93 -2
- package/scripts/lib/harness-core.mjs +10 -1318
- package/scripts/lib/lesson-maintenance.mjs +145 -0
- package/scripts/lib/markdown-utils.mjs +158 -0
- package/scripts/lib/migration-planner.mjs +478 -0
- package/scripts/lib/migration-support.mjs +312 -0
- package/scripts/lib/task-lifecycle.mjs +755 -0
- package/scripts/lib/task-scanner.mjs +682 -0
- package/scripts/smoke-dashboard.mjs +22 -0
- package/scripts/test-harness.mjs +926 -14
- package/templates/AGENTS.md.template +41 -30
- package/templates/architecture/Architecture-SSoT.md +21 -0
- package/templates/architecture/README.md +49 -0
- package/templates/architecture/critical-flows.md +22 -0
- package/templates/architecture/local-repo-context.md +20 -0
- package/templates/architecture/service-catalog.md +17 -0
- package/templates/architecture/services/service-template.md +31 -0
- package/templates/architecture/system-map.md +22 -0
- package/templates/dashboard/assets/app-src/00-state.js +41 -0
- package/templates/dashboard/assets/app-src/10-router.js +76 -0
- package/templates/dashboard/assets/app-src/20-overview.js +235 -0
- package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
- package/templates/dashboard/assets/app-src/40-modules.js +58 -0
- package/templates/dashboard/assets/app-src/45-review.js +128 -0
- package/templates/dashboard/assets/app-src/50-migration.js +169 -0
- package/templates/dashboard/assets/app-src/60-shared.js +61 -0
- package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
- package/templates/dashboard/assets/app.css +2575 -310
- package/templates/dashboard/assets/app.js +1498 -307
- package/templates/dashboard/assets/app.manifest.json +11 -0
- package/templates/dashboard/assets/i18n.js +429 -44
- package/templates/dashboard/assets/mermaid-renderer.js +58 -8
- package/templates/development/README.md +52 -0
- package/templates/development/codebase-map.md +11 -0
- package/templates/development/cross-repo-debugging.md +18 -0
- package/templates/development/external-context/service-template.md +33 -0
- package/templates/development/external-source-packs/README.md +24 -0
- package/templates/development/external-source-packs/digest-template.md +28 -0
- package/templates/development/local-setup.md +16 -0
- package/templates/development/stubs-and-mocks.md +11 -0
- package/templates/integrations/README.md +40 -0
- package/templates/integrations/api-contract.md +42 -0
- package/templates/integrations/event-contract.md +46 -0
- package/templates/integrations/third-party/vendor-template.md +42 -0
- package/templates/integrations/webhook-contract.md +41 -0
- package/templates/planning/brief.md +32 -0
- package/templates/planning/lesson_candidates.md +58 -0
- package/templates/planning/long-running-task-contract.md +7 -0
- package/templates/planning/module_brief.md +25 -0
- package/templates/planning/module_session_prompt.md +6 -0
- package/templates/planning/task_plan.md +7 -5
- package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
- package/templates/reference/docs-library-standard.md +31 -0
- package/templates/reference/execution-workflow-standard.md +4 -2
- package/templates/reference/external-source-intake-standard.md +82 -0
- package/templates/reference/harness-ledger-standard.md +1 -0
- package/templates/reference/repo-governance-standard.md +6 -4
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +69 -70
- package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
- package/templates-zh-CN/architecture/README.md +51 -0
- package/templates-zh-CN/architecture/critical-flows.md +24 -0
- package/templates-zh-CN/architecture/local-repo-context.md +20 -0
- package/templates-zh-CN/architecture/service-catalog.md +17 -0
- package/templates-zh-CN/architecture/services/service-template.md +31 -0
- package/templates-zh-CN/architecture/system-map.md +22 -0
- package/templates-zh-CN/development/README.md +54 -0
- package/templates-zh-CN/development/codebase-map.md +11 -0
- package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
- package/templates-zh-CN/development/external-context/service-template.md +33 -0
- package/templates-zh-CN/development/external-source-packs/README.md +24 -0
- package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
- package/templates-zh-CN/development/local-setup.md +16 -0
- package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
- package/templates-zh-CN/integrations/README.md +42 -0
- package/templates-zh-CN/integrations/api-contract.md +42 -0
- package/templates-zh-CN/integrations/event-contract.md +46 -0
- package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
- package/templates-zh-CN/integrations/webhook-contract.md +41 -0
- package/templates-zh-CN/planning/brief.md +32 -0
- package/templates-zh-CN/planning/lesson_candidates.md +58 -0
- package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
- package/templates-zh-CN/planning/module_brief.md +25 -0
- package/templates-zh-CN/planning/module_plan.md +2 -2
- package/templates-zh-CN/planning/module_session_prompt.md +4 -3
- package/templates-zh-CN/planning/task_plan.md +10 -4
- package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
- package/templates-zh-CN/reference/docs-library-standard.md +35 -0
- package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
- package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/dashboard/assets/app.css +0 -399
- package/templates-zh-CN/dashboard/assets/app.js +0 -435
- package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
- package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
- package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
- package/templates-zh-CN/dashboard/index.html +0 -18
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
datePrefix,
|
|
5
|
+
lessonCandidatesFile,
|
|
6
|
+
normalizeTarget,
|
|
7
|
+
readFileSafe,
|
|
8
|
+
slug,
|
|
9
|
+
toPosix,
|
|
10
|
+
} from "./core-shared.mjs";
|
|
11
|
+
import {
|
|
12
|
+
collectTasks,
|
|
13
|
+
parseLessonCandidateStatus,
|
|
14
|
+
} from "./task-scanner.mjs";
|
|
15
|
+
|
|
16
|
+
export function promoteLessonCandidate(targetInput, taskId, candidateId, { dryRun = false } = {}) {
|
|
17
|
+
const target = normalizeTarget(targetInput);
|
|
18
|
+
const normalizedRef = slug(taskId);
|
|
19
|
+
const matchesBareSlug = (item) => {
|
|
20
|
+
if (!datePrefix.test(normalizedRef)) {
|
|
21
|
+
const shortBase = datePrefix.test(item.shortId) ? item.shortId.replace(datePrefix, "") : item.shortId;
|
|
22
|
+
if (shortBase === normalizedRef) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
const candidates = collectTasks(target).filter((item) => item.id === taskId || item.shortId === taskId || item.id.endsWith(`/${taskId}`) || matchesBareSlug(item));
|
|
27
|
+
if (candidates.length > 1) {
|
|
28
|
+
const options = candidates.map((item) => `- ${item.id}`).join("\n");
|
|
29
|
+
throw new Error(`Ambiguous task reference: ${taskId}\n${options}`);
|
|
30
|
+
}
|
|
31
|
+
const task = candidates[0];
|
|
32
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
33
|
+
if (!candidateId) throw new Error("Missing lesson candidate id");
|
|
34
|
+
const taskDir = path.join(target.projectRoot, task.path.replace(/^TARGET:/, ""));
|
|
35
|
+
const candidatePath = path.join(taskDir, lessonCandidatesFile);
|
|
36
|
+
const candidateContent = readFileSafe(candidatePath);
|
|
37
|
+
const parsed = parseLessonCandidateStatus(candidateContent);
|
|
38
|
+
const row = parsed.rows.find((item) => item.id.toLowerCase() === candidateId.toLowerCase());
|
|
39
|
+
if (!row) throw new Error(`Lesson candidate not found: ${candidateId}`);
|
|
40
|
+
if (!["needs-promotion", "promoted"].includes(row.status)) {
|
|
41
|
+
throw new Error(`Lesson candidate must be needs-promotion before promotion; current status is ${row.status}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lessonId = lessonIdFromCandidate(row.id);
|
|
45
|
+
const title = row.title || lessonId;
|
|
46
|
+
const detailRelative = `docs/01-GOVERNANCE/lessons/${lessonId}-${slug(title)}.md`;
|
|
47
|
+
const detailPath = path.join(target.projectRoot, detailRelative);
|
|
48
|
+
const ssotPath = path.join(target.docsRoot, "01-GOVERNANCE/Lessons-SSoT.md");
|
|
49
|
+
const ssotContent = readFileSafe(ssotPath);
|
|
50
|
+
if (!ssotContent.trim()) throw new Error("Lessons SSoT not found");
|
|
51
|
+
|
|
52
|
+
const changes = [];
|
|
53
|
+
if (!fs.existsSync(detailPath)) changes.push({ action: dryRun ? "would-create" : "create", path: `TARGET:${detailRelative}` });
|
|
54
|
+
if (!ssotContent.includes(lessonId)) changes.push({ action: dryRun ? "would-append" : "append", path: "TARGET:docs/01-GOVERNANCE/Lessons-SSoT.md" });
|
|
55
|
+
if (row.status !== "promoted" || parsed.status !== "promoted") changes.push({ action: dryRun ? "would-update" : "update", path: task.lessonCandidatePath || `TARGET:${toPosix(path.relative(target.projectRoot, candidatePath))}` });
|
|
56
|
+
|
|
57
|
+
if (dryRun) return { dryRun: true, taskId: task.id, candidateId: row.id, lessonId, detailDoc: `TARGET:${detailRelative}`, changes };
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(path.dirname(detailPath), { recursive: true });
|
|
60
|
+
if (!fs.existsSync(detailPath)) fs.writeFileSync(detailPath, renderLessonDetail({ lessonId, candidate: row, task, detailRelative }));
|
|
61
|
+
if (!ssotContent.includes(lessonId)) fs.writeFileSync(ssotPath, appendLessonSsotRow(ssotContent, { lessonId, candidate: row, task, detailRelative }));
|
|
62
|
+
fs.writeFileSync(candidatePath, markCandidatePromoted(candidateContent, row.id, lessonId));
|
|
63
|
+
|
|
64
|
+
return { dryRun: false, taskId: task.id, candidateId: row.id, lessonId, detailDoc: `TARGET:${detailRelative}`, changes };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function lessonIdFromCandidate(candidateId) {
|
|
68
|
+
const match = String(candidateId || "").match(/^LC-(\d{4})(\d{2})(\d{2})-(\d+)$/i);
|
|
69
|
+
if (!match) return `L-${slug(candidateId)}`;
|
|
70
|
+
return `L-${match[1]}-${match[2]}-${match[3]}-${match[4].padStart(3, "0")}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderLessonDetail({ lessonId, candidate, task }) {
|
|
74
|
+
return [
|
|
75
|
+
`# ${lessonId} - ${candidate.title || "Lesson Candidate"}`,
|
|
76
|
+
"",
|
|
77
|
+
"## Source",
|
|
78
|
+
"",
|
|
79
|
+
`- Task: ${task.id}`,
|
|
80
|
+
`- Candidate: ${candidate.id}`,
|
|
81
|
+
`- Promotion target: ${candidate.promotionTarget || "not specified"}`,
|
|
82
|
+
"",
|
|
83
|
+
"## Summary",
|
|
84
|
+
"",
|
|
85
|
+
candidate.title || "Promoted lesson candidate.",
|
|
86
|
+
"",
|
|
87
|
+
"## Why It Matters",
|
|
88
|
+
"",
|
|
89
|
+
candidate.reviewDecision || "Human review marked this candidate for governance promotion.",
|
|
90
|
+
"",
|
|
91
|
+
"## Status",
|
|
92
|
+
"",
|
|
93
|
+
"- State: pending governance integration",
|
|
94
|
+
"",
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function appendLessonSsotRow(content, { lessonId, candidate, task, detailRelative }) {
|
|
99
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
100
|
+
const headerIndex = lines.findIndex((line) => /^\|\s*(ID|Lesson ID)\s*\|/.test(line));
|
|
101
|
+
if (headerIndex < 0) throw new Error("Lessons SSoT active table not found");
|
|
102
|
+
const columnCount = splitSimpleRow(lines[headerIndex]).length;
|
|
103
|
+
const date = lessonId.match(/^L-(\d{4}-\d{2}-\d{2})-/)?.[1] || new Date().toISOString().slice(0, 10);
|
|
104
|
+
const detail = `\`${detailRelative}\``;
|
|
105
|
+
const source = `\`${task.path.replace(/^TARGET:/, "docs/").replace(/^docs\/docs\//, "docs/")}/task_plan.md\``;
|
|
106
|
+
const row =
|
|
107
|
+
columnCount === 10
|
|
108
|
+
? `| ${lessonId} | ${escapeCell(candidate.title || lessonId)} | ${source} | process-change | coordinator | candidate | ${escapeCell(candidate.promotionTarget || "governance review")} | ${detail} | ${escapeCell(candidate.id)} | ${date} |`
|
|
109
|
+
: `| ${lessonId} | ${date} | ${source} | process-change | ${escapeCell(candidate.promotionTarget || "governance review")} | ${escapeCell(candidate.title || lessonId)} | ${detail} | pending | ${escapeCell(candidate.id)} |`;
|
|
110
|
+
let insertAt = headerIndex + 1;
|
|
111
|
+
while (insertAt < lines.length && lines[insertAt].trim().startsWith("|")) insertAt += 1;
|
|
112
|
+
lines.splice(insertAt, 0, row);
|
|
113
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function markCandidatePromoted(content, candidateId, lessonId) {
|
|
117
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
118
|
+
const headerIndex = lines.findIndex((line) => /^\|\s*ID\s*\|/.test(line));
|
|
119
|
+
if (headerIndex >= 0) {
|
|
120
|
+
const header = splitSimpleRow(lines[headerIndex]);
|
|
121
|
+
const statusIndex = header.findIndex((cell) => /^(Row Status|行状态|Status|状态)$/i.test(cell));
|
|
122
|
+
const decisionIndex = header.findIndex((cell) => /^(Review Decision|审查决定)$/i.test(cell));
|
|
123
|
+
for (let index = headerIndex + 2; index < lines.length && lines[index].trim().startsWith("|"); index += 1) {
|
|
124
|
+
const cells = splitSimpleRow(lines[index]);
|
|
125
|
+
if ((cells[0] || "").toLowerCase() !== candidateId.toLowerCase()) continue;
|
|
126
|
+
if (statusIndex >= 0) cells[statusIndex] = "promoted";
|
|
127
|
+
if (decisionIndex >= 0 && !cells[decisionIndex].includes(lessonId)) cells[decisionIndex] = `${cells[decisionIndex]} promoted:${lessonId}`.trim();
|
|
128
|
+
lines[index] = `| ${cells.map(escapeCell).join(" | ")} |`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return `${lines.join("\n")
|
|
132
|
+
.replace("| Task-level status | needs-promotion |", "| Task-level status | promoted |")
|
|
133
|
+
.replace("| Promotion state | not-promoted |", "| Promotion state | promoted |")
|
|
134
|
+
.replace("| Closeout token | pending |", `| Closeout token | checked-created:${lessonId} |`)
|
|
135
|
+
.replace(/\| Closeout token \| queued-promotion:[^|]+ \|/, `| Closeout token | checked-created:${lessonId} |`)
|
|
136
|
+
.trimEnd()}\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function splitSimpleRow(line) {
|
|
140
|
+
return String(line || "").replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function escapeCell(value) {
|
|
144
|
+
return String(value || "").replace(/\r?\n/g, " ").replaceAll("|", "\\|").trim();
|
|
145
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { sanitizeText, slug } from "./core-shared.mjs";
|
|
2
|
+
|
|
3
|
+
export function markdownTableRows(content) {
|
|
4
|
+
return content
|
|
5
|
+
.split(/\r?\n/)
|
|
6
|
+
.filter((line) => line.trim().startsWith("|"))
|
|
7
|
+
.map(splitMarkdownRow);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export function parseAllMarkdownTables(content, source, kindPrefix) {
|
|
12
|
+
const lines = content.split(/\r?\n/);
|
|
13
|
+
const tables = [];
|
|
14
|
+
let index = 0;
|
|
15
|
+
let tableIndex = 1;
|
|
16
|
+
while (index < lines.length) {
|
|
17
|
+
if (!lines[index].trim().startsWith("|")) {
|
|
18
|
+
index += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const start = index;
|
|
22
|
+
const block = [];
|
|
23
|
+
while (index < lines.length && lines[index].trim().startsWith("|")) {
|
|
24
|
+
block.push(lines[index]);
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
if (block.length < 2) continue;
|
|
28
|
+
const rows = block.map(splitMarkdownRow);
|
|
29
|
+
const separator = rows[1] || [];
|
|
30
|
+
if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
|
31
|
+
const columns = rows[0];
|
|
32
|
+
const dataRows = rows.slice(2).filter((row) => row.length === columns.length);
|
|
33
|
+
tables.push({
|
|
34
|
+
id: `${slug(kindPrefix)}-${String(tableIndex).padStart(2, "0")}`,
|
|
35
|
+
kind: kindPrefix,
|
|
36
|
+
source,
|
|
37
|
+
line: start + 1,
|
|
38
|
+
columns,
|
|
39
|
+
rows: dataRows.map((row, rowIndex) => ({
|
|
40
|
+
id: `${slug(kindPrefix)}-${String(tableIndex).padStart(2, "0")}-row-${String(rowIndex + 1).padStart(3, "0")}`,
|
|
41
|
+
cells: Object.fromEntries(columns.map((column, columnIndex) => [column, sanitizeText(row[columnIndex] || "")])),
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
tableIndex += 1;
|
|
45
|
+
}
|
|
46
|
+
return tables;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function splitMarkdownRow(line) {
|
|
50
|
+
let text = String(line || "").trim();
|
|
51
|
+
if (text.startsWith("|")) text = text.slice(1);
|
|
52
|
+
if (text.endsWith("|") && !text.endsWith("\\|")) text = text.slice(0, -1);
|
|
53
|
+
const cells = [];
|
|
54
|
+
let current = "";
|
|
55
|
+
let inCode = false;
|
|
56
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
57
|
+
const char = text[index];
|
|
58
|
+
if (char === "\\" && text[index + 1] === "|") {
|
|
59
|
+
current += "|";
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (char === "`") inCode = !inCode;
|
|
64
|
+
if (char === "|" && !inCode) {
|
|
65
|
+
cells.push(current.trim());
|
|
66
|
+
current = "";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
current += char;
|
|
70
|
+
}
|
|
71
|
+
cells.push(current.trim());
|
|
72
|
+
return cells;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function tableAfterHeading(content, headerPattern) {
|
|
76
|
+
const rows = markdownTableRows(content);
|
|
77
|
+
const headerIndex = rows.findIndex((cells) => cells.some((cell) => headerPattern.test(cell)));
|
|
78
|
+
if (headerIndex < 0) return { header: [], rows: [] };
|
|
79
|
+
const header = rows[headerIndex];
|
|
80
|
+
const body = [];
|
|
81
|
+
for (const row of rows.slice(headerIndex + 1)) {
|
|
82
|
+
if (row.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
|
83
|
+
if (row.length !== header.length) break;
|
|
84
|
+
body.push(row);
|
|
85
|
+
}
|
|
86
|
+
return { header, rows: body };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getColumn(header, name) {
|
|
90
|
+
return header.findIndex((cell) => cell.toLowerCase() === name.toLowerCase());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getColumnAny(header, names) {
|
|
94
|
+
return header.findIndex((cell) => names.some((name) => cell.toLowerCase() === name.toLowerCase()));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function contentHasAny(content, terms) {
|
|
98
|
+
return terms.some((term) => (term instanceof RegExp ? term.test(content) : content.includes(term)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getCell(cells, names, fallback = "") {
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
if (cells[name] !== undefined) return cells[name];
|
|
104
|
+
}
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function splitList(value) {
|
|
109
|
+
return String(value || "")
|
|
110
|
+
.split(/[,+;]/)
|
|
111
|
+
.map((item) => item.trim())
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.filter((item) => item.toLowerCase() !== "none");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function splitDependencies(value) {
|
|
117
|
+
return String(value || "")
|
|
118
|
+
.split(/\s*(?:,|;|\+|&|\/|\band\b|\bAND\b)\s*/)
|
|
119
|
+
.map((item) => item.trim())
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.filter((item) => !/^(none|n\/a|na|-|—|–|无)$/i.test(item))
|
|
122
|
+
.filter((item) => !/^same\b/i.test(item));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function firstColumn(header, names) {
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
const index = getColumn(header, name);
|
|
128
|
+
if (index >= 0) return index;
|
|
129
|
+
}
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function updateMarkdownTableRow(content, headerPattern, updater) {
|
|
134
|
+
const lines = content.split(/\r?\n/);
|
|
135
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
136
|
+
if (!lines[index].trim().startsWith("|")) continue;
|
|
137
|
+
const header = splitMarkdownRow(lines[index]);
|
|
138
|
+
if (!header.some((cell) => headerPattern.test(cell))) continue;
|
|
139
|
+
let matched = false;
|
|
140
|
+
let rowIndex = index + 2;
|
|
141
|
+
while (rowIndex < lines.length && lines[rowIndex].trim().startsWith("|")) {
|
|
142
|
+
const row = splitMarkdownRow(lines[rowIndex]);
|
|
143
|
+
if (row.length === header.length && !row.every((cell) => /^:?-{3,}:?$/.test(cell))) {
|
|
144
|
+
const next = updater(header, row);
|
|
145
|
+
if (!next) {
|
|
146
|
+
rowIndex += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
matched = true;
|
|
150
|
+
if (next.join("\u0000") !== row.join("\u0000")) matched = true;
|
|
151
|
+
lines[rowIndex] = `| ${next.join(" | ")} |`;
|
|
152
|
+
}
|
|
153
|
+
rowIndex += 1;
|
|
154
|
+
}
|
|
155
|
+
return { content: lines.join("\n"), matched };
|
|
156
|
+
}
|
|
157
|
+
return { content, matched: false };
|
|
158
|
+
}
|