@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 +9 -5
- package/README.md +77 -13
- package/package.json +1 -1
- package/sbom.json +14 -4
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +17 -16
- package/templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs +109 -0
- package/templates/.claude/workflows/feature-adr.js +1651 -232
package/.dz-manifest.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "README.md",
|
|
16
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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`
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
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
|
|
690
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|