@dzhechkov/skills-feature-adr 1.5.8 → 1.5.11

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/.dz-manifest.json CHANGED
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "README.md",
16
- "sha256": "dc985b443d8ac9d4091b09367330f7f610c35572d99f635855989fd893c5e6ac"
16
+ "sha256": "889d62c5dfafb53c0664867f0882e5712e47eb8aa7a44c6db46d082e693ed58d"
17
17
  },
18
18
  {
19
19
  "path": "bin/cli.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "path": "package.json",
28
- "sha256": "d57357d1feb9f111d0674f1c307a4d15a41cb005074351d37ebaf7dc1afe3575"
28
+ "sha256": "d13930ae60a1d849d08b798dbb3b3ed4fb5d0b50c4c3910fc76a455dd9e6ce31"
29
29
  },
30
30
  {
31
31
  "path": "src/cli.js",
@@ -249,7 +249,11 @@
249
249
  },
250
250
  {
251
251
  "path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
252
- "sha256": "630eb9047258ad0333082f407cd34522e4c278650f22ef6dba1ef897ec5cce71"
252
+ "sha256": "cbe70a524f6ce7987b5e3961ee412b92c24c00d84066d438c759659d656efaa2"
253
+ },
254
+ {
255
+ "path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
256
+ "sha256": "82c3c48c25f400f3a64d1caba3d47357f3db87460f0746f88706f901eb6ff689"
253
257
  },
254
258
  {
255
259
  "path": "templates/.claude/skills/frontend-design/LICENSE.txt",
@@ -313,7 +317,7 @@
313
317
  },
314
318
  {
315
319
  "path": "templates/.claude/workflows/feature-adr.js",
316
- "sha256": "f287a73bdf8c11c99eedbc071c6babb3201a6cec7d644d65f20ece3cd4ca51c1"
320
+ "sha256": "22416f105c1b988c07f6cb9002a507b54b07842b5b4bb109b7e300ff52272603"
317
321
  },
318
322
  {
319
323
  "path": "templates/lib/memory-protocol.md",
@@ -325,5 +329,5 @@
325
329
  }
326
330
  ]
327
331
  },
328
- "signature": "t3rY38fu7zb8GjsIjG0OOeqtw04W4nlzawE//PhPKbHbtLUMgT/1SOaXFUWEyjQvNTb13dCJXrHggR0yf/5RBA=="
332
+ "signature": "WT7wArJY3aKuIMMVgbBzFO+Rqw7qfcaVh8UQ9MENSltqdTJTFAr/0TBOS5A89UAfczkluCrkTtyUvPOTQvaNCw=="
329
333
  }
package/README.md CHANGED
@@ -106,6 +106,26 @@ ARCHITECTURE → IMPLEMENTATION → CODE → QE → FLEET QE
106
106
  # Full protocols + 6 extra skills, up to 7 fleet QE agents
107
107
  ```
108
108
 
109
+ ### Advisory micro-recall at two decision points (v1.5.9, staged)
110
+
111
+ The workflow makes one bounded decision-local recall attempt immediately before the live Step 3
112
+ ADR-alternative choice and one immediately before the live Step 6 plan-route choice. Each attempt
113
+ asks for at most three lessons, uses a decision-specific lesson-bandit context, has a 15-second
114
+ whole-attempt ceiling, and is never retried. The Step 6 Codex-to-Claude fallback reuses the same
115
+ snapshot instead of recalling again.
116
+
117
+ This is strictly advisory. Empty output, timeout, command/parse/transport failure, an unwritable
118
+ receipt, or an unestablished application probe leaves the original stage prompt, dispatch count,
119
+ result, and verdict unchanged. There is no hard gate or runtime effectiveness threshold.
120
+
121
+ The append-only evidence lives at
122
+ `features/<slug>/.fa-state/decision-recall.jsonl`: versioned `entered`, `recalled`, `applied`, and
123
+ optional `owner-label` rows. Post-hoc analysis can derive `receipted / eligible`, outcome counts,
124
+ `applied / selected`, owner-label coverage, repeat-hit numerator/denominator, irrelevant injections,
125
+ and explicit uncounted/unknown populations. The motivating timing observation remains external
126
+ `[SRC], n=1`; the required book queries were silent on retrieval timing, so this instrumentation is
127
+ a falsifiable hypothesis, not a claim that decision-point recall is needed or effective here.
128
+
109
129
  ### The K2 plan gate runs on ANY repo, not just JS/TS ones (v1.5.0)
110
130
 
111
131
  Between Step 6 and Step 7 the pipeline runs a machine plan-completeness gate (K2). Until v1.5.0 it
@@ -325,14 +345,17 @@ Workflow({ scriptPath: '.claude/workflows/feature-adr.js',
325
345
  ```
326
346
 
327
347
  Omit the Codex knobs entirely for today's all-Claude behavior. The run result reports
328
- `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` / `modelsUsed` so you can see who did what.
329
-
330
- **Live model visibility in `/workflows`:** each stage's agent label is decorated with its *resolved*
331
- model — e.g. `adr · codex:gpt-5.5:xhigh`, `plan · opus` — so you see which model actually ran a stage in
332
- the live progress tree, not just in the final report. This matters because a Codex stage's auto
333
- model-badge shows the `codex:codex-rescue` Claude wrapper (the session model), never `codex`; the label
334
- text is the honest signal. A Claude fallback appears as its own distinct node (never mislabeled as Codex),
335
- and a routing-off run adds no suffix (byte-identical to today).
348
+ `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` / `modelsUsed` as a legacy routing
349
+ summary. `dispatchOutcomes` is the authoritative who-did-what ledger: it contains only settled
350
+ attempts, never pre-dispatch intent.
351
+
352
+ **Live model visibility in `/workflows`:** each stage prints a two-phase line. `▸ ... · intent`
353
+ appears before the target model starts and names the resolved model and selection reason;
354
+ `◆ ... · outcome: ...` appears only after that attempt settles. Only the latter enters `dispatchOutcomes`.
355
+ This matters because a Codex stage's auto
356
+ model-badge shows the `codex:codex-rescue` Claude wrapper (the session model), never `codex`; the
357
+ two-phase lines are the honest signal. A Claude fallback appears as its own distinct attempt (never
358
+ mislabeled as Codex), and a routing-off intent explicitly says `routing not requested`.
336
359
 
337
360
  ### Per-stage model routing — `args.models`
338
361
 
@@ -675,7 +698,7 @@ sufficiency + honesty, overengineering, silent decisions, runtime consistency, s
675
698
  `dz challenge --plan <plan.md>` or the `challenge-panel` skill; scaffold the degradations registry via
676
699
  `dz feature-adr-setup --from-spec <spec with {"degradations":true}> --apply`.
677
700
 
678
- ### ADR quality gate (Step 3 generates → Step 8 enforces)
701
+ ### ADR quality review + Confirmation file gate (Step 3 generates → Step 8 checks)
679
702
 
680
703
  Step 3 and Step 8 share an ADR best-practices contract distilled from the
681
704
  [architecture-decision-record monograph](https://github.com/architecture-decision-record/architecture-decision-record):
@@ -686,12 +709,17 @@ Step 3 and Step 8 share an ADR best-practices contract distilled from the
686
709
  links + after-action review, a **`## Confirmation`** stanza (method, monitoring, success metric, owner)
687
710
  naming the load-bearing property, and a **`## Links`** traceability block. Template weight is tier-mapped:
688
711
  S/M → Nygard/ITD-lightweight, L/XL → MADR + Confirmation.
689
- - **Step 8** runs a **13-point ADR fitness checklist** (`qe-code-reviewer`) against every generated ADR and
690
- **fails the gate** on any miss — decision-shaped title, controlled-vocabulary Status + reversibility,
712
+ - **Step 8** runs a **13-point advisory ADR fitness checklist** (`qe-code-reviewer`) against every generated ADR —
713
+ decision-shaped title, controlled-vocabulary Status + reversibility,
691
714
  neutral Context-before-Decision, symmetric options, driver-mapped rationale, concrete/testable decision,
692
715
  negative consequences, traceability links, no placeholder, and **rejects explainer-masquerading-as-ADR**.
693
- The **Confirmation→test link is load-bearing**: if the named safety property has no automated test the ADR
694
- grades no better than C.
716
+ Those judgment-based items remain findings; they do not independently force the workflow verdict.
717
+ - The **one mandatory gate** runs after Step 7.5 and before the QE verdict: every test-file path named
718
+ under an ADR heading beginning with `## Confirmation` must exist as a readable regular file. Missing
719
+ files or unreadable/unparseable paths force a non-passing Step-8 grade while the independent QE review
720
+ still runs. A feature with no ADR prints `пропущено: ADR нет, проверять нечего` and is not failed.
721
+ `dz discrimination-check` and `dz mutation-gate` remain advisory: existence does not prove that a test
722
+ actually discriminates the load-bearing property.
695
723
 
696
724
  The pipeline **dog-foods** this: a harness test runs the gate against feature-adr's own generated ADR, so a
697
725
  Step-3↔Step-8 drift fails CI rather than shipping.
@@ -1113,6 +1141,24 @@ deliberately excluded — "skeleton first" is nonsense there.
1113
1141
 
1114
1142
  ## Status
1115
1143
 
1144
+ `1.5.10` — **staged, not published: every finished phase reports to the 📐 live panel, whatever the
1145
+ checkpoint does.** The packaged workflow's `withCheckpoint` computes the next phase label from STAGE
1146
+ COMPLETION alone, before any checkpoint branch, so `args.checkpoints:false`, an oversize or
1147
+ unserializable result, a stage with no declared artifact, and a refused persist predicate all still
1148
+ move the panel. On the common path the `dz statusline --fa-record` command RIDES the existing
1149
+ ckpt-write agent — **zero new agents** — joined by `;`, never `&&`: with `&&`, a checkpoint write
1150
+ that legitimately refuses short-circuits the phase report away while the workflow has already
1151
+ recorded that it ran, and the panel silently stops on a run that is still progressing. Only where no
1152
+ checkpoint agent is dispatched at all does the stage spend one extra effort-low `fa-phase:<stage>`
1153
+ dispatch, announced in the log. A dead or partial stage reports nothing — the panel must never claim
1154
+ progress a stage did not make. Requires `@dzhechkov/harness-cli >= 0.8.11` for the `--tier` flag and
1155
+ the second-line panel; the two workflow twins stay byte-identical (`gen-loop-blobs --check`).
1156
+
1157
+ `1.5.9` — **staged, not published: advisory decision-point micro-recall.** Step 3 ADR selection and
1158
+ Step 6 plan routing each make one top-3, 15-second, no-retry attempt inside the live checkpoint thunk.
1159
+ All recall/receipt/probe failures preserve the original dispatch. Versioned `.fa-state` receipts make
1160
+ coverage and repeat-related outcomes countable offline; no threshold controls the workflow.
1161
+
1116
1162
  `1.5.4` — **Step 5 now asks how the shipped feature will be watched.** The architecture artifact must
1117
1163
  carry a section headed exactly `Observability` answering what the feature logs, what it counts, what a
1118
1164
  failure looks like from outside, and who would notice. **"Nothing to observe" is a complete answer** —
@@ -1160,3 +1206,21 @@ Also in this release, both halves of the K2 plan-completeness gate that field us
1160
1206
  installed in the workspace was never found (`NOT-ESTABLISHED`, exit 3, the coding step never ran).
1161
1207
  New `args.workspace` pins it, the shipped call site passes it, each candidate is now labelled in the
1162
1208
  audit line, and a `K2_GATE_NOTE` fires when the two collapse onto one path.
1209
+
1210
+ ## Двухфазная строка диспатча (шаблон воркфлоу)
1211
+
1212
+ Шаблон `.claude/workflows/feature-adr.js` печатает две разные строки без флага: намерение
1213
+ `▸ <стадия> · <модель> · <причина выбора> · intent` непосредственно до запуска модели и итог
1214
+ `◆ <стадия> · <модель> · outcome: <исход>` после окончательного состояния той же ступени.
1215
+ Даже долгий или бросивший исключение запуск заранее виден человеку; при этом только итог попадает в
1216
+ `dispatchOutcomes` отчёта. Причина отказа остаётся у outcome отказавшей ступени, а intent следующего
1217
+ fallback получает нейтральную причину `fallback-rung`. Регион `stage-line` принадлежит генератору
1218
+ `gen-loop-blobs` и не правится руками.
1219
+
1220
+ ### Shared Markdown masking in feature-adr gates
1221
+
1222
+ The standalone plan-completeness gate ships with `markdown-masker.mjs`, copied byte-for-byte from
1223
+ harness-core's `src/markdown-masker.ts`. It runs without a core build. Amendment checks, swarm briefs
1224
+ and K2 share the parser while retaining their existing unclosed-block and indentation policies.
1225
+ The four-space indented-code gap remains open for amendment checks and K2; swarm briefs retain their
1226
+ existing masking of indented code. Versions are unchanged in this staged change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.5.8",
3
+ "version": "1.5.11",
4
4
  "description": "Adaptive Feature Development skill pack for Claude Code — 11-step pipeline with Complexity Router (S/M/L/XL), ADR-driven architecture, 15 agentic-qe skills, multi-agent fleet QE. Supports --full-qe, --full-qe-extended, --with-learning, and --knowledge-extractor modes.",
5
5
  "bin": {
6
6
  "skills-feature-adr": "./bin/cli.js"
package/sbom.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "dc985b443d8ac9d4091b09367330f7f610c35572d99f635855989fd893c5e6ac"
38
+ "content": "889d62c5dfafb53c0664867f0882e5712e47eb8aa7a44c6db46d082e693ed58d"
39
39
  }
40
40
  ]
41
41
  },
@@ -69,7 +69,7 @@
69
69
  },
70
70
  {
71
71
  "name": "dz:canonical-json-sha256-v2",
72
- "value": "d57357d1feb9f111d0674f1c307a4d15a41cb005074351d37ebaf7dc1afe3575"
72
+ "value": "d13930ae60a1d849d08b798dbb3b3ed4fb5d0b50c4c3910fc76a455dd9e6ce31"
73
73
  }
74
74
  ]
75
75
  },
@@ -629,7 +629,17 @@
629
629
  "hashes": [
630
630
  {
631
631
  "alg": "SHA-256",
632
- "content": "630eb9047258ad0333082f407cd34522e4c278650f22ef6dba1ef897ec5cce71"
632
+ "content": "cbe70a524f6ce7987b5e3961ee412b92c24c00d84066d438c759659d656efaa2"
633
+ }
634
+ ]
635
+ },
636
+ {
637
+ "type": "file",
638
+ "name": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
639
+ "hashes": [
640
+ {
641
+ "alg": "SHA-256",
642
+ "content": "82c3c48c25f400f3a64d1caba3d47357f3db87460f0746f88706f901eb6ff689"
633
643
  }
634
644
  ]
635
645
  },
@@ -789,7 +799,7 @@
789
799
  "hashes": [
790
800
  {
791
801
  "alg": "SHA-256",
792
- "content": "f287a73bdf8c11c99eedbc071c6babb3201a6cec7d644d65f20ece3cd4ca51c1"
802
+ "content": "22416f105c1b988c07f6cb9002a507b54b07842b5b4bb109b7e300ff52272603"
793
803
  }
794
804
  ]
795
805
  },
@@ -51,6 +51,7 @@
51
51
  // the feature's own `00_complexity_assessment.md` acid-case table (rows shaped `| A<N> | … |`), or
52
52
  // supplied explicitly with `--acid=T1,T2,…`. If neither establishes a corpus, C4 is SKIPPED-with-note
53
53
  // (a feature that declared no acid cases cannot be failed for not naming them).
54
+ import { maskMarkdown } from './markdown-masker.mjs';
54
55
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
55
56
  import { isAbsolute, join, resolve } from 'node:path';
56
57
 
@@ -180,7 +181,21 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
180
181
  const adr = readFileSync(join(adrDir, f), 'utf-8');
181
182
  const confIdx = adr.search(/^##+\s*Confirmation/mi);
182
183
  if (confIdx < 0) { failures.push(`C2: ${safe(f)} has no Confirmation section`); continue; }
183
- const conf = adr.slice(confIdx);
184
+ // The section ENDS at the next heading of the same or higher level — it does not run to EOF.
185
+ // Slicing to EOF swallowed every later section, so a `Links` entry citing an existing test as
186
+ // PRECEDENT was read as this ADR's own Confirmation and demanded of the plan (measured
187
+ // 2026-08-31 on features/finding-identity-closure: reqe.test.ts, cited under
188
+ // "Implementation precedent", failed C2 while the real Confirmation test was named correctly).
189
+ // Deeper subsections stay inside: a Confirmation with ### children is one section.
190
+ const rest = adr.slice(confIdx);
191
+ const confLevel = (rest.match(/^(#+)/) || ['', '##'])[1].length;
192
+ // Search AFTER the Confirmation heading's own line — searching from the heading itself matches
193
+ // it and collapses the section to nothing, which turns a scoping fix into a gate that reports
194
+ // "Confirmation names no test file paths" for every ADR (caught by running it).
195
+ const bodyStart = rest.indexOf('\n') + 1;
196
+ const afterHeading = rest.slice(bodyStart);
197
+ const nextHeading = afterHeading.search(new RegExp('^#{1,' + confLevel + '}[^#]', 'm'));
198
+ const conf = nextHeading < 0 ? rest : rest.slice(0, bodyStart + nextHeading);
184
199
  const paths = extractCandidatePaths(conf).filter(isTestPath);
185
200
  // UNKNOWN ECOSYSTEM STAYS A FAILURE, never a WARN: a gate that downgrades itself on the one
186
201
  // repo it cannot read is a gate that is off exactly where it is needed. The remedy is named
@@ -305,21 +320,7 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
305
320
  // `## Amendments` could become the section heading, and a fenced example row could either open a
306
321
  // phantom amendment or hand a real testless one someone else's marker. Third fence-blindness
307
322
  // found in a checker today, so it is closed here by construction rather than by care.
308
- const rawLines = plan.split('\n');
309
- const planLines = [];
310
- {
311
- let fence = null;
312
- for (const line of rawLines) {
313
- const open = /^ {0,3}(```+|~~~+)/.exec(line);
314
- if (fence === null && open) { fence = open[1][0]; planLines.push(''); continue; }
315
- if (fence !== null) {
316
- planLines.push('');
317
- if (new RegExp('^ {0,3}' + fence + '{3,}\\s*$').test(line)) fence = null;
318
- continue;
319
- }
320
- planLines.push(line);
321
- }
322
- }
323
+ const planLines = maskMarkdown(plan, { unclosed: 'mask' }).split('\n');
323
324
  let sectionStart = -1, sectionEnd = -1, cursor = 0;
324
325
  for (const pl of planLines) {
325
326
  // The SAME heading shape amendment-trace.ts accepts: up to three leading spaces, two to four
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Canonical Markdown block masker; copied byte-for-byte as markdown-masker.mjs beside K2.
3
+ * Keep this file valid JavaScript (inferred TS types, no build needed by the copy).
4
+ *
5
+ * CommonMark fences and type-2 HTML comments; same UTF-16 length and newline positions.
6
+ * Inline code cannot open an HTML block. HTML delimiters within a block are consumed left to
7
+ * right, including close/reopen on one line. This is not a complete CommonMark parser.
8
+ *
9
+ * Reader policies are deliberate: amendment-trace restores unclosed blocks; brief/K2 hide them.
10
+ * Four-space code is still unsupported by default. ONLY brief keeps its pre-existing policy.
11
+ * Containers, tab indentation and HTML block types other than comments remain unsupported.
12
+ * Callbacks expose line facts; callers own semantic diagnostics and list-barrier representation.
13
+ */
14
+ export function maskMarkdown(md = '', {
15
+ unclosed = 'restore',
16
+ indentedCode = false,
17
+ inlineComments = false,
18
+ onMasked = (_line = 0) => {},
19
+ onDisputed = (_line = 0) => {},
20
+ } = {}) {
21
+ const lines = md.split('\n');
22
+ const out = lines.slice();
23
+ const masked = lines.map(() => false);
24
+ let state = 'text';
25
+ let marker = '';
26
+ let markerLength = 0;
27
+ let openedAt = -1;
28
+ let nestedOpener = false;
29
+ let disputed = false;
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const line = lines[i] ?? '';
32
+ const blank = () => { out[i] = ' '.repeat(line.length); masked[i] = true; };
33
+ if (state === 'fence') {
34
+ blank();
35
+ const close = /^ {0,3}(`{3,}|~{3,})\s*$/.exec(line);
36
+ if (close) {
37
+ const run = close[1] ?? '';
38
+ if (run[0] === marker && run.length >= markerLength) {
39
+ state = 'text';
40
+ openedAt = -1;
41
+ }
42
+ }
43
+ continue;
44
+ }
45
+ if (state === 'text') {
46
+ const fence = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/.exec(line);
47
+ if (fence && !(fence[1]?.[0] === '`' && fence[2]?.includes('`'))) {
48
+ const run = fence[1] ?? '';
49
+ state = 'fence';
50
+ marker = run[0] ?? '';
51
+ markerLength = run.length;
52
+ openedAt = i;
53
+ blank();
54
+ continue;
55
+ }
56
+ if (indentedCode && /^ {4,}/.test(line)) {
57
+ blank();
58
+ continue;
59
+ }
60
+ }
61
+ // Outside HTML only a block opener counts: code-spanned delimiters in prose are inert.
62
+ // Inside HTML, backticks are literal and do not protect a delimiter.
63
+ const htmlStart = /^ {0,3}<!--/.test(line);
64
+ const wasDisputed = disputed;
65
+ // Ambiguity is diagnostic state, never permission to scan otherwise-visible prose.
66
+ // Brief deliberately keeps that diagnostic across blocks until the author's outer close;
67
+ // block-only readers must not hide an unrelated arrow or opener because it is pending.
68
+ if (state === 'html' || htmlStart || inlineComments) {
69
+ let maskLine = state === 'html' || htmlStart;
70
+ const tokens = [...line.matchAll(/`+|<!--|-->/g)];
71
+ for (let t = 0; t < tokens.length; t++) {
72
+ const token = tokens[t];
73
+ if (!token) continue;
74
+ if (token[0][0] === '`') {
75
+ // Code spans require an EXACT matching run; a shorter embedded run is literal.
76
+ // HTML blocks treat backticks literally. Inline-comment support is brief's existing
77
+ // policy; other readers only enter this scan at a block opener or while disputed.
78
+ if (state !== 'html') {
79
+ const end = tokens.findIndex((next, j) => j > t && next[0] === token[0]);
80
+ if (end >= 0) t = end;
81
+ }
82
+ continue;
83
+ }
84
+ if (token[0] === '<!--') {
85
+ if (state === 'html') nestedOpener = true;
86
+ else { state = 'html'; openedAt = i; }
87
+ maskLine = true;
88
+ } else if (state === 'html') {
89
+ state = 'text';
90
+ openedAt = -1;
91
+ if (nestedOpener) { disputed = true; nestedOpener = false; }
92
+ } else if (disputed) {
93
+ disputed = false;
94
+ maskLine = true;
95
+ }
96
+ }
97
+ if (maskLine) blank();
98
+ }
99
+ if (wasDisputed || disputed) onDisputed(i);
100
+ }
101
+ if (openedAt >= 0 && unclosed === 'restore') {
102
+ for (let i = openedAt; i < lines.length; i++) {
103
+ out[i] = lines[i] ?? '';
104
+ masked[i] = false;
105
+ }
106
+ }
107
+ for (let i = 0; i < masked.length; i++) if (masked[i]) onMasked(i);
108
+ return out.join('\n');
109
+ }