@chenguangyao/devflow-kit 0.1.43
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 +232 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/bin/devflow.js +9 -0
- package/docs/RFC-001-devflow-kit.md +617 -0
- package/docs/RFC-002-workflow-kernel.md +134 -0
- package/docs/enterprise-integration-supplement.md +274 -0
- package/docs/internal-gitlab-setup.md +426 -0
- package/docs/marketplace-skills.md +231 -0
- package/docs/migration-from-arb.md +232 -0
- package/docs/tooling-overview.md +774 -0
- package/docs/workflow-orchestration.md +695 -0
- package/docs/workflow-ui-prototype.html +271 -0
- package/package.json +52 -0
- package/schemas/config.schema.json +51 -0
- package/schemas/delta.schema.json +22 -0
- package/schemas/state.schema.json +130 -0
- package/schemas/status-surface.schema.json +197 -0
- package/schemas/workflow-confirmation-surface.schema.json +70 -0
- package/schemas/workflow-picker.schema.json +94 -0
- package/scripts/postinstall.js +101 -0
- package/scripts/render-workflow-ui-prototype.js +271 -0
- package/skills/apply/SKILL.md +313 -0
- package/skills/apply/references/discipline-checklist.md +145 -0
- package/skills/apply/references/subagent-implementer-prompt.md +113 -0
- package/skills/apply/references/subagent-orchestration.md +150 -0
- package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
- package/skills/apply/references/tdd-loop.md +287 -0
- package/skills/apply/references/when-plan-is-wrong.md +279 -0
- package/skills/apply/references/worktree-swarm.md +292 -0
- package/skills/archive/SKILL.md +229 -0
- package/skills/archive/references/conflict-resolution.md +336 -0
- package/skills/archive/references/knowledge-deposit.md +381 -0
- package/skills/archive/references/spec-merge.md +365 -0
- package/skills/brainstorm/SKILL.md +123 -0
- package/skills/brainstorm/references/proposal-template.md +244 -0
- package/skills/brainstorm/references/question-catalog.md +168 -0
- package/skills/brainstorm/references/session-template.md +184 -0
- package/skills/ci-fix/SKILL.md +63 -0
- package/skills/ci-fix/references/loop.md +25 -0
- package/skills/code-review/SKILL.md +279 -0
- package/skills/code-review/references/escalation-playbook.md +192 -0
- package/skills/code-review/references/language-cheatsheets/go.md +175 -0
- package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
- package/skills/code-review/references/language-cheatsheets/python.md +170 -0
- package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
- package/skills/code-review/references/output-template.md +275 -0
- package/skills/code-review/references/review-checklist.md +251 -0
- package/skills/complexity-grading/SKILL.md +259 -0
- package/skills/deliver/SKILL.md +271 -0
- package/skills/deliver/references/delivery-modes.md +299 -0
- package/skills/deliver/references/notify.md +359 -0
- package/skills/deliver/references/pr-description.md +319 -0
- package/skills/dependency-upgrade/SKILL.md +57 -0
- package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
- package/skills/df-orchestrator/SKILL.md +407 -0
- package/skills/df-orchestrator/references/complexity-grading.md +177 -0
- package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
- package/skills/df-orchestrator/references/routing-rules.md +290 -0
- package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
- package/skills/frontend-quality/SKILL.md +61 -0
- package/skills/frontend-quality/references/checklist.md +35 -0
- package/skills/handoff-resume/SKILL.md +59 -0
- package/skills/handoff-resume/references/handoff-template.md +54 -0
- package/skills/plan/SKILL.md +166 -0
- package/skills/plan/references/task-breakdown.md +207 -0
- package/skills/plan/references/task-sequencing.md +143 -0
- package/skills/plan/references/task-template.md +248 -0
- package/skills/requirement-analysis/SKILL.md +499 -0
- package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
- package/skills/requirement-analysis/references/code-recon.md +151 -0
- package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
- package/skills/requirement-analysis/references/requirement-template.md +339 -0
- package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
- package/skills/security-hardening/SKILL.md +60 -0
- package/skills/security-hardening/references/checklist.md +42 -0
- package/skills/tech-spec/SKILL.md +388 -0
- package/skills/tech-spec/references/api-contract-design.md +172 -0
- package/skills/tech-spec/references/decision-records.md +110 -0
- package/skills/tech-spec/references/design-template.md +301 -0
- package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
- package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
- package/skills/tech-spec/references/transaction-patterns.md +212 -0
- package/skills/test-spec/SKILL.md +219 -0
- package/skills/test-spec/references/coverage-strategy.md +218 -0
- package/skills/test-spec/references/edge-case-to-test.md +143 -0
- package/skills/test-spec/references/test-case-template.md +276 -0
- package/skills/verify/SKILL.md +232 -0
- package/skills/verify/references/nfr-verification.md +292 -0
- package/skills/verify/references/report-templates.md +510 -0
- package/skills/verify/references/self-test-guide.md +240 -0
- package/skills/verify/references/verify-rollback-map.md +247 -0
- package/src/cli/commands/_helpers.js +108 -0
- package/src/cli/commands/_submit.js +718 -0
- package/src/cli/commands/apply.js +198 -0
- package/src/cli/commands/archive.js +180 -0
- package/src/cli/commands/checkpoint.js +113 -0
- package/src/cli/commands/deliver.js +377 -0
- package/src/cli/commands/deploy.js +504 -0
- package/src/cli/commands/design.js +158 -0
- package/src/cli/commands/disable.js +21 -0
- package/src/cli/commands/doctor.js +178 -0
- package/src/cli/commands/enable.js +21 -0
- package/src/cli/commands/flow.js +645 -0
- package/src/cli/commands/help.js +93 -0
- package/src/cli/commands/ingest.js +602 -0
- package/src/cli/commands/init.js +341 -0
- package/src/cli/commands/knowledge.js +523 -0
- package/src/cli/commands/logs.js +43 -0
- package/src/cli/commands/new.js +202 -0
- package/src/cli/commands/plan.js +49 -0
- package/src/cli/commands/propose.js +27 -0
- package/src/cli/commands/provider.js +698 -0
- package/src/cli/commands/report.js +143 -0
- package/src/cli/commands/requirement.js +227 -0
- package/src/cli/commands/review.js +301 -0
- package/src/cli/commands/skills.js +457 -0
- package/src/cli/commands/status.js +925 -0
- package/src/cli/commands/switch.js +27 -0
- package/src/cli/commands/sync.js +47 -0
- package/src/cli/commands/test.js +366 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/update.js +74 -0
- package/src/cli/commands/verify.js +354 -0
- package/src/cli/commands/worktree.js +78 -0
- package/src/cli/index.js +72 -0
- package/src/cli/parse-args.js +102 -0
- package/src/core/autodetect.js +271 -0
- package/src/core/change.js +208 -0
- package/src/core/checkpoint.js +217 -0
- package/src/core/config.js +60 -0
- package/src/core/delta.js +290 -0
- package/src/core/markers.js +59 -0
- package/src/core/paths.js +173 -0
- package/src/core/plan-tasks.js +36 -0
- package/src/core/project-routing.js +285 -0
- package/src/core/projects.js +200 -0
- package/src/core/state.js +200 -0
- package/src/core/workflow-check.js +177 -0
- package/src/core/workflow-init.js +34 -0
- package/src/core/workflow-picker.js +154 -0
- package/src/core/workflow-policy.js +119 -0
- package/src/core/workflow-suggest.js +181 -0
- package/src/core/workflow-verify.js +88 -0
- package/src/core/workflow.js +433 -0
- package/src/core/worktree.js +241 -0
- package/src/knowledge/categories.js +107 -0
- package/src/knowledge/classify.js +125 -0
- package/src/knowledge/deposit.js +414 -0
- package/src/knowledge/migrate.js +149 -0
- package/src/knowledge/mr.js +219 -0
- package/src/knowledge/query.js +131 -0
- package/src/knowledge/registry.js +151 -0
- package/src/knowledge/sync.js +179 -0
- package/src/providers/base.js +74 -0
- package/src/providers/drivers/api-yapi.js +78 -0
- package/src/providers/drivers/ci-jenkins.js +109 -0
- package/src/providers/drivers/intake-confluence.js +544 -0
- package/src/providers/drivers/kb-git.js +549 -0
- package/src/providers/drivers/kb-weknora.js +472 -0
- package/src/providers/drivers/notify-smtp.js +515 -0
- package/src/providers/drivers/observability-oss.js +43 -0
- package/src/providers/drivers/observability-sls.js +50 -0
- package/src/providers/lifecycle.js +135 -0
- package/src/providers/loader.js +132 -0
- package/src/providers/local.js +190 -0
- package/src/providers/userconfig.js +283 -0
- package/src/reports/aggregate.js +185 -0
- package/src/reports/coverage.js +163 -0
- package/src/reports/detect.js +143 -0
- package/src/reports/parse.js +236 -0
- package/src/templates/files/ci/github.yml +38 -0
- package/src/templates/files/ci/gitlab.yml +27 -0
- package/src/templates/files/design.md +63 -0
- package/src/templates/files/ide/devflow-workflow.md +58 -0
- package/src/templates/files/ide/project-overview-reference.md +1 -0
- package/src/templates/files/ide/project-overview.md +27 -0
- package/src/templates/files/knowledge-index.json +17 -0
- package/src/templates/files/knowledge.md +28 -0
- package/src/templates/files/meta.json +8 -0
- package/src/templates/files/plan.md +38 -0
- package/src/templates/files/proposal.md +33 -0
- package/src/templates/files/reports/contract-test.md +40 -0
- package/src/templates/files/reports/e2e-test.md +30 -0
- package/src/templates/files/reports/integration-test.md +36 -0
- package/src/templates/files/reports/joint-test.md +58 -0
- package/src/templates/files/reports/perf.md +24 -0
- package/src/templates/files/reports/regression.md +20 -0
- package/src/templates/files/reports/remote-test.md +55 -0
- package/src/templates/files/reports/self-test.md +43 -0
- package/src/templates/files/reports/smoke-test.md +22 -0
- package/src/templates/files/reports/unit-test.md +36 -0
- package/src/templates/files/requirement.md +51 -0
- package/src/templates/files/review.md +38 -0
- package/src/templates/files/tests.md +36 -0
- package/src/templates/files/verify.md +32 -0
- package/src/templates/index.js +21 -0
- package/src/utils/log.js +37 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const change = require('../../core/change.js');
|
|
8
|
+
const aggregate = require('../../reports/aggregate.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Submit-doc rendering, subject composition, attachment collection and
|
|
12
|
+
* human-confirmation gate for `devflow deliver --notify`.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors arb-workflow-kit deploy-submit Step 3-5 conventions:
|
|
15
|
+
* 1. Generate a single ๆๆตๅ.md as the email body
|
|
16
|
+
* 2. Generate one consolidated reports/test-report.md as the default test attachment
|
|
17
|
+
* 3. Step 4: human confirmation before send (subject + recipients + attachments)
|
|
18
|
+
*
|
|
19
|
+
* No external deps โ uses fs / child_process / readline only.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
23
|
+
// Subject
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compose email subject. Mirrors arb's `ใๆๆตใ{้ๆฑ็ผๅท} {้ๆฑๅ็งฐ}` format,
|
|
27
|
+
* augmented with optional project + environment for multi-project mailboxes.
|
|
28
|
+
*
|
|
29
|
+
* full: `ใๆๆตใ[<project>][<env>] <title>` (when project + env present)
|
|
30
|
+
* short: `ใๆๆตใ <title>` (no project/env)
|
|
31
|
+
*/
|
|
32
|
+
function composeSubject({ st, project, env, prefix }) {
|
|
33
|
+
const tag = prefix || 'ใๆๆตใ';
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (project) parts.push(`[${project}]`);
|
|
36
|
+
if (env) parts.push(`[${env}]`);
|
|
37
|
+
const title = (st && st.title) || (st && st.slug) || 'change';
|
|
38
|
+
return `${tag}${parts.join('')} ${title}`.replace(/\s+/g, ' ').trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
42
|
+
// Submit doc body
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render the ๆๆตๅ markdown body. Returns a string (caller writes it).
|
|
46
|
+
*
|
|
47
|
+
* Sections:
|
|
48
|
+
* 1. ๅบๆฌไฟกๆฏ (table)
|
|
49
|
+
* 2. ๅๆดๅ
ๅฎน (extracted from design.md head section, first ## block)
|
|
50
|
+
* 3. ๅบ่กจๅๆด (extracted from design.md DDL/data section and ddl.sql)
|
|
51
|
+
* 4. ๆต่ฏ้็น (extracted from design.md '## *้ฃ้ฉ|ๆต่ฏ' section if any)
|
|
52
|
+
* 5. ่ชๆตๆ
ๅต (table built from reports/*.md frontmatter)
|
|
53
|
+
*/
|
|
54
|
+
async function renderSubmitDoc({ root, slug, st, env, base, head, build, detect, time }) {
|
|
55
|
+
const cdir = change.resolveChangeDir(root, slug);
|
|
56
|
+
const designPath = path.join(cdir, 'design.md');
|
|
57
|
+
const ddlPath = path.join(cdir, 'ddl.sql');
|
|
58
|
+
const reportsDir = path.join(cdir, 'reports');
|
|
59
|
+
|
|
60
|
+
const project = (detect && detect.remote && detect.remote.project) || '';
|
|
61
|
+
const repoFull = (detect && detect.remote && detect.remote.fullPath) || '';
|
|
62
|
+
const ts = time || new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
63
|
+
const submitBranch = head || primaryProjectBranch(st) || '';
|
|
64
|
+
|
|
65
|
+
// 1) ๅบๆฌไฟกๆฏ่กจ
|
|
66
|
+
const infoRows = [
|
|
67
|
+
['้ๆฑ็ผๅท', slug],
|
|
68
|
+
['้ๆฑๅ็งฐ', st.title || slug],
|
|
69
|
+
['ๆๆตๆถ้ด', ts],
|
|
70
|
+
['ๆๅก', project || '(ๆช่ฏๅซ)'],
|
|
71
|
+
['ไปๅบ', repoFull || '(ๆช่ฏๅซ)'],
|
|
72
|
+
['็ฎๆ ๅๆฏ', base || '(่ชๅจๆฃๆต)'],
|
|
73
|
+
['ๆๆตๅๆฏ', submitBranch || '(ๆช่ฏๅซ)'],
|
|
74
|
+
['ๆๅปบๅท', build || '-'],
|
|
75
|
+
['ๆต่ฏ็ฏๅข', env || 'test'],
|
|
76
|
+
['ๅคๆๅบฆ', st.level || 'L2'],
|
|
77
|
+
];
|
|
78
|
+
const infoTable = renderTable(['้กน', 'ๅผ'], infoRows);
|
|
79
|
+
|
|
80
|
+
// 2) ๅๆดๅ
ๅฎน + 3) ๆต่ฏ้็น (best-effort extraction from design.md)
|
|
81
|
+
let changes = '_(design.md ไธๅญๅจ,ๅๆดๅ
ๅฎน็ฑๆฌๆฌก PR diff ่ชๆฅ)_';
|
|
82
|
+
let dbChanges = '_(ๆ ๆฐๆฎๅบ่กจ็ปๆๅๆด)_';
|
|
83
|
+
let risks = '_(ๆ ๆพๅผ้ฃ้ฉ่;่ฏท้็นๆ ธๅฏนๅๆดๅ
ๅฎนใๅบ่กจๅๆดๅ่ชๆต่ฆ็)_';
|
|
84
|
+
const ddlSql = fs.existsSync(ddlPath) ? await fsp.readFile(ddlPath, 'utf8') : '';
|
|
85
|
+
if (fs.existsSync(designPath)) {
|
|
86
|
+
const designMd = await fsp.readFile(designPath, 'utf8');
|
|
87
|
+
changes = extractDesignSummary(designMd);
|
|
88
|
+
dbChanges = extractDbChangeSummary({ designMd, ddlSql });
|
|
89
|
+
risks = extractRiskSection(designMd) || risks;
|
|
90
|
+
} else if (ddlSql.trim()) {
|
|
91
|
+
dbChanges = extractDbChangeSummary({ ddlSql });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4) ่ชๆตๆ
ๅต โ ๆซ reports/*.md frontmatter
|
|
95
|
+
const reports = await summariseReports({ reportsDir });
|
|
96
|
+
let testTable;
|
|
97
|
+
if (reports.length === 0) {
|
|
98
|
+
testTable = '_(ๅฐๆ ๆฅๅใ่ฟ่ก `devflow test unit` / `devflow report self-test` ๅๅๅๆๆต)_';
|
|
99
|
+
} else {
|
|
100
|
+
const rows = reports.map((r) => [
|
|
101
|
+
labelKind(r.kind),
|
|
102
|
+
statusIcon(r.status),
|
|
103
|
+
r.total != null ? String(r.total) : '-',
|
|
104
|
+
r.passed != null ? String(r.passed) : '-',
|
|
105
|
+
r.failed != null ? String(r.failed) : '-',
|
|
106
|
+
r.coverage || '-',
|
|
107
|
+
]);
|
|
108
|
+
testTable = renderTable(
|
|
109
|
+
['็ฑปๅ', '็ถๆ', '็จไพ', '้่ฟ', 'ๅคฑ่ดฅ', '่ฆ็็'],
|
|
110
|
+
rows
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
`# ใๆๆตใ${st.title || slug}`,
|
|
116
|
+
'',
|
|
117
|
+
'## ๅบๆฌไฟกๆฏ',
|
|
118
|
+
'',
|
|
119
|
+
infoTable,
|
|
120
|
+
'',
|
|
121
|
+
'## ๅๆดๅ
ๅฎน',
|
|
122
|
+
'',
|
|
123
|
+
changes,
|
|
124
|
+
'',
|
|
125
|
+
'## ๅบ่กจๅๆด',
|
|
126
|
+
'',
|
|
127
|
+
dbChanges,
|
|
128
|
+
'',
|
|
129
|
+
'## ๆต่ฏ้็น',
|
|
130
|
+
'',
|
|
131
|
+
risks,
|
|
132
|
+
'',
|
|
133
|
+
'## ่ชๆตๆ
ๅต',
|
|
134
|
+
'',
|
|
135
|
+
testTable,
|
|
136
|
+
'',
|
|
137
|
+
].join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render a consolidated test report attachment for submit mail.
|
|
142
|
+
*
|
|
143
|
+
* This intentionally replaces the old "attach every reports/*.md" behavior:
|
|
144
|
+
* QA gets one readable test document, while raw unit/integration/e2e/smoke
|
|
145
|
+
* files stay in the change folder unless --attach-reports is explicitly used.
|
|
146
|
+
*/
|
|
147
|
+
async function renderTestReportDoc({ root, slug, st, env, detect, time }) {
|
|
148
|
+
const cdir = change.resolveChangeDir(root, slug);
|
|
149
|
+
const reportsDir = path.join(cdir, 'reports');
|
|
150
|
+
const aggregateReport = aggregate.readAggregate(reportsDir);
|
|
151
|
+
if (aggregateReport.reports.length) {
|
|
152
|
+
return fs.readFileSync(path.join(reportsDir, 'test-report.md'), 'utf8');
|
|
153
|
+
}
|
|
154
|
+
const project = (detect && detect.remote && detect.remote.project) || '';
|
|
155
|
+
const ts = time || new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
156
|
+
const reports = await summariseReports({ reportsDir });
|
|
157
|
+
const status = overallReportStatus(reports);
|
|
158
|
+
const totals = sumReportTotals(reports);
|
|
159
|
+
|
|
160
|
+
const lines = [
|
|
161
|
+
`# ๆต่ฏๆฅๅ - ${st.title || slug}`,
|
|
162
|
+
'',
|
|
163
|
+
'## ไธใๆง่ก็ป่ฎบ',
|
|
164
|
+
'',
|
|
165
|
+
renderTable(['้กน', 'ๅผ'], [
|
|
166
|
+
['้ๆฑ็ผๅท', slug],
|
|
167
|
+
['้ๆฑๅ็งฐ', st.title || slug],
|
|
168
|
+
['ๆๅก', project || '(ๆช่ฏๅซ)'],
|
|
169
|
+
['ๆต่ฏ็ฏๅข', env || 'test'],
|
|
170
|
+
['ๆฅๅๆถ้ด', ts],
|
|
171
|
+
['ๆดไฝ็ป่ฎบ', statusIcon(status)],
|
|
172
|
+
['็จไพ็ป่ฎก', totals.total == null ? '-' : `ๆป่ฎก ${totals.total},้่ฟ ${totals.passed},ๅคฑ่ดฅ ${totals.failed}`],
|
|
173
|
+
]),
|
|
174
|
+
'',
|
|
175
|
+
'## ไบใๆต่ฏ่ฆ็ๆฑๆป',
|
|
176
|
+
'',
|
|
177
|
+
renderReportSummaryTable(reports),
|
|
178
|
+
'',
|
|
179
|
+
'## ไธใAPI / ้ๆๆต่ฏ',
|
|
180
|
+
'',
|
|
181
|
+
renderApiIntegrationSummary({ reportsDir, reports }),
|
|
182
|
+
'',
|
|
183
|
+
'## ๅใ่่ฐๆต่ฏ',
|
|
184
|
+
'',
|
|
185
|
+
renderJointTestSummary({ reportsDir, reports }),
|
|
186
|
+
'',
|
|
187
|
+
'## ไบใๅคฑ่ดฅไธ้็',
|
|
188
|
+
'',
|
|
189
|
+
renderFailureSummary({ reportsDir, reports }),
|
|
190
|
+
'',
|
|
191
|
+
'## ๅ
ญใๆๆต็ป่ฎบ',
|
|
192
|
+
'',
|
|
193
|
+
status === 'pass'
|
|
194
|
+
? '- ๅทฒๅฎๆๅฏนๅบ็บงๅซ็ๆต่ฏ้ช่ฏ,ๅฏๆๆตใ'
|
|
195
|
+
: '- ๅญๅจๅคฑ่ดฅใๅพ
่กฅๅ
ๆๆช่ฏๅซ็ๆต่ฏ่ฏๆฎ,่ฏท็กฎ่ฎค้ฃ้ฉๅๅๆๆตใ',
|
|
196
|
+
'',
|
|
197
|
+
];
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Pull the section between the 1st `## ` and the 2nd `## ` of design.md.
|
|
203
|
+
* Falls back to the first 30 lines of body when no `## ` sections.
|
|
204
|
+
*/
|
|
205
|
+
function extractDesignSummary(md) {
|
|
206
|
+
const lines = md.split('\n');
|
|
207
|
+
const idx = [];
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
if (/^## /.test(lines[i])) idx.push(i);
|
|
210
|
+
}
|
|
211
|
+
if (idx.length >= 2) {
|
|
212
|
+
return lines.slice(idx[0], idx[1]).join('\n').trim();
|
|
213
|
+
}
|
|
214
|
+
if (idx.length === 1) {
|
|
215
|
+
return lines.slice(idx[0]).join('\n').trim();
|
|
216
|
+
}
|
|
217
|
+
// fallback: first 30 non-empty lines
|
|
218
|
+
return lines.filter((l) => l.trim()).slice(0, 30).join('\n').trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Find a `## ...้ฃ้ฉ...` or `## ...ๆต่ฏ...` section and return its body.
|
|
223
|
+
* Returns null when not found.
|
|
224
|
+
*/
|
|
225
|
+
function extractRiskSection(md) {
|
|
226
|
+
const lines = md.split('\n');
|
|
227
|
+
let start = -1;
|
|
228
|
+
for (let i = 0; i < lines.length; i++) {
|
|
229
|
+
if (/^##\s.*(้ฃ้ฉ|ๅๅฝ|ๆต่ฏ)/.test(lines[i])) { start = i; break; }
|
|
230
|
+
}
|
|
231
|
+
if (start === -1) return null;
|
|
232
|
+
let end = lines.length;
|
|
233
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
234
|
+
if (/^## /.test(lines[i])) { end = i; break; }
|
|
235
|
+
}
|
|
236
|
+
return lines.slice(start, end).join('\n').trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function extractDbChangeSummary({ designMd = '', ddlSql = '' } = {}) {
|
|
240
|
+
const dbSections = extractDbSections(designMd);
|
|
241
|
+
const sqlParts = [
|
|
242
|
+
...dbSections.flatMap((section) => extractSqlBlocks(section)),
|
|
243
|
+
ddlSql,
|
|
244
|
+
].map((s) => s.trim()).filter(Boolean);
|
|
245
|
+
const ddlText = sqlParts.join('\n\n');
|
|
246
|
+
const changes = parseDdlChanges(ddlText);
|
|
247
|
+
if (changes.length) {
|
|
248
|
+
return renderTable(
|
|
249
|
+
['็ฑปๅ', '่กจ', 'ๅญๆฎต', '่ฏดๆ'],
|
|
250
|
+
changes.map((item) => [item.type, item.table || '-', item.field || '-', item.note || '-'])
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const meaningful = dbSections.find(hasMeaningfulDbChange);
|
|
254
|
+
if (meaningful) return meaningful.trim();
|
|
255
|
+
if (hasMeaningfulDbChange(ddlSql)) return `\`\`\`sql\n${ddlSql.trim()}\n\`\`\``;
|
|
256
|
+
return '_(ๆ ๆฐๆฎๅบ่กจ็ปๆๅๆด)_';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function extractDbSections(md) {
|
|
260
|
+
const lines = String(md || '').split('\n');
|
|
261
|
+
const sections = [];
|
|
262
|
+
for (let i = 0; i < lines.length; i++) {
|
|
263
|
+
const m = /^(#{2,4})\s+(.+?)\s*$/.exec(lines[i]);
|
|
264
|
+
if (!m) continue;
|
|
265
|
+
const title = m[2].replace(/\{#[^}]+\}/g, '').trim();
|
|
266
|
+
if (!/(DDL|ๆฐๆฎๅฑ|ๆฐๆฎๅบ|ๅบ่กจ|่กจ็ปๆ)/i.test(title)) continue;
|
|
267
|
+
const level = m[1].length;
|
|
268
|
+
let end = lines.length;
|
|
269
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
270
|
+
const next = /^(#{2,4})\s+/.exec(lines[j]);
|
|
271
|
+
if (next && next[1].length <= level) { end = j; break; }
|
|
272
|
+
}
|
|
273
|
+
sections.push(lines.slice(i, end).join('\n').trim());
|
|
274
|
+
}
|
|
275
|
+
return sections;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function extractSqlBlocks(md) {
|
|
279
|
+
const out = [];
|
|
280
|
+
const re = /```(?:sql|mysql)?\s*\n([\s\S]*?)```/gi;
|
|
281
|
+
let m;
|
|
282
|
+
while ((m = re.exec(String(md || '')))) out.push(m[1]);
|
|
283
|
+
return out.length ? out : [String(md || '')];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseDdlChanges(sql) {
|
|
287
|
+
const out = [];
|
|
288
|
+
const statements = splitSqlStatements(stripSqlComments(sql));
|
|
289
|
+
for (const stmt of statements) {
|
|
290
|
+
const s = stmt.replace(/\s+/g, ' ').trim();
|
|
291
|
+
if (!s) continue;
|
|
292
|
+
let m = /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([A-Za-z0-9_.]+)`?/i.exec(s);
|
|
293
|
+
if (m) {
|
|
294
|
+
out.push({ type: 'ๆฐๅปบ่กจ', table: cleanSqlName(m[1]), field: '-', note: firstSqlLine(stmt) });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
m = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([A-Za-z0-9_.]+)`?\s+ADD\s+(?:COLUMN\s+)?(?:IF\s+NOT\s+EXISTS\s+)?`?([A-Za-z0-9_]+)`?/i.exec(s);
|
|
298
|
+
if (m) {
|
|
299
|
+
out.push({ type: 'ๆฐๅขๅญๆฎต', table: cleanSqlName(m[1]), field: cleanSqlName(m[2]), note: ddlCommentOrLine(stmt) });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
m = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([A-Za-z0-9_.]+)`?\s+(?:MODIFY|ALTER)\s+(?:COLUMN\s+)?`?([A-Za-z0-9_]+)`?/i.exec(s);
|
|
303
|
+
if (m) {
|
|
304
|
+
out.push({ type: 'ไฟฎๆนๅญๆฎต', table: cleanSqlName(m[1]), field: cleanSqlName(m[2]), note: ddlCommentOrLine(stmt) });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
m = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([A-Za-z0-9_.]+)`?\s+CHANGE\s+(?:COLUMN\s+)?`?([A-Za-z0-9_]+)`?\s+`?([A-Za-z0-9_]+)`?/i.exec(s);
|
|
308
|
+
if (m) {
|
|
309
|
+
const from = cleanSqlName(m[2]);
|
|
310
|
+
const to = cleanSqlName(m[3]);
|
|
311
|
+
out.push({ type: 'ไฟฎๆนๅญๆฎต', table: cleanSqlName(m[1]), field: from === to ? to : `${from} -> ${to}`, note: ddlCommentOrLine(stmt) });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
m = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([A-Za-z0-9_.]+)`?\s+DROP\s+(?:COLUMN\s+)?(?:IF\s+EXISTS\s+)?`?([A-Za-z0-9_]+)`?/i.exec(s);
|
|
315
|
+
if (m) {
|
|
316
|
+
out.push({ type: 'ๅ ้คๅญๆฎต', table: cleanSqlName(m[1]), field: cleanSqlName(m[2]), note: firstSqlLine(stmt) });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return dedupeDdlChanges(out);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function splitSqlStatements(sql) {
|
|
323
|
+
return String(sql || '').split(/;\s*(?:\n|$)/).map((s) => s.trim()).filter(Boolean);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function stripSqlComments(sql) {
|
|
327
|
+
return String(sql || '')
|
|
328
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
329
|
+
.split('\n')
|
|
330
|
+
.filter((line) => !/^\s*--/.test(line))
|
|
331
|
+
.join('\n');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function cleanSqlName(name) {
|
|
335
|
+
return String(name || '').replace(/`/g, '').trim();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function ddlCommentOrLine(stmt) {
|
|
339
|
+
const comment = /\bCOMMENT\s+'([^']+)'/i.exec(stmt) || /\bCOMMENT\s+"([^"]+)"/i.exec(stmt);
|
|
340
|
+
return comment ? comment[1].trim() : firstSqlLine(stmt);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function firstSqlLine(stmt) {
|
|
344
|
+
return String(stmt || '').split('\n').map((line) => line.trim()).filter(Boolean)[0] || '-';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function dedupeDdlChanges(items) {
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
const out = [];
|
|
350
|
+
for (const item of items) {
|
|
351
|
+
const key = `${item.type}\0${item.table}\0${item.field}`;
|
|
352
|
+
if (seen.has(key)) continue;
|
|
353
|
+
seen.add(key);
|
|
354
|
+
out.push(item);
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function hasMeaningfulDbChange(text) {
|
|
360
|
+
const s = String(text || '');
|
|
361
|
+
return /\b(CREATE|ALTER|DROP|RENAME)\s+(TABLE|INDEX)|\bADD\s+COLUMN\b|\bMODIFY\s+COLUMN\b|\bCHANGE\s+COLUMN\b|ๆฐๅข.*(่กจ|ๅญๆฎต)|ไฟฎๆน.*ๅญๆฎต|ๅ ้ค.*ๅญๆฎต/i.test(s);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
365
|
+
// Reports summary (scan reports/*.md frontmatter)
|
|
366
|
+
|
|
367
|
+
const KIND_LABEL = {
|
|
368
|
+
unit: 'ๅๅ
ๆต่ฏ',
|
|
369
|
+
integration: '้ๆๆต่ฏ',
|
|
370
|
+
e2e: '็ซฏๅฐ็ซฏ',
|
|
371
|
+
joint: '่่ฐๆต่ฏ',
|
|
372
|
+
remote: '่ฟ็จ API',
|
|
373
|
+
smoke: 'ๅ็',
|
|
374
|
+
regression: 'ๅๅฝ',
|
|
375
|
+
perf: 'ๅๆต',
|
|
376
|
+
'self-test': '่ชๆต(็ฐๅบฆ)',
|
|
377
|
+
};
|
|
378
|
+
function labelKind(k) { return KIND_LABEL[k] || k; }
|
|
379
|
+
function statusIcon(s) {
|
|
380
|
+
if (s === 'pass') return 'โ
้่ฟ';
|
|
381
|
+
if (s === 'fail') return 'โ ๅคฑ่ดฅ';
|
|
382
|
+
if (s === 'blocked') return 'โ ้ปๅก';
|
|
383
|
+
if (s === 'pending') return 'โณ ๅพ
่กฅๅ
';
|
|
384
|
+
return s || '-';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function summariseReports({ reportsDir }) {
|
|
388
|
+
if (!fs.existsSync(reportsDir)) return [];
|
|
389
|
+
const aggregateReport = aggregate.readAggregate(reportsDir);
|
|
390
|
+
const out = aggregateReport.reports.map((r) => ({
|
|
391
|
+
...r,
|
|
392
|
+
file: `test-report.md#${r.kind}`,
|
|
393
|
+
}));
|
|
394
|
+
const seenKinds = new Set(out.map((r) => r.kind));
|
|
395
|
+
const files = (await fsp.readdir(reportsDir)).filter((f) => f.endsWith('.md'));
|
|
396
|
+
for (const f of files.sort()) {
|
|
397
|
+
if (f === 'submit.md' || f === 'test-report.md') continue; // generated mail artifacts
|
|
398
|
+
const fm = await readFrontmatter(path.join(reportsDir, f));
|
|
399
|
+
const inferredKind = fm && (fm.kind || f.replace(/-test\.md$|\.md$/, ''));
|
|
400
|
+
if (inferredKind && seenKinds.has(inferredKind)) continue;
|
|
401
|
+
if (!fm) {
|
|
402
|
+
out.push({ kind: f.replace(/\.md$/, ''), status: 'pending', file: f });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
out.push({
|
|
406
|
+
kind: fm.kind || f.replace(/-test\.md$|\.md$/, ''),
|
|
407
|
+
status: fm.status || 'pending',
|
|
408
|
+
total: fm.total != null ? Number(fm.total) : null,
|
|
409
|
+
passed: fm.passed != null ? Number(fm.passed) : null,
|
|
410
|
+
failed: fm.failed != null ? Number(fm.failed) : null,
|
|
411
|
+
coverage: fm.coverage || null,
|
|
412
|
+
failureType: fm.failureType || fm.failure_type || null,
|
|
413
|
+
file: f,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function overallReportStatus(reports) {
|
|
420
|
+
if (!reports.length) return 'pending';
|
|
421
|
+
if (reports.some((r) => r.status === 'fail')) return 'fail';
|
|
422
|
+
if (reports.some((r) => r.status === 'blocked')) return 'blocked';
|
|
423
|
+
if (reports.some((r) => r.status === 'pending' || !r.status)) return 'pending';
|
|
424
|
+
if (reports.some((r) => r.status === 'draft')) return 'pending';
|
|
425
|
+
return 'pass';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function sumReportTotals(reports) {
|
|
429
|
+
let seen = false;
|
|
430
|
+
const out = { total: 0, passed: 0, failed: 0 };
|
|
431
|
+
for (const r of reports) {
|
|
432
|
+
if (r.total != null || r.passed != null || r.failed != null) seen = true;
|
|
433
|
+
out.total += Number.isFinite(r.total) ? r.total : 0;
|
|
434
|
+
out.passed += Number.isFinite(r.passed) ? r.passed : 0;
|
|
435
|
+
out.failed += Number.isFinite(r.failed) ? r.failed : 0;
|
|
436
|
+
}
|
|
437
|
+
return seen ? out : { total: null, passed: null, failed: null };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderReportSummaryTable(reports) {
|
|
441
|
+
if (!reports.length) return '_(ๅฐๆ ๆต่ฏๆฅๅใ่ฟ่ก `devflow test ...` ๆ `devflow report self-test` ๅๅๆๆต)_';
|
|
442
|
+
const rows = reports.map((r) => [
|
|
443
|
+
labelKind(r.kind),
|
|
444
|
+
statusIcon(r.status),
|
|
445
|
+
r.total != null ? String(r.total) : '-',
|
|
446
|
+
r.passed != null ? String(r.passed) : '-',
|
|
447
|
+
r.failed != null ? String(r.failed) : '-',
|
|
448
|
+
r.coverage || '-',
|
|
449
|
+
r.failureType || '-',
|
|
450
|
+
r.file || '-',
|
|
451
|
+
]);
|
|
452
|
+
return renderTable(['็ฑปๅ', '็ถๆ', '็จไพ', '้่ฟ', 'ๅคฑ่ดฅ', '่ฆ็็', 'ๅคฑ่ดฅ็ฑปๅ', 'ๆฅๆบ'], rows);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderApiIntegrationSummary({ reportsDir, reports }) {
|
|
456
|
+
const selected = reports.filter((r) => ['unit', 'integration', 'remote', 'smoke', 'deploy', 'self-test'].includes(r.kind));
|
|
457
|
+
if (!selected.length) return '_(ๆชๅ็ฐ API / ้ๆ / ๅ็ / ้จ็ฝฒๅ่ชๆตๆฅๅ)_';
|
|
458
|
+
return selected.map((r) => renderReportDigest(reportsDir, r, ['ๆ่ฆ', '็ฏๅขไฟกๆฏ', 'ๆง่ก็ปๆ', '็จไพๆง่ก่ฏฆๆ
', 'ๆต่ฏ่ฆ็', '้จ็ฝฒ่ฏๆฎ', 'ๅฝไปค', 'ๆฃๆฅ้กน', 'ๅคฑ่ดฅ่ฏฆๆ
'])).join('\n\n');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderJointTestSummary({ reportsDir, reports }) {
|
|
462
|
+
const selected = reports.filter((r) => isJointTestReport(reportsDir, r));
|
|
463
|
+
if (!selected.length) {
|
|
464
|
+
return [
|
|
465
|
+
'_(ๆชๅ็ฐ่่ฐๆต่ฏๆฅๅใๆถๅๅๅ็ซฏ่่ฐๆถ,ๅปบ่ฎฎ่ฟ่ก `devflow test joint --cmd="..." --backend-url=... --frontend-url=...`๏ผๆๅจ `reports/test-report.md#self-test` ไธญ่กฅๅ
โ่่ฐๆต่ฏๆฅๅโ็ซ ่ใ)_',
|
|
466
|
+
'',
|
|
467
|
+
'ๅปบ่ฎฎ่ฎฐๅฝ:',
|
|
468
|
+
'- ๅ็ซฏๅฐๅใๅ็ซฏๅฐๅใๆฐๆฎๅบ/Mock ็ฏๅข',
|
|
469
|
+
'- ๆต่งๅจๆไฝๆญฅ้ชค:ๆๅผ้กต้ข -> ๅกซๅ/็นๅป -> ้ช่ฏ็ปๆ',
|
|
470
|
+
'- ๅ
ณ้ฎๆชๅพๆๅฝๅฑ่ฏๆฎ',
|
|
471
|
+
].join('\n');
|
|
472
|
+
}
|
|
473
|
+
return selected.map((r) => renderReportDigest(reportsDir, r, ['็ฏๅขไฟกๆฏ', '็ฏๅข', 'ๆง่ก็ปๆ', '็จไพๆง่ก่ฏฆๆ
', 'ๅบๆฏ', 'ๅคฑ่ดฅ่ฏฆๆ
'])).join('\n\n');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function renderFailureSummary({ reportsDir, reports }) {
|
|
477
|
+
const failed = reports.filter((r) => r.status === 'fail' || r.status === 'blocked' || (r.failed != null && r.failed > 0));
|
|
478
|
+
if (!failed.length) return '- ๆๆ ๅคฑ่ดฅ็จไพ่ฎฐๅฝใ';
|
|
479
|
+
return failed.map((r) => renderReportDigest(reportsDir, r, ['ๅคฑ่ดฅ่ฏฆๆ
', 'ๅคฑ่ดฅ็จไพ', 'ๅคฑ่ดฅ / ๆๅจ็จไพ'])).join('\n\n');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isJointTestReport(reportsDir, r) {
|
|
483
|
+
if (r.kind === 'joint') return true;
|
|
484
|
+
if (r.kind === 'e2e') return true;
|
|
485
|
+
const txt = readReportTextSync(reportsDir, r.file);
|
|
486
|
+
return /่่ฐ|ๅๅ็ซฏ|ๆต่งๅจ|ๆชๅพ|ๅ็ซฏๅฐๅ|ๅ็ซฏๅฐๅ/.test(txt);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function renderReportDigest(reportsDir, report, headings) {
|
|
490
|
+
const txt = readReportTextSync(reportsDir, report.file);
|
|
491
|
+
const parts = [`### ${labelKind(report.kind)} (${statusIcon(report.status)})`];
|
|
492
|
+
const picked = [];
|
|
493
|
+
for (const h of headings) {
|
|
494
|
+
const section = extractMarkdownSection(txt, h);
|
|
495
|
+
if (section) picked.push(section);
|
|
496
|
+
}
|
|
497
|
+
if (picked.length) {
|
|
498
|
+
parts.push(picked.join('\n\n'));
|
|
499
|
+
} else {
|
|
500
|
+
parts.push(renderTable(['้กน', 'ๅผ'], [
|
|
501
|
+
['ๆฅๆบ', report.file || '-'],
|
|
502
|
+
['็จไพ', report.total != null ? String(report.total) : '-'],
|
|
503
|
+
['้่ฟ', report.passed != null ? String(report.passed) : '-'],
|
|
504
|
+
['ๅคฑ่ดฅ', report.failed != null ? String(report.failed) : '-'],
|
|
505
|
+
['่ฆ็็', report.coverage || '-'],
|
|
506
|
+
]));
|
|
507
|
+
}
|
|
508
|
+
return parts.join('\n\n');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function readReportTextSync(reportsDir, file) {
|
|
512
|
+
if (!file) return '';
|
|
513
|
+
const section = /^test-report\.md#(.+)$/.exec(file);
|
|
514
|
+
if (section) {
|
|
515
|
+
const aggregateReport = aggregate.readAggregate(reportsDir);
|
|
516
|
+
const found = aggregateReport.reports.find((r) => r.kind === section[1]);
|
|
517
|
+
return found ? found.body : '';
|
|
518
|
+
}
|
|
519
|
+
try { return fs.readFileSync(path.join(reportsDir, file), 'utf8'); } catch (_) { return ''; }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function extractMarkdownSection(md, headingText) {
|
|
523
|
+
if (!md) return '';
|
|
524
|
+
const lines = md.split('\n');
|
|
525
|
+
for (let i = 0; i < lines.length; i++) {
|
|
526
|
+
const m = /^(#{2,4})\s+(.+?)\s*$/.exec(lines[i]);
|
|
527
|
+
if (!m) continue;
|
|
528
|
+
const title = m[2].replace(/[๏ผ:]\s*$/, '').trim();
|
|
529
|
+
if (title !== headingText) continue;
|
|
530
|
+
const level = m[1].length;
|
|
531
|
+
let end = lines.length;
|
|
532
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
533
|
+
const next = /^(#{2,4})\s+/.exec(lines[j]);
|
|
534
|
+
if (next && next[1].length <= level) { end = j; break; }
|
|
535
|
+
}
|
|
536
|
+
return lines.slice(i, end).join('\n').trim();
|
|
537
|
+
}
|
|
538
|
+
return '';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function readFrontmatter(p) {
|
|
542
|
+
let txt;
|
|
543
|
+
try { txt = await fsp.readFile(p, 'utf8'); } catch (_) { return null; }
|
|
544
|
+
if (!txt.startsWith('---')) return null;
|
|
545
|
+
const end = txt.indexOf('\n---', 3);
|
|
546
|
+
if (end < 0) return null;
|
|
547
|
+
const yaml = txt.slice(3, end).trim();
|
|
548
|
+
const out = {};
|
|
549
|
+
for (const line of yaml.split('\n')) {
|
|
550
|
+
const m = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line.trim());
|
|
551
|
+
if (m) out[m[1]] = m[2].trim();
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
557
|
+
// Markdown table rendering
|
|
558
|
+
|
|
559
|
+
function renderTable(header, rows) {
|
|
560
|
+
const sep = header.map(() => '---');
|
|
561
|
+
const out = [
|
|
562
|
+
`| ${header.join(' | ')} |`,
|
|
563
|
+
`| ${sep.join(' | ')} |`,
|
|
564
|
+
];
|
|
565
|
+
for (const r of rows) out.push(`| ${r.map((c) => String(c).replace(/\|/g, '\\|')).join(' | ')} |`);
|
|
566
|
+
return out.join('\n');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
570
|
+
// Attachments
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Build the default attachment list. Email defaults to one consolidated test
|
|
574
|
+
* attachment (`reports/test-report.md`) to avoid noisy multi-attachment mail.
|
|
575
|
+
*
|
|
576
|
+
* includeSubmit add reports/submit.md (default false)
|
|
577
|
+
* includeTestReport add reports/test-report.md (default true)
|
|
578
|
+
* includeDocs add requirement.md / design.md / plan.md (default false)
|
|
579
|
+
* includeDesignPlan add design.md / plan.md only (default false)
|
|
580
|
+
* includeReports add raw reports/*.md (default false)
|
|
581
|
+
* extra additional explicit paths (caller-controlled)
|
|
582
|
+
*/
|
|
583
|
+
async function collectAttachments({
|
|
584
|
+
root,
|
|
585
|
+
slug,
|
|
586
|
+
includeSubmit = false,
|
|
587
|
+
includeTestReport = true,
|
|
588
|
+
includeDocs = false,
|
|
589
|
+
includeDesignPlan = false,
|
|
590
|
+
includeReports = false,
|
|
591
|
+
extra = [],
|
|
592
|
+
}) {
|
|
593
|
+
const cdir = change.resolveChangeDir(root, slug);
|
|
594
|
+
const out = [];
|
|
595
|
+
const push = (p) => { if (fs.existsSync(p) && !out.includes(p)) out.push(p); };
|
|
596
|
+
|
|
597
|
+
if (includeSubmit) {
|
|
598
|
+
push(path.join(cdir, 'reports', 'submit.md'));
|
|
599
|
+
}
|
|
600
|
+
if (includeTestReport) {
|
|
601
|
+
push(path.join(cdir, 'reports', 'test-report.md'));
|
|
602
|
+
}
|
|
603
|
+
if (includeDocs) {
|
|
604
|
+
for (const f of ['requirement.md', 'design.md', 'plan.md']) {
|
|
605
|
+
push(path.join(cdir, f));
|
|
606
|
+
}
|
|
607
|
+
} else if (includeDesignPlan) {
|
|
608
|
+
for (const f of ['design.md', 'plan.md']) {
|
|
609
|
+
push(path.join(cdir, f));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (includeReports) {
|
|
613
|
+
const rdir = path.join(cdir, 'reports');
|
|
614
|
+
if (fs.existsSync(rdir)) {
|
|
615
|
+
const list = (await fsp.readdir(rdir)).filter((f) => f.endsWith('.md') && f !== 'submit.md' && f !== 'test-report.md').sort();
|
|
616
|
+
for (const f of list) push(path.join(rdir, f));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const e of extra) {
|
|
620
|
+
if (!e) continue;
|
|
621
|
+
push(path.isAbsolute(e) ? e : path.join(root, e));
|
|
622
|
+
}
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
627
|
+
// Confirm gate
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Show preview and prompt Y/n. Mirrors arb deploy-submit Step 4 (ไบบๅทฅ็กฎ่ฎค).
|
|
631
|
+
* Returns { ok, reason }.
|
|
632
|
+
*
|
|
633
|
+
* Override hooks for testing:
|
|
634
|
+
* _stdin โ alternative readable (default: process.stdin)
|
|
635
|
+
* _stdout โ alternative writable (default: process.stdout)
|
|
636
|
+
* _isTty โ () => bool override
|
|
637
|
+
*/
|
|
638
|
+
async function confirmSend(opts = {}) {
|
|
639
|
+
const stdout = opts._stdout || process.stdout;
|
|
640
|
+
const stdin = opts._stdin || process.stdin;
|
|
641
|
+
const isTty = opts._isTty
|
|
642
|
+
? opts._isTty()
|
|
643
|
+
: !!(process.stdin && process.stdin.isTTY && process.stdout && process.stdout.isTTY);
|
|
644
|
+
|
|
645
|
+
const print = (s) => stdout.write(s + '\n');
|
|
646
|
+
print('');
|
|
647
|
+
print('โโโโโโโโโโโ ๆๆต้ฎไปถ้ข่ง โโโโโโโโโโโ');
|
|
648
|
+
print(` Subject: ${opts.subject}`);
|
|
649
|
+
print(` To: ${(opts.to || []).join(', ') || '(none)'}`);
|
|
650
|
+
if ((opts.cc || []).length) print(` Cc: ${opts.cc.join(', ')}`);
|
|
651
|
+
print(` ้ไปถ (${(opts.attachments || []).length}):`);
|
|
652
|
+
for (const a of opts.attachments || []) print(` - ${a}`);
|
|
653
|
+
const bodyHead = (opts.body || '').split('\n').slice(0, 30).join('\n');
|
|
654
|
+
print('');
|
|
655
|
+
print(' ๆญฃๆ(ๅ 30 ่ก):');
|
|
656
|
+
for (const ln of bodyHead.split('\n')) print(` โ ${ln}`);
|
|
657
|
+
print('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
658
|
+
print('');
|
|
659
|
+
|
|
660
|
+
if (!isTty) {
|
|
661
|
+
return { ok: false, reason: 'non-interactive shell โ pass --no-confirm to send without prompt' };
|
|
662
|
+
}
|
|
663
|
+
const ans = await prompt({ stdin, stdout, q: '็กฎ่ฎคๅ้? [Y/n] ', def: 'Y' });
|
|
664
|
+
const lc = ans.trim().toLowerCase();
|
|
665
|
+
if (lc === '' || lc === 'y' || lc === 'yes') return { ok: true };
|
|
666
|
+
return { ok: false, reason: `user declined (answered "${ans}")` };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function prompt({ stdin, stdout, q, def }) {
|
|
670
|
+
return new Promise((resolve) => {
|
|
671
|
+
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: false });
|
|
672
|
+
rl.question(q, (ans) => { rl.close(); resolve((ans == null ? '' : ans) || def); });
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
677
|
+
// Misc helpers exported for deliver
|
|
678
|
+
|
|
679
|
+
/** Best-effort current branch name; '' on failure. */
|
|
680
|
+
function currentBranch(root) {
|
|
681
|
+
try {
|
|
682
|
+
const r = cp.spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
683
|
+
cwd: root, encoding: 'utf8',
|
|
684
|
+
});
|
|
685
|
+
if (r.status === 0) return r.stdout.trim();
|
|
686
|
+
} catch (_) { /* noop */ }
|
|
687
|
+
return '';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function primaryProjectBranch(st) {
|
|
691
|
+
const projects = Array.isArray(st && st.projects) ? st.projects : [];
|
|
692
|
+
const primary = projects.find((p) => p && p.role === 'primary' && p.branch)
|
|
693
|
+
|| projects.find((p) => p && p.branch);
|
|
694
|
+
return primary ? primary.branch : '';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
module.exports = {
|
|
698
|
+
composeSubject,
|
|
699
|
+
renderSubmitDoc,
|
|
700
|
+
renderTestReportDoc,
|
|
701
|
+
collectAttachments,
|
|
702
|
+
confirmSend,
|
|
703
|
+
summariseReports,
|
|
704
|
+
currentBranch,
|
|
705
|
+
primaryProjectBranch,
|
|
706
|
+
_internals: {
|
|
707
|
+
extractDesignSummary,
|
|
708
|
+
extractRiskSection,
|
|
709
|
+
extractDbChangeSummary,
|
|
710
|
+
parseDdlChanges,
|
|
711
|
+
readFrontmatter,
|
|
712
|
+
renderTable,
|
|
713
|
+
prompt,
|
|
714
|
+
overallReportStatus,
|
|
715
|
+
extractMarkdownSection,
|
|
716
|
+
isJointTestReport,
|
|
717
|
+
},
|
|
718
|
+
};
|