@erclx/aitk 1.3.0 → 1.5.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.
@@ -29,13 +29,10 @@ import {
29
29
  logStep,
30
30
  logWarn,
31
31
  outro,
32
+ palette,
32
33
  } from '@/ui'
33
34
  import { describeSkew } from '@/version/skew'
34
35
 
35
- const GREY = '\x1b[0;90m'
36
- const YELLOW = '\x1b[0;33m'
37
- const NC = '\x1b[0m'
38
-
39
36
  const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
40
37
  standards: ['standards', 'sync'],
41
38
  snippets: ['snippets', 'sync'],
@@ -180,6 +177,7 @@ function renderCheck(report: CheckReport): void {
180
177
  )
181
178
  if (uncovered.length === 0) return
182
179
 
180
+ const { GREY, NC } = palette(process.stderr)
183
181
  process.stderr.write(
184
182
  `${GREY}Unstamped: ${uncovered.join(', ')}. Run the matching sync to record one.${NC}\n`,
185
183
  )
@@ -338,6 +336,7 @@ async function runSync(target: string): Promise<number> {
338
336
  if (typeof resolved === 'number') return resolved
339
337
 
340
338
  const git = createGitRunner(resolved)
339
+ const { GREY, NC, YELLOW } = palette(process.stderr)
341
340
 
342
341
  logStep('Checking working tree')
343
342
  if (!isTreeClean(await git.status([]))) {
@@ -28,12 +28,10 @@ import {
28
28
  logStep,
29
29
  logWarn,
30
30
  outro,
31
+ palette,
31
32
  select,
32
33
  } from '@/ui'
33
34
 
34
- const GREEN = '\x1b[0;32m'
35
- const NC = '\x1b[0m'
36
-
37
35
  const PASS_THROUGH_VERBS = ['ref', 'create', 'verify'] as const
38
36
 
39
37
  interface SyncOptions {
@@ -256,6 +254,7 @@ async function runSync(
256
254
  report(result, includeReferences)
257
255
 
258
256
  const mode = resolveWriteMode(opts)
257
+ const { GREEN, NC } = palette(process.stderr)
259
258
 
260
259
  if (result.totalChanges === 0) {
261
260
  // The stamp is a write like any other, so a run with no authority to write
@@ -1,12 +1,7 @@
1
1
  import { resolve } from 'node:path'
2
2
  import type { Command } from 'commander'
3
3
  import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
4
-
5
- const GREY = '\x1b[0;90m'
6
- const WHITE = '\x1b[1;37m'
7
- const RED = '\x1b[0;31m'
8
- const GREEN = '\x1b[0;32m'
9
- const NC = '\x1b[0m'
4
+ import { palette } from '@/ui'
10
5
 
11
6
  interface TranscriptOptions {
12
7
  out: string
@@ -24,6 +19,7 @@ export function register(program: Command): void {
24
19
  )
25
20
  .action(async (url: string, opts: TranscriptOptions) => {
26
21
  const outDir = resolve(process.cwd(), opts.out)
22
+ const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
27
23
  process.stderr.write(
28
24
  `${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}aitk transcripts${NC}\n`,
29
25
  )
@@ -9,6 +9,7 @@ import {
9
9
  logStep,
10
10
  logWarn,
11
11
  outro,
12
+ palette,
12
13
  select,
13
14
  } from '@/ui'
14
15
  import {
@@ -20,9 +21,6 @@ import {
20
21
  WIKI_INDEX_REL,
21
22
  } from '@/wiki/init'
22
23
 
23
- const GREEN = '\x1b[0;32m'
24
- const NC = '\x1b[0m'
25
-
26
24
  export function register(program: Command): void {
27
25
  const wiki = program
28
26
  .command('wiki')
@@ -59,6 +57,7 @@ async function runInit(target: string): Promise<number> {
59
57
  }
60
58
 
61
59
  const plan = planWikiInit(resolved)
60
+ const { GREEN, NC } = palette(process.stderr)
62
61
 
63
62
  logStep(`Scanning ${WIKI_DIR_REL}`)
64
63
  if (plan.changes.includes('dir')) logAdd(WIKI_DIR_REL)
@@ -0,0 +1,364 @@
1
+ import { access, readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { AUDITS } from '@/audits/catalog'
4
+ import { bodyLines } from '@/markdown/scan'
5
+
6
+ /**
7
+ * The record this measures, relative to the project root.
8
+ *
9
+ * One fixed path rather than a folder walk, because the standard governing it
10
+ * names one document, and the length rule this measures is stated by whichever
11
+ * record sits there rather than by the standard or by this file.
12
+ */
13
+ export const RECORD_REL = '.claude/ARCHITECTURE.md'
14
+
15
+ /**
16
+ * The line allowances a record states for itself, absent when it states none.
17
+ *
18
+ * No standard sets a length rule for this document, so the numbers belong to
19
+ * whichever record declares them rather than to the toolkit. Holding a pair in
20
+ * code and gating every project against it audits a target against a rule it
21
+ * never adopted, which is the failure `canResolveAtRoot` already answers on the
22
+ * folder side. A record stating no rule is measured and never gated.
23
+ */
24
+ export interface Allowances {
25
+ readonly frame: number
26
+ readonly perDecision: number
27
+ }
28
+
29
+ /**
30
+ * What a machine could do with the entry's reasoning, which is a candidate
31
+ * rather than a verdict.
32
+ *
33
+ * - `countable`: carries a figure over a tree, so a run could recompute it.
34
+ * - `invariant`: quantifies over a named tree, so a walk could falsify it.
35
+ * - `neither`: says why a choice was made, which nothing tests.
36
+ */
37
+ export type ClaimKind = 'countable' | 'invariant' | 'neither'
38
+
39
+ export interface DecisionReport {
40
+ readonly heading: string
41
+ /** Line of the `###` heading, so a report line opens at the entry. */
42
+ readonly line: number
43
+ readonly claim: ClaimKind
44
+ /** The digit-spelled figures behind a `countable` reading, in order. */
45
+ readonly figures: readonly string[]
46
+ /** The first sentence behind an `invariant` reading, absent otherwise. */
47
+ readonly quantified?: string
48
+ /**
49
+ * Executable checks the entry names, which is the only coverage signal the
50
+ * record carries. Empty on an entry that names none, including one whose
51
+ * claim some check happens to cover without the entry saying so.
52
+ */
53
+ readonly checks: readonly string[]
54
+ }
55
+
56
+ export interface ArchitectureReport {
57
+ readonly rel: string
58
+ readonly lines: number
59
+ /** What the record declared, absent when it states no length rule. */
60
+ readonly allowances?: Allowances
61
+ /** The frame plus the per-decision allowance, absent alongside it. */
62
+ readonly ceiling?: number
63
+ readonly decisions: readonly DecisionReport[]
64
+ }
65
+
66
+ const DECISION_HEADING = /^###\s+(.+?)\s*$/
67
+ const SECTION_HEADING = /^##\s+\S/
68
+ const CODE_SPAN = /`[^`]*`/g
69
+ /** Dropped ahead of the figure scan, since an anchor date is not a claim. */
70
+ const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
71
+ const FIGURE = /\b\d+(?:,\d{3})*\b/g
72
+ const SENTENCE_SPLIT = /(?<=[.])\s+/
73
+
74
+ /** A code span naming a path, which is what a quantifier has to govern. */
75
+ const PATH_SPAN = String.raw`\`[^\`]*(?:/|\.md|\.ts|\.sh|\.json)[^\`]*\``
76
+ const QUANTIFIER = String.raw`\b(?:every|each|no|nothing|any|all|never|only)\b`
77
+ /** Six words is the widest gap the corpus puts between the two. */
78
+ const WINDOW = String.raw`(?:\s+\S+){0,6}?\s+`
79
+
80
+ const QUANTIFIES_PATH = new RegExp(
81
+ `${QUANTIFIER}${WINDOW}${PATH_SPAN}|${PATH_SPAN}${WINDOW}${QUANTIFIER}`,
82
+ 'i',
83
+ )
84
+
85
+ /** A code span naming a shell check this repository could run. */
86
+ const SCRIPT_SPAN = /^scripts\/[\w./-]+\.sh$/
87
+ /** A code span invoking the CLI, which may or may not name a registered audit. */
88
+ const AITK_SPAN = /^aitk\s+(.+)$/
89
+
90
+ /**
91
+ * The audit invocations a decision could name, spelled as a reader writes them.
92
+ *
93
+ * Read off the catalog rather than listed, so a verb renamed at its source
94
+ * stops matching here instead of going on matching a string nobody maintains.
95
+ */
96
+ const AUDIT_INVOCATIONS: readonly string[] = AUDITS.map((audit) =>
97
+ audit.argv.filter((arg) => arg !== '--json').join(' '),
98
+ )
99
+
100
+ function codeSpans(text: string): string[] {
101
+ return (text.match(CODE_SPAN) ?? []).map((span) => span.slice(1, -1))
102
+ }
103
+
104
+ /**
105
+ * Names the executable checks an entry spells.
106
+ *
107
+ * A script has to exist on disk and an `aitk` invocation has to match a
108
+ * registered audit, because an entry naming a check that was removed is an
109
+ * uncovered claim wearing a covered one's words.
110
+ */
111
+ async function namedChecks(root: string, body: string): Promise<string[]> {
112
+ const spans = [...new Set(codeSpans(body))]
113
+
114
+ const verbs = spans.filter((span) => {
115
+ const invocation = span.match(AITK_SPAN)?.[1]
116
+ return invocation !== undefined && AUDIT_INVOCATIONS.includes(invocation)
117
+ })
118
+
119
+ const scripts = spans.filter((span) => SCRIPT_SPAN.test(span))
120
+ const present = await Promise.all(
121
+ scripts.map((span) =>
122
+ access(join(root, span)).then(
123
+ () => true,
124
+ () => false,
125
+ ),
126
+ ),
127
+ )
128
+
129
+ return [...verbs, ...scripts.filter((_, index) => present[index])]
130
+ }
131
+
132
+ /**
133
+ * Reads the figures a decision carries, which is the countable-claim signal.
134
+ *
135
+ * Digits alone. A cardinal spelled in words reads as pronominal far more often
136
+ * than as measured in this corpus, where "the alternative and it is one nobody
137
+ * passes" outnumbers "eleven copies", and admitting the spelled form classified
138
+ * 22 of 24 entries as countable, which distinguishes nothing. The cost is that
139
+ * a measured claim written in words reads as uncounted, which the report says.
140
+ */
141
+ function figuresIn(body: string): string[] {
142
+ return body.replace(ISO_DATE, ' ').replace(CODE_SPAN, ' ').match(FIGURE) ?? []
143
+ }
144
+
145
+ /**
146
+ * The first sentence quantifying over a named tree, or undefined.
147
+ *
148
+ * The quantifier has to sit within a short window of the path so a sentence
149
+ * mentioning both without relating them does not read as a claim about the
150
+ * tree. Both orders are matched, since the corpus writes the property before
151
+ * the path as readily as after it.
152
+ */
153
+ function quantifiedSentence(body: string): string | undefined {
154
+ return body
155
+ .replace(ISO_DATE, ' ')
156
+ .split(SENTENCE_SPLIT)
157
+ .map((sentence) => sentence.replace(/\n/g, ' ').trim())
158
+ .find((sentence) => QUANTIFIES_PATH.test(sentence))
159
+ }
160
+
161
+ /**
162
+ * Classifies one entry, with the countable reading taking precedence.
163
+ *
164
+ * An entry carrying both a figure and a quantified tree is recomputable, which
165
+ * is the stronger test, and the two readings are reported as one kind because
166
+ * the coverage split a reader wants is testable against unverifiable rather
167
+ * than a per-entry inventory of every claim in it.
168
+ */
169
+ export function classifyDecision(body: string): {
170
+ claim: ClaimKind
171
+ figures: string[]
172
+ quantified?: string
173
+ } {
174
+ const figures = figuresIn(body)
175
+ if (figures.length > 0) return { claim: 'countable', figures }
176
+
177
+ const quantified = quantifiedSentence(body)
178
+ if (quantified !== undefined) {
179
+ return { claim: 'invariant', figures: [], quantified }
180
+ }
181
+
182
+ return { claim: 'neither', figures: [] }
183
+ }
184
+
185
+ interface RawDecision {
186
+ readonly heading: string
187
+ readonly line: number
188
+ readonly body: string
189
+ }
190
+
191
+ /**
192
+ * Splits the record into its `###` entries.
193
+ *
194
+ * A heading inside a fenced block is skipped, since the seed template shows the
195
+ * shape it asks a project to write and a template entry is not a decision.
196
+ *
197
+ * A heading carrying several decisions counts once, so the total reads low by
198
+ * however many it holds. The report states that rather than parsing for it,
199
+ * because splitting a decision from its heading needs a marker the standard
200
+ * does not ask a record to carry.
201
+ */
202
+ export function splitDecisions(source: string): RawDecision[] {
203
+ const lines = bodyLines(source)
204
+ const decisions: RawDecision[] = []
205
+ let open: { heading: string; line: number; body: string[] } | undefined
206
+
207
+ for (const line of lines) {
208
+ if (line.fenced) {
209
+ open?.body.push(line.text)
210
+ continue
211
+ }
212
+
213
+ const heading = line.text.match(DECISION_HEADING)?.[1]
214
+ if (heading !== undefined) {
215
+ if (open) decisions.push({ ...open, body: open.body.join('\n') })
216
+ open = { heading, line: line.number, body: [] }
217
+ continue
218
+ }
219
+
220
+ // A decision runs to the next `###` or to the section that follows the
221
+ // decision list, so the risks below never read as the last entry's body.
222
+ if (SECTION_HEADING.test(line.text)) {
223
+ if (open) decisions.push({ ...open, body: open.body.join('\n') })
224
+ open = undefined
225
+ continue
226
+ }
227
+
228
+ open?.body.push(line.text)
229
+ }
230
+
231
+ if (open) decisions.push({ ...open, body: open.body.join('\n') })
232
+ return decisions
233
+ }
234
+
235
+ /** Cardinals a record spells rather than writes, which the corpus does for both. */
236
+ const SPELLED: Record<string, number> = {
237
+ one: 1,
238
+ two: 2,
239
+ three: 3,
240
+ four: 4,
241
+ five: 5,
242
+ six: 6,
243
+ seven: 7,
244
+ eight: 8,
245
+ nine: 9,
246
+ ten: 10,
247
+ }
248
+
249
+ const CARDINAL = String.raw`(\d+|${Object.keys(SPELLED).join('|')})`
250
+ const FRAME_CLAUSE = new RegExp(String.raw`${CARDINAL}-line frame`, 'i')
251
+ const PER_DECISION_CLAUSE = new RegExp(
252
+ String.raw`${CARDINAL}\s+lines?\s+a\s+decision`,
253
+ 'i',
254
+ )
255
+
256
+ function readCardinal(token: string | undefined): number | undefined {
257
+ if (token === undefined) return undefined
258
+ const spelled = SPELLED[token.toLowerCase()]
259
+ if (spelled !== undefined) return spelled
260
+
261
+ const digits = Number.parseInt(token, 10)
262
+ return Number.isNaN(digits) ? undefined : digits
263
+ }
264
+
265
+ /**
266
+ * Reads the allowances a record declares for itself, or nothing.
267
+ *
268
+ * Both clauses have to be present, because half a formula is not one. A record
269
+ * whose wording drifts past these clauses falls back to reporting rather than
270
+ * to a stale ceiling held here, so the failure is visible in the run's own
271
+ * output instead of gating a project on a rule nobody can point at.
272
+ */
273
+ export function readAllowances(source: string): Allowances | undefined {
274
+ const frame = readCardinal(source.match(FRAME_CLAUSE)?.[1])
275
+ const perDecision = readCardinal(source.match(PER_DECISION_CLAUSE)?.[1])
276
+
277
+ if (frame === undefined || perDecision === undefined) return undefined
278
+ return { frame, perDecision }
279
+ }
280
+
281
+ /** Whether a read failed because nothing sits at the path. */
282
+ function isMissing(error: unknown): boolean {
283
+ const code = (error as { code?: unknown }).code
284
+ return code === 'ENOENT' || code === 'ENOTDIR'
285
+ }
286
+
287
+ /** The ceiling the record's own formula derives from its decision count. */
288
+ export function ceilingFor(allowances: Allowances, decisions: number): number {
289
+ return allowances.frame + allowances.perDecision * decisions
290
+ }
291
+
292
+ /**
293
+ * Measures the record, or reports nothing when the project carries none.
294
+ *
295
+ * Absent rather than empty, for the reason the sibling checks state: a project
296
+ * with no record and one whose record holds no decision are different answers,
297
+ * and a zeroed report reads as the second.
298
+ */
299
+ export async function measureArchitecture(
300
+ root: string,
301
+ ): Promise<ArchitectureReport | undefined> {
302
+ const path = join(root, RECORD_REL)
303
+
304
+ let source: string
305
+ try {
306
+ source = await readFile(path, 'utf8')
307
+ } catch (error) {
308
+ // Only a record that is not there reads as absent. A record present and
309
+ // unreadable propagates the way every sibling reader here lets one
310
+ // propagate, since swallowing it reports a project with no record and the
311
+ // length gate passes over a file nobody opened.
312
+ if (!isMissing(error)) throw error
313
+ return undefined
314
+ }
315
+
316
+ const allowances = readAllowances(source)
317
+ const raw = splitDecisions(source)
318
+ const decisions = await Promise.all(
319
+ raw.map(async (entry) => {
320
+ const { claim, figures, quantified } = classifyDecision(entry.body)
321
+ return {
322
+ heading: entry.heading,
323
+ line: entry.line,
324
+ claim,
325
+ figures,
326
+ ...(quantified !== undefined && { quantified }),
327
+ checks: await namedChecks(root, entry.body),
328
+ }
329
+ }),
330
+ )
331
+
332
+ return {
333
+ rel: RECORD_REL,
334
+ lines: source.replace(/\n$/, '').split('\n').length,
335
+ ...(allowances !== undefined && {
336
+ allowances,
337
+ ceiling: ceilingFor(allowances, raw.length),
338
+ }),
339
+ decisions,
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Whether the record is longer than the ceiling it derives for itself.
345
+ *
346
+ * False for a record declaring no allowances, which has no ceiling to be past.
347
+ * That is the answer rather than a gap, since the length rule is the record's
348
+ * own and a project that never wrote one owes nothing to it.
349
+ */
350
+ export function isOverLength(report: ArchitectureReport): boolean {
351
+ return report.ceiling !== undefined && report.lines > report.ceiling
352
+ }
353
+
354
+ /** How many entries carry a claim a machine could test. */
355
+ export function testableCount(report: ArchitectureReport): number {
356
+ return report.decisions.filter((entry) => entry.claim !== 'neither').length
357
+ }
358
+
359
+ /** How many testable entries name a check that exists. */
360
+ export function coveredCount(report: ArchitectureReport): number {
361
+ return report.decisions.filter(
362
+ (entry) => entry.claim !== 'neither' && entry.checks.length > 0,
363
+ ).length
364
+ }
@@ -4,6 +4,15 @@ import type { FolderDrift } from '@/context/index-drift'
4
4
  export interface GateInput {
5
5
  /** Cited paths that resolved to nothing, which gate under either mode. */
6
6
  readonly unresolvedCitations: number
7
+ /**
8
+ * Whether the architecture record is longer than the ceiling it derives for
9
+ * itself, which gates under either mode for the reason a citation does.
10
+ *
11
+ * False when the project carries no record and false under
12
+ * `--citations-only`, which never measures it. That mode runs one check by
13
+ * construction, so widening it here would gate on a reading it never took.
14
+ */
15
+ readonly recordOverLength: boolean
7
16
  readonly sections: readonly SectionFinding[]
8
17
  readonly drift: readonly FolderDrift[]
9
18
  /**
@@ -24,19 +33,24 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
24
33
  /**
25
34
  * Whether the audit found something that should fail the caller.
26
35
  *
27
- * An unresolved citation is a broken pointer and gates unconditionally. The two
28
- * findings `--gate` adds are the ones answerable from the file itself: a
29
- * required section it does not declare, and an index disagreeing with its
30
- * folder. Length, depth, bullet, table, and provenance findings are thresholds
31
- * a reader weighs, so they stay out under both modes.
36
+ * An unresolved citation is a broken pointer and gates unconditionally, and so
37
+ * does a record past its own ceiling: the record states the limit for itself
38
+ * and derives it from a count, which makes it the one measure here that is a
39
+ * fact rather than a threshold a reader weighs. The two findings `--gate` adds
40
+ * are the ones answerable from the file itself: a required section it does not
41
+ * declare, and an index disagreeing with its folder. Entry length, depth,
42
+ * bullet, table, provenance, and the record's claim coverage are judgments, so
43
+ * they stay out under both modes.
32
44
  */
33
45
  export function isGating({
34
46
  unresolvedCitations,
47
+ recordOverLength,
35
48
  sections,
36
49
  drift,
37
50
  widened,
38
51
  }: GateInput): boolean {
39
52
  if (unresolvedCitations > 0) return true
53
+ if (recordOverLength) return true
40
54
  if (!widened) return false
41
55
 
42
56
  return sections.length > 0 || hasDrift(drift)
@@ -1,6 +1,19 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
 
3
- export type Row = Record<string, string>
3
+ /**
4
+ * One table cell, split into the value a renderer emits and whether the record
5
+ * marked it as unsourced.
6
+ *
7
+ * The marker sits inside the cell rather than in a trailing column, because a
8
+ * trailing marker breaks the table parse. Splitting it out here is what keeps a
9
+ * swatch or a font sample built from the value alone.
10
+ */
11
+ export interface Cell {
12
+ value: string
13
+ tagged: boolean
14
+ }
15
+
16
+ export type Row = Record<string, Cell>
4
17
 
5
18
  export interface DesignDoc {
6
19
  personality: string
@@ -12,6 +25,11 @@ export interface DesignDoc {
12
25
  iconography: string
13
26
  }
14
27
 
28
+ const VERIFY_TAG = /\s*\?\s*verify\s*$/
29
+
30
+ /** A cell whose whole content is one balanced code span and nothing else. */
31
+ const CODE_SPAN = /^`([^`]*)`$/
32
+
15
33
  export function parseDesignDoc(path: string): DesignDoc {
16
34
  const raw = readFileSync(path, 'utf8')
17
35
  const sections = splitSections(raw)
@@ -58,22 +76,41 @@ function table(body: string | undefined): Row[] {
58
76
  if (!body) return []
59
77
  const rows = body.split('\n').filter((l) => l.trim().startsWith('|'))
60
78
  if (rows.length < 2) return []
61
- const headers = splitRow(rows[0])
79
+ const headers = splitRow(rows[0]).map((c) => c.value)
62
80
  const data = rows.slice(2)
63
81
  return data.map((line) => {
64
82
  const cells = splitRow(line)
65
83
  const row: Row = {}
66
84
  headers.forEach((h, i) => {
67
- row[h] = (cells[i] ?? '').trim()
85
+ row[h] = cells[i] ?? emptyCell()
68
86
  })
69
87
  return row
70
88
  })
71
89
  }
72
90
 
73
- function splitRow(line: string): string[] {
91
+ function emptyCell(): Cell {
92
+ return { value: '', tagged: false }
93
+ }
94
+
95
+ function splitRow(line: string): Cell[] {
74
96
  return line
75
97
  .replace(/^\s*\|/, '')
76
98
  .replace(/\|\s*$/, '')
77
99
  .split('|')
78
- .map((c) => c.trim().replace(/\s*\?\s*verify\s*$/, ''))
100
+ .map(parseCell)
101
+ }
102
+
103
+ /**
104
+ * The tag is tested against the cell with any surrounding code span removed,
105
+ * since a value wrapping itself in backticks puts one after the tag and an
106
+ * end-anchored test misses it there. The span is restored around the clean
107
+ * value so an untagged cell and a tagged one carry the same formatting.
108
+ */
109
+ function parseCell(raw: string): Cell {
110
+ const trimmed = raw.trim()
111
+ const span = trimmed.match(CODE_SPAN)
112
+ const inner = span ? span[1] : trimmed
113
+ if (!VERIFY_TAG.test(inner)) return { value: trimmed, tagged: false }
114
+ const value = inner.replace(VERIFY_TAG, '')
115
+ return { value: span ? `\`${value}\`` : value, tagged: true }
79
116
  }