iterate-plugin 3.8.0 → 3.9.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.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * src/session-record.ts — dsh session-linked run records (dsh ≥ 0.2.1-rc.1).
3
+ *
4
+ * Upstream capability (dsh `0.2.1-rc.1`, released 2026-10-17): the *Session
5
+ * state-record API*. An experimental plugin may append its own record onto a
6
+ * session's append-only log with `appendPluginRecord(session, type, data)` and
7
+ * read it back with `pluginRecordOf(event)` (`@deepseek-ai/dsh-session`). The
8
+ * record is marked `ignorable`, never enters the model-visible surface, and is
9
+ * carried along by fork / resume / format migration.
10
+ *
11
+ * Why the plugin adopts it:
12
+ * The iterate run state has, until now, lived ONLY under `<projectRoot>/.iterate/`.
13
+ * That is correct for normal operation but has two blind spots the session
14
+ * log closes:
15
+ * 1. **Loss recovery** — if `.iterate/` is deleted, moved, or the working
16
+ * directory changes, the observatory goes blank even though the session
17
+ * that produced the run is still open. A compact run record on the
18
+ * session log lets `iterate_transcript read` rehydrate the run summary
19
+ * instead of returning an empty view.
20
+ * 2. **Session anchoring** — run provenance is tied to the *session* that
21
+ * produced it, not only to whatever directory the process happens to be
22
+ * in when someone looks afterwards.
23
+ *
24
+ * Design (fail-safe, never a hard dependency):
25
+ * - The API is loaded lazily via a dynamic `import()` and used structurally
26
+ * (no compile-time coupling to `PluginRecordMap`), so a host whose copy of
27
+ * `@deepseek-ai/dsh-session` predates the API simply reports
28
+ * `supported: false` instead of failing to load the plugin.
29
+ * - Every entry point returns a structured result; nothing throws. Writing a
30
+ * record is best-effort — a run must never fail because the session log
31
+ * could not accept a bookkeeping record.
32
+ * - `normalizeRunRecord` re-validates a restored payload, because a record
33
+ * read from a resumed session carries whatever JSON an earlier build wrote.
34
+ */
35
+ /** The `plugin:` record type this plugin owns (grammar-checked by dsh). */
36
+ export const ITERATE_RUN_RECORD_TYPE = 'plugin:iterate/run';
37
+ /** Bump when the record payload shape changes incompatibly. */
38
+ export const ITERATE_RUN_RECORD_VERSION = 1;
39
+ // ─── Pure payload construction / validation ─────────────────────────────────
40
+ /** Coerce an unknown to a finite non-negative integer, else `fallback`. */
41
+ function nonNegInt(value, fallback = 0) {
42
+ if (typeof value !== 'number' || !Number.isFinite(value))
43
+ return fallback;
44
+ const n = Math.floor(value);
45
+ return n >= 0 ? n : fallback;
46
+ }
47
+ /** Extract a string, else `''`. */
48
+ function str(value) {
49
+ return typeof value === 'string' ? value : '';
50
+ }
51
+ /** Extract one of the allowed literal values, else `null`. */
52
+ function oneOf(value, allowed) {
53
+ return typeof value === 'string' && allowed.includes(value) ? value : null;
54
+ }
55
+ /**
56
+ * Build a run record from the fields a persisted transcript manifest exposes.
57
+ * Pure: the caller passes already-normalized values, so this is unit-testable
58
+ * without touching disk or a session.
59
+ */
60
+ export function buildRunRecord(input) {
61
+ const convergence = Array.isArray(input.convergence)
62
+ ? input.convergence
63
+ .filter((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)
64
+ .slice(-1000)
65
+ : [];
66
+ const c = input.counts ?? {};
67
+ const stopped = str(input.stoppedReason);
68
+ return {
69
+ version: ITERATE_RUN_RECORD_VERSION,
70
+ project: str(input.project),
71
+ goal: str(input.goal),
72
+ mode: oneOf(input.mode, ['dry-run', 'normal']),
73
+ taskMode: oneOf(input.taskMode, ['code', 'iterate']),
74
+ round: nonNegInt(input.round),
75
+ maxRounds: nonNegInt(input.maxRounds),
76
+ active: input.active !== false,
77
+ stoppedReason: stopped ? stopped : null,
78
+ convergence,
79
+ findings: {
80
+ total: nonNegInt(c.total),
81
+ critical: nonNegInt(c.critical),
82
+ high: nonNegInt(c.high),
83
+ medium: nonNegInt(c.medium),
84
+ low: nonNegInt(c.low),
85
+ },
86
+ fixedCount: nonNegInt(input.fixedCount),
87
+ qualityStatus: oneOf(input.qualityStatus, ['pass', 'fail', 'pending']),
88
+ updatedAt: str(input.now) || new Date().toISOString(),
89
+ };
90
+ }
91
+ /**
92
+ * Owner-side validation of a payload read back from a session log.
93
+ * A restored record carries whatever JSON an earlier build of this plugin
94
+ * wrote, so every field is re-checked; anything unrecognizable returns `null`
95
+ * (the caller skips it) rather than surfacing junk to a renderer.
96
+ */
97
+ export function normalizeRunRecord(data) {
98
+ if (!data || typeof data !== 'object' || Array.isArray(data))
99
+ return null;
100
+ const d = data;
101
+ // A record written by a future/incompatible build is skipped rather than
102
+ // coerced: the version gate is the contract that lets the payload evolve.
103
+ if (nonNegInt(d.version) !== ITERATE_RUN_RECORD_VERSION)
104
+ return null;
105
+ const findings = (d.findings && typeof d.findings === 'object' && !Array.isArray(d.findings)
106
+ ? d.findings
107
+ : {});
108
+ return buildRunRecord({
109
+ project: d.project,
110
+ goal: d.goal,
111
+ mode: d.mode,
112
+ taskMode: d.taskMode,
113
+ round: d.round,
114
+ maxRounds: d.maxRounds,
115
+ active: d.active,
116
+ stoppedReason: d.stoppedReason,
117
+ convergence: d.convergence,
118
+ counts: {
119
+ total: findings.total,
120
+ critical: findings.critical,
121
+ high: findings.high,
122
+ medium: findings.medium,
123
+ low: findings.low,
124
+ },
125
+ fixedCount: d.fixedCount,
126
+ qualityStatus: d.qualityStatus,
127
+ now: typeof d.updatedAt === 'string' ? d.updatedAt : undefined,
128
+ });
129
+ }
130
+ // ─── Session access (lazy, fail-safe) ────────────────────────────────────────
131
+ /** Lazily-resolved production API; `undefined` = not yet attempted. */
132
+ let cachedApi;
133
+ /** Test override; when set (including `null`) the dynamic import is bypassed. */
134
+ let testApiOverride;
135
+ /** Resolve the `@deepseek-ai/dsh-session` record API, or `null` when absent. */
136
+ export function loadSessionRecordApi() {
137
+ if (testApiOverride !== undefined)
138
+ return Promise.resolve(testApiOverride);
139
+ if (cachedApi === undefined) {
140
+ cachedApi = import('@deepseek-ai/dsh-session')
141
+ .then((mod) => {
142
+ const m = mod;
143
+ if (typeof m.appendPluginRecord !== 'function' || typeof m.pluginRecordOf !== 'function') {
144
+ return null;
145
+ }
146
+ return {
147
+ appendPluginRecord: m.appendPluginRecord.bind(mod),
148
+ pluginRecordOf: m.pluginRecordOf.bind(mod),
149
+ };
150
+ })
151
+ .catch(() => null);
152
+ }
153
+ return cachedApi;
154
+ }
155
+ /**
156
+ * Override the session-record API for tests. Pass `undefined` to restore the
157
+ * real dynamic import, or an object / `null` to force a deterministic result.
158
+ */
159
+ export function setSessionRecordApiForTests(api) {
160
+ testApiOverride = api;
161
+ }
162
+ /** Extract the session from a tool execution (`exec.agent.session`), defensively. */
163
+ export function sessionOf(exec) {
164
+ try {
165
+ const agent = exec?.agent;
166
+ const session = agent?.session;
167
+ if (!session || typeof session !== 'object')
168
+ return undefined;
169
+ const s = session;
170
+ if (typeof s.ownEvents === 'function' || typeof s.snapshotEvents === 'function')
171
+ return s;
172
+ return undefined;
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ /** Read every event a session exposes (own events preferred, snapshot fallback). */
179
+ function eventsOf(session) {
180
+ try {
181
+ if (typeof session.ownEvents === 'function')
182
+ return session.ownEvents() ?? [];
183
+ }
184
+ catch {
185
+ // fall through to snapshot
186
+ }
187
+ try {
188
+ if (typeof session.snapshotEvents === 'function')
189
+ return session.snapshotEvents() ?? [];
190
+ }
191
+ catch {
192
+ // no readable log
193
+ }
194
+ return [];
195
+ }
196
+ /**
197
+ * Best-effort append of one run record to the session log.
198
+ * Never throws: an absent API, a degraded session, or a rejected payload all
199
+ * surface as `{ ok: false }` so the surrounding tool call can continue.
200
+ */
201
+ export async function appendRunRecord(session, record) {
202
+ const api = await loadSessionRecordApi();
203
+ if (!api)
204
+ return { ok: false, supported: false, error: 'session record API unavailable' };
205
+ if (!session)
206
+ return { ok: false, supported: true, error: 'no session in scope' };
207
+ try {
208
+ const seq = api.appendPluginRecord(session, ITERATE_RUN_RECORD_TYPE, record);
209
+ return { ok: true, supported: true, seq };
210
+ }
211
+ catch (err) {
212
+ return {
213
+ ok: false,
214
+ supported: true,
215
+ error: `session rejected the run record: ${err instanceof Error ? err.message : String(err)}`,
216
+ };
217
+ }
218
+ }
219
+ /** Read every iterate run record from the session log (oldest → newest). */
220
+ export async function readRunRecords(session) {
221
+ const api = await loadSessionRecordApi();
222
+ if (!api)
223
+ return { supported: false, records: [] };
224
+ if (!session)
225
+ return { supported: true, records: [] };
226
+ try {
227
+ const records = [];
228
+ for (const event of eventsOf(session)) {
229
+ const rec = api.pluginRecordOf(event);
230
+ if (!rec || rec.type !== ITERATE_RUN_RECORD_TYPE)
231
+ continue;
232
+ const normalized = normalizeRunRecord(rec.data);
233
+ if (!normalized)
234
+ continue;
235
+ records.push({ ...normalized, seq: nonNegInt(rec.seq), time: nonNegInt(rec.time) });
236
+ }
237
+ return { supported: true, records };
238
+ }
239
+ catch (err) {
240
+ return {
241
+ supported: true,
242
+ records: [],
243
+ error: `failed to read session records: ${err instanceof Error ? err.message : String(err)}`,
244
+ };
245
+ }
246
+ }
@@ -22,7 +22,8 @@ You have the iterate plugin installed, which registers these tools:
22
22
  - \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence, and whether the run was interrupted (a checkpoint on disk with no newer decision-log activity means the previous run was interrupted and can be resumed)
23
23
  - \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
24
24
  - \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
25
- - \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers; \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, per-round validation results (\`validations\`: command + exitCode + allowed + rejectReason), and timeline so the client observatory panel reflects the run; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
25
+ - \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers — and, when the file is missing or malformed, recovers a summary manifest from the session log's run record (returns \`recovered:true\`); \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, per-round validation results (\`validations\`: command + exitCode + allowed + rejectReason), and timeline so the client observatory panel reflects the run, and links the run to the dsh session (\`sessionRecord\`); \`recall\` lists the run records linked to this session (newest first) so a run stays traceable after \`.iterate/\` is wiped; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
26
+ - \`iterate_report\` — export the current run as a portable report. \`format:"markdown"\` (default) renders a PR-pasteable report (run header, quality gate, findings, fixes, validations, defense events, experience bank); \`format:"sarif"\` renders a SARIF 2.1.0 log for CI / GitHub Code Scanning (one rule per review dimension, severity → error/warning/note, file+line locations, suggested fix attached). Set \`write:true\` to persist it under \`.iterate/report.md\` / \`.iterate/report.sarif\`; \`lang\` (zh | en) selects the report language. Read-only apart from the optional report file.
26
27
  - \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it; \`remove\` deletes a stale or incorrect entry by \`id\` so bad experiences never resurface.
27
28
  - \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json\`; \`clear\` removes the persisted certificate to reset a stale gate before a fresh iteration.
28
29
  - \`iterate_defense_events\` — defense event stream (\`.iterate/defense-events.json\`): \`list\`/\`counts\` review precondition failures, rollbacks, invariant violations, and falsified assumptions; \`record\` logs a new event when a defense fires. Human-readable labels follow the project \`language\` (en/zh).
@@ -0,0 +1,239 @@
1
+ /**
2
+ * src/tools/report.ts — `iterate_report` tool.
3
+ *
4
+ * iterate_report — render the current iterate run into a portable report.
5
+ *
6
+ * Two formats, one normalized data model (shared with the client export
7
+ * buttons via `lib/parse.js`, so the on-screen export and this tool can never
8
+ * drift):
9
+ *
10
+ * - `markdown` — a PR-pasteable run report: header (mode/round/status), the
11
+ * quality-gate table, the findings table, the fixes table,
12
+ * validations, defense events, and the experience bank.
13
+ * - `sarif` — a SARIF 2.1.0 log so CI / GitHub Code Scanning can consume
14
+ * iterate findings as code-scanning results (ruleId =
15
+ * dimension, severity → error/warning/note, file+line
16
+ * locations, suggested fix attached).
17
+ *
18
+ * Reads only local state (`.iterate/transcript.json` + the quality / experience
19
+ * / defense stores) and writes only `.iterate/report.<ext>` when `write:true`.
20
+ * Malformed or missing state is rendered as-is (empty tables) — an export must
21
+ * never fail because a run was partial.
22
+ */
23
+ import { existsSync, readFileSync } from 'node:fs';
24
+ import { mkdirSync, writeFileSync } from 'node:fs';
25
+ import { defineTool } from '@deepseek-ai/dsh-tools';
26
+ import { resolveProjectRootForExec } from "../config-loader.js";
27
+ import { iterateDir, transcriptPath } from "../paths.js";
28
+ import { readQualityGate } from "./quality-store.js";
29
+ import { readExperienceBank } from "./experience-store.js";
30
+ import { readDefenseEvents } from "./defense-store.js";
31
+ import { resultHeadline } from "./present.js";
32
+ import { renderRunReportMarkdown, renderRunReportSarif, REPORT_FORMATS } from "../report-render.js";
33
+ /** Formats accepted by the tool (kept in sync with the shared renderer). */
34
+ const FORMATS = new Set(REPORT_FORMATS);
35
+ /** Read the persisted transcript manifest, tolerating a missing/garbled file. */
36
+ function readManifest(projectRoot) {
37
+ const file = transcriptPath(projectRoot);
38
+ if (!existsSync(file))
39
+ return { manifest: {}, source: 'empty' };
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(file, 'utf-8'));
42
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
43
+ return { manifest: parsed, source: 'disk' };
44
+ }
45
+ return { manifest: {}, source: 'empty' };
46
+ }
47
+ catch {
48
+ return { manifest: {}, source: 'empty' };
49
+ }
50
+ }
51
+ /**
52
+ * Register the `iterate_report` tool.
53
+ * Renders the current run into a Markdown or SARIF report.
54
+ */
55
+ export function registerReportTool(ctx) {
56
+ ctx.tools.register(defineTool({
57
+ name: 'iterate_report',
58
+ // Result card: surface the report headline ("report: markdown · N findings")
59
+ // as the card title. Pure: reads only the rendered result (replay-safe).
60
+ presentResult: (_args, result) => {
61
+ const title = resultHeadline(result);
62
+ return title ? { card: 'generic', title } : undefined;
63
+ },
64
+ // `write:true` persists a file; a pure render joins a parallel group.
65
+ isConcurrencySafe: (args) => args.write !== true,
66
+ description: 'Export the current iterate run as a portable report. ' +
67
+ '`format:"markdown"` (default) renders a PR-pasteable report: run header (mode / round / status), ' +
68
+ 'quality-gate table, findings table, fixes table, per-round validations, defense events, and the ' +
69
+ 'experience bank. `format:"sarif"` renders a SARIF 2.1.0 log for CI / GitHub Code Scanning ' +
70
+ '(one rule per review dimension; finding severity maps to error/warning/note; findings carry ' +
71
+ 'file + line locations and the suggested fix). ' +
72
+ 'Set `write:true` to also persist the report under `.iterate/report.md` / `.iterate/report.sarif` ' +
73
+ 'and get the path back. `lang` selects the report language (zh | en). ' +
74
+ 'Read-only apart from the optional report file; never touches source files.',
75
+ parameters: {
76
+ format: {
77
+ type: 'string',
78
+ description: 'Report format: "markdown" (default) or "sarif".',
79
+ enum: ['markdown', 'sarif'],
80
+ },
81
+ write: {
82
+ type: 'boolean',
83
+ description: 'When true, persist the report to `.iterate/report.md` / `.iterate/report.sarif` and return its path. Default: false.',
84
+ },
85
+ lang: {
86
+ type: 'string',
87
+ description: 'Report language: "zh" (default) or "en". Affects labels/headings only.',
88
+ enum: ['zh', 'en'],
89
+ },
90
+ path: {
91
+ type: 'string',
92
+ description: 'Project root directory (default: current working directory).',
93
+ },
94
+ },
95
+ output: {
96
+ schema: {
97
+ type: 'object',
98
+ additionalProperties: false,
99
+ properties: {
100
+ ok: { type: 'boolean', required: true },
101
+ kind: { type: 'string' },
102
+ format: { type: 'string' },
103
+ /** The rendered report text (Markdown, or pretty-printed SARIF JSON). */
104
+ content: { type: 'string' },
105
+ /** The SARIF log object, when format is "sarif". */
106
+ sarif: { type: 'json' },
107
+ /** Whether a source transcript manifest was found on disk. */
108
+ source: { type: 'string' },
109
+ written: { type: 'boolean' },
110
+ reportPath: { type: 'string' },
111
+ findings: { type: 'number' },
112
+ error: { type: 'string' },
113
+ },
114
+ },
115
+ render: (_args, value) => {
116
+ if (!value.ok)
117
+ return [{ type: 'text', text: `report export failed: ${value.error}` }];
118
+ const content = typeof value.content === 'string' ? value.content : '';
119
+ const header = [
120
+ `report: ${String(value.format ?? 'markdown')} · ${String(value.findings ?? 0)} findings · source=${String(value.source ?? 'empty')}`,
121
+ value.written ? `written to ${String(value.reportPath ?? '')}` : '',
122
+ ].filter(Boolean).join(' · ');
123
+ return [{ type: 'text', text: `${header}\n\n${content}` }];
124
+ },
125
+ },
126
+ async execute(args, exec) {
127
+ const resolved = resolveProjectRootForExec(exec, args.path);
128
+ if (!resolved.ok)
129
+ return { ok: false, kind: 'report', error: resolved.reason };
130
+ const projectRoot = resolved.root;
131
+ const rawFormat = typeof args.format === 'string' ? args.format : 'markdown';
132
+ const format = FORMATS.has(rawFormat) ? rawFormat : 'markdown';
133
+ const lang = args.lang === 'en' ? 'en' : 'zh';
134
+ const { manifest, source } = readManifest(projectRoot);
135
+ let qualityGate = null;
136
+ let experienceBank = null;
137
+ let defenseEvents = null;
138
+ try {
139
+ qualityGate = readQualityGate(projectRoot);
140
+ }
141
+ catch {
142
+ qualityGate = null;
143
+ }
144
+ try {
145
+ experienceBank = readExperienceBank(projectRoot);
146
+ }
147
+ catch {
148
+ experienceBank = null;
149
+ }
150
+ try {
151
+ defenseEvents = readDefenseEvents(projectRoot);
152
+ }
153
+ catch {
154
+ defenseEvents = null;
155
+ }
156
+ const extra = { qualityGate, experienceBank, defenseEvents };
157
+ let content;
158
+ let sarif;
159
+ let findings = 0;
160
+ try {
161
+ if (format === 'sarif') {
162
+ const doc = renderRunReportSarif(manifest, extra, {
163
+ toolVersion: pluginVersion(),
164
+ });
165
+ sarif = doc;
166
+ content = JSON.stringify(doc, null, 2);
167
+ findings = Array.isArray(doc?.runs?.[0]?.results) ? doc.runs[0].results.length : 0;
168
+ }
169
+ else {
170
+ content = renderRunReportMarkdown(manifest, extra, { lang });
171
+ findings = countMarkdownFindings(content);
172
+ }
173
+ }
174
+ catch (err) {
175
+ return {
176
+ ok: false,
177
+ kind: 'report',
178
+ format,
179
+ error: `failed to render report: ${err instanceof Error ? err.message : String(err)}`,
180
+ };
181
+ }
182
+ let written = false;
183
+ let reportPath;
184
+ if (args.write === true) {
185
+ try {
186
+ const dir = iterateDir(projectRoot);
187
+ mkdirSync(dir, { recursive: true });
188
+ reportPath = `${dir}/report.${format === 'sarif' ? 'sarif' : 'md'}`;
189
+ writeFileSync(reportPath, content, 'utf-8');
190
+ written = true;
191
+ }
192
+ catch (err) {
193
+ return {
194
+ ok: false,
195
+ kind: 'report',
196
+ format,
197
+ error: `failed to write report: ${err instanceof Error ? err.message : String(err)}`,
198
+ };
199
+ }
200
+ }
201
+ return {
202
+ ok: true,
203
+ kind: 'report',
204
+ format,
205
+ content,
206
+ ...(sarif !== undefined ? { sarif } : {}),
207
+ source,
208
+ written,
209
+ ...(reportPath ? { reportPath } : {}),
210
+ findings,
211
+ };
212
+ },
213
+ }));
214
+ }
215
+ /**
216
+ * Best-effort plugin version for the SARIF driver. Resolved from the package
217
+ * manifest next to this module (`src/tools` and `dist/tools` are both two
218
+ * levels below the root); an unreadable/unparsable manifest yields ''.
219
+ */
220
+ function pluginVersion() {
221
+ try {
222
+ const raw = readFileSync(new URL('../../package.json', import.meta.url), 'utf-8');
223
+ const pkg = JSON.parse(raw);
224
+ return typeof pkg.version === 'string' ? pkg.version : '';
225
+ }
226
+ catch {
227
+ return '';
228
+ }
229
+ }
230
+ /** Count the findings table rows in a rendered Markdown report (for the card). */
231
+ function countMarkdownFindings(markdown) {
232
+ const m = /##\s+.*?\((\d+)\)/.exec(markdown);
233
+ if (m && m[1]) {
234
+ const n = Number(m[1]);
235
+ if (Number.isFinite(n))
236
+ return n;
237
+ }
238
+ return 0;
239
+ }