dsh-data-cleaning-agent 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.en.md +21 -7
- package/README.md +23 -6
- package/docs/COMPATIBILITY.md +31 -4
- package/docs/G5-E2E-RUNBOOK.md +110 -0
- package/docs/G5-HOST-BRIDGE.md +136 -0
- package/docs/PHASE2-ACCEPTANCE.md +133 -0
- package/docs/QCC-ENRICHMENT-DESIGN.md +241 -0
- package/docs/QCC-PHASES-ROADMAP.md +254 -0
- package/docs/RELEASE-0.4.0.md +57 -0
- package/docs/USER-GUIDE.md +53 -1
- package/lib/index.js +5 -0
- package/lib/qcc-phase2-acceptance.js +191 -0
- package/lib/qcc-phase2.js +99 -0
- package/lib/qcc-runs.js +322 -0
- package/lib/qcc-safety.js +77 -0
- package/lib/qcc.js +702 -0
- package/lib/skill-enrich.js +63 -0
- package/lib/web.js +189 -1
- package/package.json +12 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内嵌 Skill:`enterprise-enrichment`(企查查企业名单补全)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `data-cleaning` 职责分离:
|
|
5
|
+
* - `data-cleaning`:确定性清洗 / 补全 / 概览(本地引擎,不依赖外部数据)。
|
|
6
|
+
* - `enterprise-enrichment`:用企查查 MCP 工具按最新工商信息补全企业名单
|
|
7
|
+
* (依赖 `qcc-dsh-mcp-oauth` 已连接;本 Skill 不重造 OAuth)。
|
|
8
|
+
*
|
|
9
|
+
* 方案 A(模型中介式):模型亲自调用 `mcp__qcc-company__*` / `mcp__qcc-risk__*`
|
|
10
|
+
* 完成消歧 → 工商详情 → 风险标签,再组装结果。本插件零后端改动。
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
QCC_PHASE2_COMPANY_TOOLS,
|
|
14
|
+
QCC_PHASE2_DIMENSION_GROUPS,
|
|
15
|
+
QCC_PHASE2_HISTORY_TOOLS,
|
|
16
|
+
} from './qcc-phase2.js';
|
|
17
|
+
|
|
18
|
+
export const ENRICH_SKILL_NAME = 'enterprise-enrichment';
|
|
19
|
+
|
|
20
|
+
const companyTools = QCC_PHASE2_COMPANY_TOOLS;
|
|
21
|
+
const historyTools = QCC_PHASE2_HISTORY_TOOLS;
|
|
22
|
+
|
|
23
|
+
const groupSummary = Object.entries(QCC_PHASE2_DIMENSION_GROUPS)
|
|
24
|
+
.map(([id, group]) => ` - \`${id}\`(${group.label}): ${group.tools.join(', ')}`)
|
|
25
|
+
.join('\n');
|
|
26
|
+
|
|
27
|
+
export function registerEnrichSkill(skills) {
|
|
28
|
+
return skills.register({
|
|
29
|
+
name: ENRICH_SKILL_NAME,
|
|
30
|
+
description:
|
|
31
|
+
'Enrich company lists with Qichacha (QCC) registration, company panorama, ownership, governance, and optional historical-business dimensions.',
|
|
32
|
+
whenToUse:
|
|
33
|
+
'When the user gives company names and asks to enrich / complete them with Qichacha (企查查), including registration fields, company panorama, ownership penetration, governance, historical changes, or risk tags.',
|
|
34
|
+
source: 'dsh-data-cleaning-agent',
|
|
35
|
+
content: [
|
|
36
|
+
'You are an enterprise-list enrichment assistant. You fill company lists with Qichacha (QCC) data by calling QCC MCP tools. Never invent, pad, or fabricate any field.',
|
|
37
|
+
'',
|
|
38
|
+
'Workflow:',
|
|
39
|
+
'1. Check QCC availability first: run `qcc_oauth_status`. If not connected, tell the user to run `qcc_oauth_connect` first and stop. If the token is expired, guide the user to `qcc_oauth_connect` (it reuses the grant and refreshes without a new authorization page).',
|
|
40
|
+
'2. Parse the company-name list from what the user gave (pasted text / CSV / JSON / inline list). Keep only the distinct company-name column.',
|
|
41
|
+
`3. For EACH name: run \`${companyTools.resolveEntity}\`.`,
|
|
42
|
+
' - Unique exact match → lock that entity and keep its credit code.',
|
|
43
|
+
' - Multiple candidates → DO NOT auto-pick the first. List the candidates (name + region + credit code) and ask the user which one to use.',
|
|
44
|
+
' - No match → mark that row as `unresolved` and continue.',
|
|
45
|
+
`4. For each locked entity, always run \`${companyTools.registration}\`. Run \`${companyTools.verifyIdentity}\` when the input includes a credit code or the user asks for identity verification.`,
|
|
46
|
+
'5. Determine requested dimension groups from the user request. If it is not explicit, ask the user to choose `panorama`, `ownership`, `governance`, and/or `history`; do not invoke every 0.4.0 tool by default. The verified group contract is:',
|
|
47
|
+
groupSummary,
|
|
48
|
+
'6. Call only the tools required by the selected groups. Process large lists in explicit batches, announce the next batch before paid calls, preserve input row order, and never drop a row silently.',
|
|
49
|
+
`7. The \`history\` group requires an enterprise-certified account. Only call ${Object.values(historyTools).map((tool) => `\`${tool}\``).join(', ')} when the user requested history and the account is eligible. If a history tool is unavailable or returns a permission error, mark the group \`permission_required\` or \`not_available\`, continue current-data groups, and never replace history with guessed values.`,
|
|
50
|
+
'8. When risk tags are explicitly requested, run `mcp__qcc-risk__get_company_risk_scan` and use hit dimensions + returned counts only. Risk is outside the 0.4.0 panorama contract and must not be called implicitly.',
|
|
51
|
+
'9. Assemble each requested dimension with `value`, `status`, and `source_tool`. Missing values are `unresolved`; an absent field never means "none" or zero.',
|
|
52
|
+
'10. Report enriched / unresolved / ambiguous / permission-required counts and a small requested preview. Do not paste a full sensitive list into chat. Use a same-origin Host download or artifact when that capability is available; otherwise say that no downloadable artifact was created instead of pretending one exists.',
|
|
53
|
+
'',
|
|
54
|
+
'Safety rules:',
|
|
55
|
+
'- Never fabricate a credit code, legal representative, capital, amount, ratio, or status.',
|
|
56
|
+
'- Never auto-select among ambiguous candidates — always confirm with the user.',
|
|
57
|
+
'- Quote amounts / ratios / counts exactly as the QCC tool returned them; never recompute, multiply ownership chains, aggregate, or estimate.',
|
|
58
|
+
'- Preserve provenance: every populated dimension must identify the QCC source tool that returned it.',
|
|
59
|
+
'- Do not continue a paid batch after cancellation, authorization failure, or an unresolved ambiguity that affects entity identity.',
|
|
60
|
+
'- Never expose QCC tokens or credentials.',
|
|
61
|
+
].join('\n'),
|
|
62
|
+
});
|
|
63
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { parseCsv, parseXlsx, parseJson, detectFormat, toCsv } from './engine.js';
|
|
11
11
|
import { runSync, DataCleaningJobs } from './jobs.js';
|
|
12
|
+
import { QccBridgeError, QccHostBridge } from './qcc.js';
|
|
13
|
+
import { fingerprintRequest, G5RunStore } from './qcc-runs.js';
|
|
12
14
|
|
|
13
15
|
const MAX_BODY = 16 * 1024 * 1024; // 16 MiB 上传上限(MVP)
|
|
14
16
|
|
|
@@ -52,6 +54,49 @@ async function readBody(req, max = MAX_BODY) {
|
|
|
52
54
|
return Buffer.concat(chunks);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function requirePaidConfirmation(payload) {
|
|
58
|
+
if (payload?.confirmPaidCalls !== true) {
|
|
59
|
+
throw new QccBridgeError(
|
|
60
|
+
'QCC_CONFIRM_REQUIRED',
|
|
61
|
+
'Set confirmPaidCalls=true after the user confirms QCC paid data calls.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function qccHttpStatus(code) {
|
|
67
|
+
if (code === 'QCC_RUN_NOT_FOUND') return 404;
|
|
68
|
+
if (code === 'QCC_AUTH_REQUIRED') return 401;
|
|
69
|
+
if (code === 'QCC_PERMISSION_DENIED') return 403;
|
|
70
|
+
if (code === 'QCC_QUOTA_EXHAUSTED') return 402;
|
|
71
|
+
if (code === 'QCC_RATE_LIMITED') return 429;
|
|
72
|
+
if (code === 'QCC_TIMEOUT') return 504;
|
|
73
|
+
if (
|
|
74
|
+
code === 'QCC_NOT_CONNECTED'
|
|
75
|
+
|| code === 'QCC_TOOL_UNAVAILABLE'
|
|
76
|
+
|| code === 'QCC_UPSTREAM_UNAVAILABLE'
|
|
77
|
+
|| code === 'QCC_IDEMPOTENCY_CAPACITY'
|
|
78
|
+
) return 503;
|
|
79
|
+
if (
|
|
80
|
+
code === 'QCC_CONFIRM_REQUIRED'
|
|
81
|
+
|| code === 'QCC_ABORTED'
|
|
82
|
+
|| code === 'QCC_IDEMPOTENCY_CONFLICT'
|
|
83
|
+
|| code === 'QCC_REVIEW_NOT_PENDING'
|
|
84
|
+
|| code === 'QCC_CANDIDATE_INVALID'
|
|
85
|
+
|| code === 'QCC_OPERATION_IN_PROGRESS'
|
|
86
|
+
|| code === 'QCC_RETRY_NOT_FAILED'
|
|
87
|
+
|| code === 'QCC_RETRY_NOT_ALLOWED'
|
|
88
|
+
) return 409;
|
|
89
|
+
return 400;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeQccError(res, error) {
|
|
93
|
+
const code = error?.code ?? 'QCC_BRIDGE';
|
|
94
|
+
const payload = error instanceof QccBridgeError
|
|
95
|
+
? error.toJSON()
|
|
96
|
+
: { code, message: 'G5 Host Bridge request failed' };
|
|
97
|
+
writeJson(res, qccHttpStatus(code), { ok: false, ...payload });
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/** 从 JSON 协议解析上传:{ filename, content }。content 为字符串;xlsx 时为 base64。 */
|
|
56
101
|
async function parseUpload(body) {
|
|
57
102
|
let payload;
|
|
@@ -147,7 +192,12 @@ async function withRows() {
|
|
|
147
192
|
if (!p.ok) throw new Error(JSON.stringify(p));
|
|
148
193
|
return { rows: p.rows, headers: p.headers };
|
|
149
194
|
}
|
|
150
|
-
|
|
195
|
+
const content = $('src').value.trim();
|
|
196
|
+
if (!content) return { rows: [], headers: null };
|
|
197
|
+
if (content.startsWith('[')) return { rows: JSON.parse(content), headers: null };
|
|
198
|
+
const p = await call('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content });
|
|
199
|
+
if (!p.ok) throw new Error(JSON.stringify(p));
|
|
200
|
+
return { rows: p.rows, headers: p.headers };
|
|
151
201
|
}
|
|
152
202
|
|
|
153
203
|
$('parse').onclick = async () => {
|
|
@@ -212,6 +262,9 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
212
262
|
const tools = wctx.tools;
|
|
213
263
|
const skills = wctx.skills;
|
|
214
264
|
const disposers = [];
|
|
265
|
+
const qccBridge = new QccHostBridge({ tools, logger });
|
|
266
|
+
const g5Runs = new G5RunStore();
|
|
267
|
+
report.qccBridgeMounted = true;
|
|
215
268
|
let state = null; // DataCleaningJobs,惰性初始化
|
|
216
269
|
let stateReady = null;
|
|
217
270
|
|
|
@@ -248,10 +301,145 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
248
301
|
skillListed: null,
|
|
249
302
|
jobs: Boolean(wctx.jobs),
|
|
250
303
|
storageDomain: Boolean(wctx.storageDomain),
|
|
304
|
+
qccBridge: qccBridge.capabilities(),
|
|
251
305
|
},
|
|
252
306
|
});
|
|
253
307
|
});
|
|
254
308
|
|
|
309
|
+
register('/data-cleaning/api/g5/capabilities', (req, res) => {
|
|
310
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
311
|
+
writeJson(res, 200, {
|
|
312
|
+
ok: true,
|
|
313
|
+
marker: 'g5-host-bridge',
|
|
314
|
+
capabilities: qccBridge.capabilities(),
|
|
315
|
+
limits: { maxRows: 100, maxConcurrency: 4 },
|
|
316
|
+
paidCallConfirmationRequired: true,
|
|
317
|
+
idempotencyRequired: true,
|
|
318
|
+
candidateResume: true,
|
|
319
|
+
manualRetry: true,
|
|
320
|
+
runPersistence: 'host-memory',
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
register('/data-cleaning/api/phase2/capabilities', (req, res) => {
|
|
325
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
326
|
+
if (req.method !== 'GET') {
|
|
327
|
+
return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'GET required' });
|
|
328
|
+
}
|
|
329
|
+
writeJson(res, 200, {
|
|
330
|
+
ok: true,
|
|
331
|
+
marker: 'qcc-phase2-capabilities',
|
|
332
|
+
capabilities: qccBridge.phase2Capabilities(),
|
|
333
|
+
executesTools: false,
|
|
334
|
+
paidCalls: false,
|
|
335
|
+
historyAuthorizationRequiresRealCall: true,
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const runPayload = (run, replayed = false) => {
|
|
340
|
+
const headers = [...new Set([
|
|
341
|
+
...run.headers,
|
|
342
|
+
...run.rows.flatMap((row) => Object.keys(row)),
|
|
343
|
+
])];
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
marker: 'g5-host-bridge',
|
|
347
|
+
...run,
|
|
348
|
+
idempotencyReplayed: replayed,
|
|
349
|
+
rowCount: run.rows.length,
|
|
350
|
+
csv: toCsv(headers, run.rows),
|
|
351
|
+
downloadName: 'qcc-enriched.csv',
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
register('/data-cleaning/api/g5/enrich', async (req, res) => {
|
|
356
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
357
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'POST required' });
|
|
358
|
+
try {
|
|
359
|
+
const body = await readBody(req);
|
|
360
|
+
const payload = JSON.parse(body.toString('utf8'));
|
|
361
|
+
requirePaidConfirmation(payload);
|
|
362
|
+
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
|
|
363
|
+
const headers = Array.isArray(payload?.headers) ? payload.headers.map(String) : [];
|
|
364
|
+
const nameField = String(payload?.nameField ?? 'name');
|
|
365
|
+
const includeRisk = payload?.includeRisk === true;
|
|
366
|
+
const concurrency = Number(payload?.concurrency ?? 2);
|
|
367
|
+
const input = { rows, headers, nameField, includeRisk, concurrency };
|
|
368
|
+
const executed = await g5Runs.executeOnce({
|
|
369
|
+
key: payload?.idempotencyKey,
|
|
370
|
+
fingerprint: fingerprintRequest('enrich', input),
|
|
371
|
+
operation: async () => {
|
|
372
|
+
const audit = [];
|
|
373
|
+
const result = await qccBridge.enrichRows(rows, {
|
|
374
|
+
nameField,
|
|
375
|
+
includeRisk,
|
|
376
|
+
concurrency,
|
|
377
|
+
maxRows: 100,
|
|
378
|
+
onAudit: (event) => audit.push(event),
|
|
379
|
+
});
|
|
380
|
+
return g5Runs.createRun({ headers, nameField, includeRisk, concurrency, result, audit });
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
writeJson(res, 200, runPayload(executed.value, executed.replayed));
|
|
384
|
+
} catch (error) {
|
|
385
|
+
writeQccError(res, error);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
register('/data-cleaning/api/g5/resolve', async (req, res) => {
|
|
390
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
391
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'POST required' });
|
|
392
|
+
try {
|
|
393
|
+
const payload = JSON.parse((await readBody(req)).toString('utf8'));
|
|
394
|
+
requirePaidConfirmation(payload);
|
|
395
|
+
const input = {
|
|
396
|
+
runId: String(payload?.runId ?? ''),
|
|
397
|
+
companyName: String(payload?.companyName ?? ''),
|
|
398
|
+
selectedCreditNo: String(payload?.selectedCreditNo ?? ''),
|
|
399
|
+
};
|
|
400
|
+
const executed = await g5Runs.executeOnce({
|
|
401
|
+
key: payload?.idempotencyKey,
|
|
402
|
+
fingerprint: fingerprintRequest('resolve', input),
|
|
403
|
+
operation: () => g5Runs.resolveCandidate(input.runId, input, qccBridge),
|
|
404
|
+
});
|
|
405
|
+
writeJson(res, 200, runPayload(executed.value, executed.replayed));
|
|
406
|
+
} catch (error) {
|
|
407
|
+
writeQccError(res, error);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
register('/data-cleaning/api/g5/retry', async (req, res) => {
|
|
412
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
413
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'POST required' });
|
|
414
|
+
try {
|
|
415
|
+
const payload = JSON.parse((await readBody(req)).toString('utf8'));
|
|
416
|
+
requirePaidConfirmation(payload);
|
|
417
|
+
const input = {
|
|
418
|
+
runId: String(payload?.runId ?? ''),
|
|
419
|
+
companyNames: Array.isArray(payload?.companyNames) ? payload.companyNames.map(String) : [],
|
|
420
|
+
};
|
|
421
|
+
const executed = await g5Runs.executeOnce({
|
|
422
|
+
key: payload?.idempotencyKey,
|
|
423
|
+
fingerprint: fingerprintRequest('retry', input),
|
|
424
|
+
operation: () => g5Runs.retryCompanies(input.runId, input.companyNames, qccBridge),
|
|
425
|
+
});
|
|
426
|
+
writeJson(res, 200, runPayload(executed.value, executed.replayed));
|
|
427
|
+
} catch (error) {
|
|
428
|
+
writeQccError(res, error);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
register('/data-cleaning/api/g5/run', (req, res) => {
|
|
433
|
+
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
434
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'GET required' });
|
|
435
|
+
try {
|
|
436
|
+
const runId = String((req.url ?? '').split('/').filter(Boolean).pop() ?? '');
|
|
437
|
+
writeJson(res, 200, runPayload(g5Runs.get(runId)));
|
|
438
|
+
} catch (error) {
|
|
439
|
+
writeQccError(res, error);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
255
443
|
register('/data-cleaning/api/mvp/parse', async (req, res) => {
|
|
256
444
|
if (!isTrusted(req)) return writeJson(res, 403, { ok: false, error: 'untrusted origin' });
|
|
257
445
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Clean, complete, and profile enterprise name lists in DeepSeek Harness — a data cleaning & completion agent plugin with local CSV/XLSX/JSON engine and optional Qichacha (QCC) MCP enrichment. Maintained by Qichacha/QCC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
"marketing",
|
|
22
22
|
"docs/USER-GUIDE.md",
|
|
23
23
|
"docs/FIRST-CONTRIBUTION.md",
|
|
24
|
-
"docs/COMPATIBILITY.md"
|
|
24
|
+
"docs/COMPATIBILITY.md",
|
|
25
|
+
"docs/QCC-ENRICHMENT-DESIGN.md",
|
|
26
|
+
"docs/QCC-PHASES-ROADMAP.md",
|
|
27
|
+
"docs/PHASE2-ACCEPTANCE.md",
|
|
28
|
+
"docs/RELEASE-0.4.0.md",
|
|
29
|
+
"docs/G5-HOST-BRIDGE.md",
|
|
30
|
+
"docs/G5-E2E-RUNBOOK.md"
|
|
25
31
|
],
|
|
26
32
|
"scripts": {
|
|
27
33
|
"test": "node --test",
|
|
28
|
-
"lint": "node --check lib/index.js && node --check lib/engine.js && node --check lib/tools.js && node --check lib/skill.js && node --check lib/jobs.js && node --check lib/web.js && node --check lib/client.js",
|
|
34
|
+
"lint": "node --check lib/index.js && node --check lib/engine.js && node --check lib/tools.js && node --check lib/skill.js && node --check lib/qcc-phase2.js && node --check lib/qcc-phase2-acceptance.js && node --check lib/skill-enrich.js && node --check lib/jobs.js && node --check lib/qcc-safety.js && node --check lib/qcc.js && node --check lib/qcc-runs.js && node --check lib/web.js && node --check lib/client.js && node --check scripts/check-market-registration.mjs && node --check scripts/g5-e2e.mjs && node --check scripts/phase2-acceptance.mjs",
|
|
29
35
|
"docs:check": "node scripts/check-readme-version.mjs",
|
|
30
36
|
"marketing:check": "node scripts/check-marketing.mjs",
|
|
31
37
|
"verify-pack": "node scripts/verify-pack.mjs",
|
|
38
|
+
"market:check": "node scripts/check-market-registration.mjs",
|
|
39
|
+
"e2e:g5": "node scripts/g5-e2e.mjs",
|
|
40
|
+
"e2e:phase2": "node scripts/phase2-acceptance.mjs",
|
|
32
41
|
"check": "npm run lint && npm run docs:check && npm run marketing:check && npm run verify-pack && npm test",
|
|
33
42
|
"prepublishOnly": "npm run check"
|
|
34
43
|
},
|