@dzhechkov/keysarium 1.7.4 → 1.7.6
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 +13 -9
- package/README.md +16 -1
- package/bin/cli.js +0 -0
- package/package.json +4 -4
- package/sbom.json +18 -8
- package/templates/.claude/skills/feature-adr/modules/00-complexity-router.md +21 -1
- package/templates/.claude/skills/feature-adr/modules/06-implementation-plan.md +13 -6
- package/templates/.claude/skills/feature-adr/modules/07-code.md +1 -0
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +45 -0
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +127 -18
- package/templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs +109 -0
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
package/.dz-manifest.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "README.md",
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "3b4f6570d8cebd26429409ed23da99ddd082c378946cf722fe62d5b405e288c2"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "README.ru.md",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "package.json",
|
|
20
|
-
"sha256": "
|
|
20
|
+
"sha256": "67d60a0ad58316ec7c0cd264683d6d17e5fe7f33c746f151b95c0def597037d6"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"path": "src/cli.js",
|
|
@@ -353,7 +353,7 @@
|
|
|
353
353
|
},
|
|
354
354
|
{
|
|
355
355
|
"path": "templates/.claude/skills/feature-adr/modules/00-complexity-router.md",
|
|
356
|
-
"sha256": "
|
|
356
|
+
"sha256": "6a98ca7af49aabcb2e14a081815f1167740ac4dba2e0cbb938c87aceac47bfa3"
|
|
357
357
|
},
|
|
358
358
|
{
|
|
359
359
|
"path": "templates/.claude/skills/feature-adr/modules/01-requirements.md",
|
|
@@ -381,15 +381,15 @@
|
|
|
381
381
|
},
|
|
382
382
|
{
|
|
383
383
|
"path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
|
|
384
|
-
"sha256": "
|
|
384
|
+
"sha256": "5c8d4c79d5329702b8c75b9afb36ae335aa63c99e74045c0afa98e12658ee975"
|
|
385
385
|
},
|
|
386
386
|
{
|
|
387
387
|
"path": "templates/.claude/skills/feature-adr/modules/07-code.md",
|
|
388
|
-
"sha256": "
|
|
388
|
+
"sha256": "fb054591e86d55cee184eab96b5f0440053ada8fa9edf619e4a66ec5ac371587"
|
|
389
389
|
},
|
|
390
390
|
{
|
|
391
391
|
"path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
|
|
392
|
-
"sha256": "
|
|
392
|
+
"sha256": "7973676cc9a5a6d8429c0bcdbe163bb5dc16e2d2f28a72698e44ad72d234e199"
|
|
393
393
|
},
|
|
394
394
|
{
|
|
395
395
|
"path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
|
|
@@ -481,7 +481,11 @@
|
|
|
481
481
|
},
|
|
482
482
|
{
|
|
483
483
|
"path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
|
|
484
|
-
"sha256": "
|
|
484
|
+
"sha256": "08e69d2e63350fe2dc64483226fcd30901b77a8e930ca6280ef71181c67675b4"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
|
|
488
|
+
"sha256": "82c3c48c25f400f3a64d1caba3d47357f3db87460f0746f88706f901eb6ff689"
|
|
485
489
|
},
|
|
486
490
|
{
|
|
487
491
|
"path": "templates/.claude/skills/frontend-design/LICENSE.txt",
|
|
@@ -669,7 +673,7 @@
|
|
|
669
673
|
},
|
|
670
674
|
{
|
|
671
675
|
"path": "templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md",
|
|
672
|
-
"sha256": "
|
|
676
|
+
"sha256": "d9dd399cbb60d6b5fa9734ad44128a49790ef15bbde25c809d69d5c918601eb3"
|
|
673
677
|
},
|
|
674
678
|
{
|
|
675
679
|
"path": "templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md",
|
|
@@ -985,5 +989,5 @@
|
|
|
985
989
|
}
|
|
986
990
|
]
|
|
987
991
|
},
|
|
988
|
-
"signature": "
|
|
992
|
+
"signature": "Ld7xkqWgfHmJp0Xv+wKYPylP5opGZmCfibsIDam0NS90aZ6R9oLfJVeRljITB25umc7/zHZMW1MWLbMornUgDQ=="
|
|
989
993
|
}
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Full 7-phase pipeline for AI case studies, hackathons, and casariums. Provides s
|
|
|
18
18
|
> the teach protocol. See `skills/goap-research-ed25519/SKILL.md`.
|
|
19
19
|
|
|
20
20
|
## Growth engine (reverse-engineering-unicorn M5)
|
|
21
|
-
### What M5 produces, since 0.2.0 / 1.7.
|
|
21
|
+
### What M5 produces, since 0.2.0 / 1.7.5 / 1.6.0
|
|
22
22
|
|
|
23
23
|
M5 no longer ends as analysis. Three changes, in the order they matter:
|
|
24
24
|
|
|
@@ -450,6 +450,13 @@ changes. See `@dzhechkov/skills-feature-adr@1.5.3`.
|
|
|
450
450
|
|
|
451
451
|
`1.5.38` — a fix to the bundled `feature-adr` gate script in `templates/`, no pipeline change.
|
|
452
452
|
|
|
453
|
+
The bundled K2 plan-completeness gate (staged, not yet versioned or published) now also checks
|
|
454
|
+
**requirement coverage (C8)** — every id declared in `01_requirements.md` must be referenced by the plan (WARN with
|
|
455
|
+
a count by default, per-id FAIL under `--require-requirements`, which the bundled pipeline passes) — counts ADR
|
|
456
|
+
decisions by in-file heading rather than by filename, and treats an unclosed code fence in a declaration input as
|
|
457
|
+
NOT-ESTABLISHED. The bundled workflow gained ONE bracketed plan-repair round with a proven restore. See
|
|
458
|
+
`@dzhechkov/skills-feature-adr` for the full account.
|
|
459
|
+
|
|
453
460
|
The bundled K2 plan-completeness gate now tells an **ABSENT** `00_complexity_assessment.md` from a
|
|
454
461
|
deliberate skip: a missing file is a WARNING that names the missing artifact, while a present file
|
|
455
462
|
carrying no acid table is an honest SKIP. Both used to report as a clean skip, so the acid check
|
|
@@ -458,3 +465,11 @@ carried that file and the last four in a row did not.
|
|
|
458
465
|
|
|
459
466
|
`1.5.40` — the bundled `feature-adr` Step-8 module now describes the amendment gate as the command
|
|
460
467
|
`dz amendment-check` instead of a judgement the reviewer is asked to make. Text only; no pipeline change.
|
|
468
|
+
|
|
469
|
+
### Shared Markdown masking in feature-adr gates
|
|
470
|
+
|
|
471
|
+
The standalone plan-completeness gate ships with `markdown-masker.mjs`, copied byte-for-byte from
|
|
472
|
+
harness-core's `src/markdown-masker.ts`. It runs without a core build. Amendment checks, swarm briefs
|
|
473
|
+
and K2 share the parser while retaining their existing unclosed-block and indentation policies.
|
|
474
|
+
The four-space indented-code gap remains open for amendment checks and K2; swarm briefs retain their
|
|
475
|
+
existing masking of indented code. Versions are unchanged in this staged change.
|
package/bin/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/keysarium",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
4
|
+
"scripts": {},
|
|
4
5
|
"description": "AI Case Research Toolkit for Claude Code — full 7-phase pipeline with feature-adr, dream cycles, background workers, multi-platform support, reward learning, and Cloud.ru AI Factory mapping (--ai-factory flag)",
|
|
5
6
|
"main": "src/cli.js",
|
|
6
7
|
"bin": {
|
|
@@ -51,6 +52,5 @@
|
|
|
51
52
|
},
|
|
52
53
|
"publishConfig": {
|
|
53
54
|
"access": "public"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/sbom.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hashes": [
|
|
16
16
|
{
|
|
17
17
|
"alg": "SHA-256",
|
|
18
|
-
"content": "
|
|
18
|
+
"content": "3b4f6570d8cebd26429409ed23da99ddd082c378946cf722fe62d5b405e288c2"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"name": "dz:canonical-json-sha256-v2",
|
|
52
|
-
"value": "
|
|
52
|
+
"value": "67d60a0ad58316ec7c0cd264683d6d17e5fe7f33c746f151b95c0def597037d6"
|
|
53
53
|
}
|
|
54
54
|
]
|
|
55
55
|
},
|
|
@@ -889,7 +889,7 @@
|
|
|
889
889
|
"hashes": [
|
|
890
890
|
{
|
|
891
891
|
"alg": "SHA-256",
|
|
892
|
-
"content": "
|
|
892
|
+
"content": "6a98ca7af49aabcb2e14a081815f1167740ac4dba2e0cbb938c87aceac47bfa3"
|
|
893
893
|
}
|
|
894
894
|
]
|
|
895
895
|
},
|
|
@@ -959,7 +959,7 @@
|
|
|
959
959
|
"hashes": [
|
|
960
960
|
{
|
|
961
961
|
"alg": "SHA-256",
|
|
962
|
-
"content": "
|
|
962
|
+
"content": "5c8d4c79d5329702b8c75b9afb36ae335aa63c99e74045c0afa98e12658ee975"
|
|
963
963
|
}
|
|
964
964
|
]
|
|
965
965
|
},
|
|
@@ -969,7 +969,7 @@
|
|
|
969
969
|
"hashes": [
|
|
970
970
|
{
|
|
971
971
|
"alg": "SHA-256",
|
|
972
|
-
"content": "
|
|
972
|
+
"content": "fb054591e86d55cee184eab96b5f0440053ada8fa9edf619e4a66ec5ac371587"
|
|
973
973
|
}
|
|
974
974
|
]
|
|
975
975
|
},
|
|
@@ -979,7 +979,7 @@
|
|
|
979
979
|
"hashes": [
|
|
980
980
|
{
|
|
981
981
|
"alg": "SHA-256",
|
|
982
|
-
"content": "
|
|
982
|
+
"content": "7973676cc9a5a6d8429c0bcdbe163bb5dc16e2d2f28a72698e44ad72d234e199"
|
|
983
983
|
}
|
|
984
984
|
]
|
|
985
985
|
},
|
|
@@ -1209,7 +1209,17 @@
|
|
|
1209
1209
|
"hashes": [
|
|
1210
1210
|
{
|
|
1211
1211
|
"alg": "SHA-256",
|
|
1212
|
-
"content": "
|
|
1212
|
+
"content": "08e69d2e63350fe2dc64483226fcd30901b77a8e930ca6280ef71181c67675b4"
|
|
1213
|
+
}
|
|
1214
|
+
]
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
"type": "file",
|
|
1218
|
+
"name": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
|
|
1219
|
+
"hashes": [
|
|
1220
|
+
{
|
|
1221
|
+
"alg": "SHA-256",
|
|
1222
|
+
"content": "82c3c48c25f400f3a64d1caba3d47357f3db87460f0746f88706f901eb6ff689"
|
|
1213
1223
|
}
|
|
1214
1224
|
]
|
|
1215
1225
|
},
|
|
@@ -1679,7 +1689,7 @@
|
|
|
1679
1689
|
"hashes": [
|
|
1680
1690
|
{
|
|
1681
1691
|
"alg": "SHA-256",
|
|
1682
|
-
"content": "
|
|
1692
|
+
"content": "d9dd399cbb60d6b5fa9734ad44128a49790ef15bbde25c809d69d5c918601eb3"
|
|
1683
1693
|
}
|
|
1684
1694
|
]
|
|
1685
1695
|
},
|
|
@@ -103,19 +103,39 @@ Runs after tier classification, only when `{AGENTIC_QE_MODE}` = `direct` | `dire
|
|
|
103
103
|
|
|
104
104
|
Step 1 folds `{LEARNED_PATTERNS}` into the requirements brief as "lessons from previous features" — advisory context only, never requirements themselves. Why here: Step 0 is the one point where recall can be keyed to the feature's phase and domain — per-prompt hook auto-injection cannot see which pipeline step is running.
|
|
105
105
|
|
|
106
|
+
### 7. Task Kind Classification (experiment-envelope, ADR-001)
|
|
107
|
+
|
|
108
|
+
Classify the feature as exactly ONE of the six task kinds below, and write `Task kind: <x>` as its
|
|
109
|
+
own line in the artifact. This feeds the experiment envelope the conveyor builds after this step —
|
|
110
|
+
downstream learning reads the kind, not a paraphrase, so the line must use one of the exact words.
|
|
111
|
+
|
|
112
|
+
| Task kind | Criterion |
|
|
113
|
+
|-----------|-----------|
|
|
114
|
+
| `feature` | A genuinely new capability, adapter, command, or skill did not exist before this run. |
|
|
115
|
+
| `bugfix` | The change corrects observed incorrect behavior in existing code — a defect, not an absence. |
|
|
116
|
+
| `refactor` | Behavior is unchanged; the change restructures, renames, or simplifies existing code. |
|
|
117
|
+
| `tooling` | The change is to build/CI/dev-tooling/scripts rather than to the product's own runtime behavior. |
|
|
118
|
+
| `docs` | The change is documentation-only (README, ADR prose, comments) with no code delta. |
|
|
119
|
+
| `research` | The deliverable is a finding or a design decision, not a shipped code change. |
|
|
120
|
+
|
|
121
|
+
If `args.taskKind` was passed explicitly by the caller, it OVERRIDES this classification — record
|
|
122
|
+
both: `Task kind: <forced value> (forced by the caller)` as the kind of record, and your own
|
|
123
|
+
classification separately if it differs, the same pattern Step 0 already uses for a forced tier.
|
|
124
|
+
|
|
106
125
|
## Output
|
|
107
126
|
|
|
108
127
|
Set the following variables:
|
|
109
128
|
|
|
110
129
|
```
|
|
111
130
|
{COMPLEXITY_TIER} = S | M | L | XL
|
|
131
|
+
{TASK_KIND} = feature | bugfix | refactor | tooling | docs | research
|
|
112
132
|
{ACTIVE_STEPS} = [0, 1, 6, 7, 8] # example for S
|
|
113
133
|
{TIME_BUDGET} = { requirements: 2, planning: 0, implementation: 10, qe: 3 }
|
|
114
134
|
{DIMENSION_SCORES} = { files: 1, domains: 1, integrations: 1, breaking: 1, models: 1, crosscutting: 1 }
|
|
115
135
|
{LEARNED_PATTERNS} = [ ... ] # top-3 by confidence from memory_query; [] in reference mode / no hits / error
|
|
116
136
|
```
|
|
117
137
|
|
|
118
|
-
Create artifact: `features/<slug>/00_complexity_assessment.md`
|
|
138
|
+
Create artifact: `features/<slug>/00_complexity_assessment.md`, including the `Task kind: <x>` line.
|
|
119
139
|
|
|
120
140
|
## Checkpoint 0 Format
|
|
121
141
|
|
|
@@ -127,7 +127,9 @@ For each group, identify:
|
|
|
127
127
|
|
|
128
128
|
Before finalizing the plan, validate completeness:
|
|
129
129
|
|
|
130
|
-
1. Cross-reference every `{REQUIREMENT}` (FR-N) → at least one TASK covers it
|
|
130
|
+
1. Cross-reference every `{REQUIREMENT}` (FR-N) → at least one TASK covers it (C8 — the gate checks
|
|
131
|
+
this by identifier: WARN with a count by default, FAIL under `--require-requirements`, which the
|
|
132
|
+
pipeline passes)
|
|
131
133
|
2. Cross-reference every `{ADR_DECISION}` → at least one TASK implements it
|
|
132
134
|
3. Cross-reference every critical risk from `{QUALITY_RISKS}` → mitigation in some TASK
|
|
133
135
|
4. Name every acid token `A<n>` from `00_complexity_assessment.md` VERBATIM in the plan (C4), each
|
|
@@ -233,11 +235,16 @@ C2 recognises JS/TS, pytest, Go, Rust, JVM and .NET test paths, extensible per p
|
|
|
233
235
|
|
|
234
236
|
Never proceed on a non-zero exit, and never treat empty output as a pass — the last line
|
|
235
237
|
(`K2 plan-completeness: PASS|FAIL|NOT-ESTABLISHED`) is the verdict, and its absence is not one.
|
|
236
|
-
What it checks: C1 every ADR
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
What it checks: C1 every ADR **decision** (a `# ADR-NNN` / `## ADR-NNN` heading INSIDE the file, not
|
|
239
|
+
just the filename prefix — a file with several headings owes several plan citations) has a plan task
|
|
240
|
+
citing it · C2 every ADR Confirmation test path is named in the plan · C3 the `EXPECTED_CODE_TARGETS:`
|
|
241
|
+
block parses line by line · C4 the feature's declared acid corpus is named · C5 (WARN) the
|
|
242
|
+
`Inputs read:` line · C8 every requirement id declared in `01_requirements.md` (`FR-N`, `NFR-N`,
|
|
243
|
+
`AC-N`, `C-N`) is cited by the plan by word boundary — WARN with a count by default, FAIL per missing
|
|
244
|
+
id under `--require-requirements` (the pipeline passes this flag; a bare interactive run of the
|
|
245
|
+
script does not). An S-tier run with no `03_adr/` skips C1/C2 with a note (it cannot be failed for
|
|
246
|
+
ADRs it never had) — unless the plan itself cites `ADR-<n>`, which is NOT-ESTABLISHED. C1 is a grep:
|
|
247
|
+
it catches "forgot entirely", not "mentioned but not tasked".
|
|
241
248
|
|
|
242
249
|
Pass the run's tier so the check cannot be dodged: `--tier=S|M|L|XL`. An M/L/XL feature with no
|
|
243
250
|
`03_adr/` FAILS C1/C2 (an M+ feature owes ADRs); only `--tier=S` — or no tier at all, and then the
|
|
@@ -20,6 +20,7 @@ opus (complex code generation)
|
|
|
20
20
|
- `{ADR_DECISIONS}` from Step 3 (M+)
|
|
21
21
|
- `{DOMAIN_MODEL}` from Step 4 (L/XL)
|
|
22
22
|
- Codebase context (existing patterns, conventions)
|
|
23
|
+
- {LEARNED_PATTERNS} for Step 7 — the decision-recall block (≤3 lessons), appended to the coder prompt by the pipeline
|
|
23
24
|
|
|
24
25
|
## Protocol
|
|
25
26
|
|
|
@@ -311,6 +311,51 @@ Compile all findings into a structured report:
|
|
|
311
311
|
✅ READY FOR MERGE | ❌ NEEDS FIXES | ⚠️ CONDITIONAL APPROVAL
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
+
### 7.1 Findings ledger (machine-readable)
|
|
315
|
+
|
|
316
|
+
Prose is for people; `dz score`/`dz recap` need a machine-readable surface too (qe-findings-record,
|
|
317
|
+
ADR-001). Write BOTH of these into `08_qe_report.md`, in addition to the prose report above:
|
|
318
|
+
|
|
319
|
+
1. Exactly ONE line, in the PROSE body of the file (never inside a fenced code block, an indented
|
|
320
|
+
code block, a `>` blockquote, or an HTML comment — the parser masks all four before it looks):
|
|
321
|
+
`QE-VERDICT: <A|A-|A+|B|B+|B-|C|C+|C-|D>` — the letter grade you gave above, machine-parseable,
|
|
322
|
+
ASCII hyphen for the sign (the parser also accepts U+2212 as the same sign; write ASCII). Two such
|
|
323
|
+
lines make the report `ambiguous`, never "last wins" — write it once. A line that starts with
|
|
324
|
+
`QE-VERDICT` but gets the grammar wrong (wrong dash, trailing prose, wrong case, no colon) is worse
|
|
325
|
+
than writing nothing: the parser reports `invalid` and REFUSES to fall back to guessing a grade
|
|
326
|
+
from prose — get the one line right rather than close.
|
|
327
|
+
2. A `## Findings ledger` section — this exact heading, occurring EXACTLY ONCE in the file, at the
|
|
328
|
+
top level (never inside a fence/indented block/blockquote/comment either) — with a table directly
|
|
329
|
+
under it, under this EXACT header (copy it verbatim):
|
|
330
|
+
|
|
331
|
+
```markdown
|
|
332
|
+
## Findings ledger
|
|
333
|
+
|
|
334
|
+
| Finding | Severity | Status | Round | Author | Title |
|
|
335
|
+
|---------|----------|--------|-------|--------|-------|
|
|
336
|
+
| F1 | HIGH | fixed | 1 | codex | escaping bug in the token scanner |
|
|
337
|
+
| F2 | MEDIUM | open | 1 | lead | style nit, not blocking |
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
A table found ANYWHERE else — before the heading, after the section ends at the next heading, with
|
|
341
|
+
no heading in the file, or with the heading duplicated — is REFUSED as `outside ledger section`,
|
|
342
|
+
never parsed as the real ledger; a second table found INSIDE the section is REFUSED as `duplicate
|
|
343
|
+
table`. Both refusals name how many of the rejected table's rows were ignored.
|
|
344
|
+
|
|
345
|
+
Closed dictionaries — a row using anything else is REFUSED by the parser, never coerced to the
|
|
346
|
+
nearest known value:
|
|
347
|
+
- **Severity**: `BLOCKER` `CRITICAL` `HIGH` `MEDIUM` `LOW` `INFO`
|
|
348
|
+
- **Status**: `confirmed` `fixed` `partial` `refuted` `named-limit` `open`
|
|
349
|
+
- **Round**: an integer ≥ 1 (which review round raised it)
|
|
350
|
+
- **Author**: `codex` `claude` `lead`
|
|
351
|
+
- **Finding**: a short id token with no spaces (`F1`, `R2-3`, …); **Title**: free text — a literal
|
|
352
|
+
`|` inside Title must be escaped as `\|`, or wrapped in inline code (`` `a|b` ``), or it fragments
|
|
353
|
+
the row into extra columns and the row is refused as malformed.
|
|
354
|
+
|
|
355
|
+
An empty (header-only) table is read as `hollow: true` — worse than no table at all, because it
|
|
356
|
+
claims a ledger exists and says nothing. If there are no findings, omit the section entirely rather
|
|
357
|
+
than writing an empty table.
|
|
358
|
+
|
|
314
359
|
### 8. QE Pattern Store (Direct Mode only)
|
|
315
360
|
|
|
316
361
|
When `{AGENTIC_QE_MODE}` = `direct` | `direct-extended`, after the gap loop is closed and the verdict is set:
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
// Generalized from features/wave1-instrument-repair/check-plan-completeness.mjs (that copy is the
|
|
4
4
|
// historical artifact of its run and stays untouched); this one is parameterized by feature dir.
|
|
5
5
|
//
|
|
6
|
-
// USAGE: node .claude/skills/feature-adr/scripts/check-plan-completeness.mjs [<feature-dir>] [--tier=M] [--acid=A1,A2]
|
|
6
|
+
// USAGE: node .claude/skills/feature-adr/scripts/check-plan-completeness.mjs [<feature-dir>] [--tier=M] [--acid=A1,A2] [--require-requirements]
|
|
7
7
|
// <feature-dir> defaults to the current working directory.
|
|
8
8
|
// --tier=S|M|L|XL closes the ADR-less dodge (see S-TIER HONESTY); omitting it keeps the
|
|
9
9
|
// heuristic, and the skip note then names the dodge out loud.
|
|
10
|
+
// --require-requirements promotes C8 (below) from a WARN-with-count to a per-id FAIL; the
|
|
11
|
+
// pipeline passes this flag, so the check is introduced two-shot (ADR-001 plan-inherits-
|
|
12
|
+
// requirements): WARN first so the corpus can be measured without repainting every green
|
|
13
|
+
// fixture red, FAIL once the planner prompt names the contract (feature-adr.js Step 6).
|
|
10
14
|
//
|
|
11
15
|
// VERDICT CONTRACT (unchanged from the proven copy — never a silent pass):
|
|
12
16
|
// PASS exit 0 last line: `K2 plan-completeness: PASS (...)`
|
|
@@ -21,6 +25,12 @@
|
|
|
21
25
|
// 3dbd2851-adjacent) must parse task structure. Kept honest here: C1 catches "forgot entirely",
|
|
22
26
|
// not "mentioned but not tasked".
|
|
23
27
|
//
|
|
28
|
+
// KNOWN LIMITATION (fix round 1, 2026-09-16, same class as C1 above): C8 is also a grep — a PROSE
|
|
29
|
+
// mention of "FR-3" satisfies it exactly like a task reference. Kept honest here too: C8 catches
|
|
30
|
+
// "forgot entirely", not "mentioned but not tasked". Masking the PLAN side (not just the 01/ADR
|
|
31
|
+
// side) for C1 and C8 together, so a prose mention stops satisfying either check, is a separate
|
|
32
|
+
// backlog item — filed by the lead, not chased here.
|
|
33
|
+
//
|
|
24
34
|
// Checks:
|
|
25
35
|
// C1 every ADR file in 03_adr/ has >=1 task line in 06_implementation_plan.md citing it (ADR-00N)
|
|
26
36
|
// C2 every Confirmation-numbered check in each ADR is named in the plan (by its test-file path)
|
|
@@ -29,6 +39,23 @@
|
|
|
29
39
|
// — SFDIPOT condition: line-level validation, reject-with-reason, not just block presence
|
|
30
40
|
// C4 the plan names the feature's OWN acid corpus (see "acid corpus" below)
|
|
31
41
|
// C5 the plan has an 'Inputs read:' line naming 03_adr, 05_architecture (wave-2 seam, cheap here)
|
|
42
|
+
// C8 every requirement id DECLARED in 01_requirements.md (FR-N, NFR-N, AC-N, C-N, with an optional
|
|
43
|
+
// letter group FR-AN and an optional fraction FR-N.N) is CITED by the plan, by word boundary —
|
|
44
|
+
// set difference over identifiers, exactly like C1, never text similarity. WARN with a count by
|
|
45
|
+
// default; FAIL per missing id under --require-requirements (see USAGE above). 01 absent or
|
|
46
|
+
// declaring no ids in the four contract shapes ⇒ WARN, same honesty discipline as C4's absent
|
|
47
|
+
// 00_complexity_assessment.md.
|
|
48
|
+
// NAMED LIMIT (C-1, measured over 394 corpus files 2026-09-16): forms outside FR|NFR|AC|C — bare
|
|
49
|
+
// `RN`/`QN` registries, an id with no hyphen (`FR1`) — are NOT caught. Minority forms in the
|
|
50
|
+
// corpus; widening the regex "just in case" would manufacture false WARN/FAIL on real plans, so
|
|
51
|
+
// the limit is named here rather than chased.
|
|
52
|
+
// NAMED LIMIT (fix round 1, 2026-09-16): the `C-N` shape is read as a Constraint requirement id
|
|
53
|
+
// by this regex — the repo convention this gate trusts. A defect table uses `D-N` (`| D1 |`),
|
|
54
|
+
// never `C-N`; a table of open defects mislabeled with `C-N` rows would be misread as
|
|
55
|
+
// requirement ids. That is a corpus-naming-convention limit, not a bug this gate works around.
|
|
56
|
+
// Declared ids are read from 01 THROUGH `maskMarkdown` (fenced code and HTML comments stripped
|
|
57
|
+
// first, same reader C6 already uses below), so an id quoted inside an example or a comment is
|
|
58
|
+
// never mistaken for a declaration.
|
|
32
59
|
//
|
|
33
60
|
// S-TIER HONESTY (no 03_adr/): an S-tier run legitimately has no ADR files, and forcing it to fail a
|
|
34
61
|
// plan gate it can never satisfy would make the gate a nuisance to route around. So:
|
|
@@ -51,6 +78,7 @@
|
|
|
51
78
|
// the feature's own `00_complexity_assessment.md` acid-case table (rows shaped `| A<N> | … |`), or
|
|
52
79
|
// supplied explicitly with `--acid=T1,T2,…`. If neither establishes a corpus, C4 is SKIPPED-with-note
|
|
53
80
|
// (a feature that declared no acid cases cannot be failed for not naming them).
|
|
81
|
+
import { maskMarkdown } from './markdown-masker.mjs';
|
|
54
82
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
55
83
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
56
84
|
|
|
@@ -59,9 +87,11 @@ const acidArg = argv.find((a) => a.startsWith('--acid='));
|
|
|
59
87
|
const tierArg = argv.find((a) => a.startsWith('--tier='));
|
|
60
88
|
const TIER = tierArg ? tierArg.slice('--tier='.length).trim().toUpperCase() : null;
|
|
61
89
|
const TIER_REQUIRES_ADR = TIER === 'M' || TIER === 'L' || TIER === 'XL';
|
|
90
|
+
const REQUIRE_REQUIREMENTS = argv.includes('--require-requirements');
|
|
62
91
|
const dirArg = argv.find((a) => !a.startsWith('--'));
|
|
63
92
|
const FDIR = resolve(dirArg && dirArg !== '' ? (isAbsolute(dirArg) ? dirArg : join(process.cwd(), dirArg)) : process.cwd());
|
|
64
93
|
const planPath = join(FDIR, '06_implementation_plan.md');
|
|
94
|
+
const requirementsPath = join(FDIR, '01_requirements.md');
|
|
65
95
|
const adrDir = join(FDIR, '03_adr');
|
|
66
96
|
const complexityPath = join(FDIR, '00_complexity_assessment.md');
|
|
67
97
|
|
|
@@ -77,6 +107,23 @@ const out = (s) => console.log(s);
|
|
|
77
107
|
const notEstablished = (why) => { out(`K2 plan-completeness: NOT-ESTABLISHED — ${safe(why)}`); process.exit(3); };
|
|
78
108
|
let failures = [], warnings = [], skips = [];
|
|
79
109
|
|
|
110
|
+
// Lead delta after Codex rounds 2+3 (2026-09-16, plan-inherits-requirements): on the DECLARATION side
|
|
111
|
+
// (01_requirements.md ids, ADR headings) an UNCLOSED fence is AMBIGUOUS INPUT — CommonMark reads it as
|
|
112
|
+
// code to EOF, a human reads it as prose that forgot a backtick. Round 2 measured that masking it
|
|
113
|
+
// silently DROPPED every id after it (FR-2 vanished, no line said so); round 3 showed that restoring
|
|
114
|
+
// it silently ESTABLISHED a heading that was only example code. Neither silent reading is honest, so
|
|
115
|
+
// the gate does what it does for every other input it cannot read: NOT-ESTABLISHED, naming the file —
|
|
116
|
+
// "close the fence" is a one-character fix, and the corpus has ZERO such files today (MEASURED
|
|
117
|
+
// 2026-09-16 11:58 UTC over 831 requirements/ADR files). The detector is the two masking modes
|
|
118
|
+
// disagreeing: an unclosed fence with an empty or whitespace-only tail changes no id and needs no
|
|
119
|
+
// verdict — a NAMED LIMIT of the proxy, not a gap in it.
|
|
120
|
+
function maskDeclarations(text, label) {
|
|
121
|
+
const strict = maskMarkdown(text, { unclosed: 'mask' });
|
|
122
|
+
const restored = maskMarkdown(text, { unclosed: 'restore' });
|
|
123
|
+
if (strict !== restored) notEstablished(`${label} has an UNCLOSED code fence — an ambiguous declaration input establishes nothing (close the fence and rerun)`);
|
|
124
|
+
return strict;
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
if (!existsSync(FDIR)) notEstablished(`feature dir absent: ${FDIR}`);
|
|
81
128
|
if (!existsSync(planPath)) notEstablished('06_implementation_plan.md absent');
|
|
82
129
|
const plan = readFileSync(planPath, 'utf-8');
|
|
@@ -165,9 +212,35 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
|
|
|
165
212
|
skips.push(`C1: no 03_adr/ and the plan claims no ADR work — ADR-coverage check SKIPPED${TIER === 'S' ? ' (--tier=S, the legitimate S-tier shape)' : ' (NO --tier supplied: an M/L/XL run that simply never wrote 03_adr/ would dodge C1/C2 here — pass --tier to close it)'}`);
|
|
166
213
|
skips.push('C2: no 03_adr/ — Confirmation-test coverage check SKIPPED');
|
|
167
214
|
} else {
|
|
168
|
-
// C1 — ADR ids referenced by plan tasks
|
|
215
|
+
// C1 — ADR ids referenced by plan tasks (FR-4). Decisions are counted by HEADING inside the file
|
|
216
|
+
// (`# ADR-NNN` / `## ADR-NNN`, multiline), not by the filename prefix: a file named `001-004-*.md`
|
|
217
|
+
// that actually contains four decisions used to be checked as ONE. A file with no such heading
|
|
218
|
+
// falls back to the old filename-prefix path, WARNed so the fallback is never silent.
|
|
219
|
+
// Fix round 1 (2026-09-16): the file is read THROUGH `maskMarkdown` first (fenced code and HTML
|
|
220
|
+
// comments blanked, positions preserved) — a heading printed inside a ```-fenced example is not a
|
|
221
|
+
// real decision. Up to 3 leading spaces of indentation are tolerated (CommonMark's own limit for a
|
|
222
|
+
// line to still be a heading; 4+ spaces is indented code and is already blanked by the mask).
|
|
223
|
+
// NAMED LIMIT: a nested `### ADR-002` heading cited INSIDE ADR-001's own file as a cross-reference
|
|
224
|
+
// (rather than living in ADR-002's own file) is still counted as a decision owed a task — a corpus
|
|
225
|
+
// rarity, and telling "own decision" from "cross-reference" needs semantic parsing, the same class
|
|
226
|
+
// of limit the C1 grep above already accepts.
|
|
227
|
+
const ADR_HEADING_RE = /^ {0,3}#{1,6}\s*ADR-(\d+)\b/gm;
|
|
169
228
|
for (const f of adrFiles) {
|
|
229
|
+
let adrTextForHeadings = null;
|
|
230
|
+
try { adrTextForHeadings = maskDeclarations(readFileSync(join(adrDir, f), 'utf-8'), `C1: ${safe(f)}`); } catch { adrTextForHeadings = null; }
|
|
231
|
+
const headingNums = adrTextForHeadings === null
|
|
232
|
+
? []
|
|
233
|
+
: [...new Set([...adrTextForHeadings.matchAll(ADR_HEADING_RE)].map((mm) => mm[1]))];
|
|
234
|
+
if (headingNums.length > 0) {
|
|
235
|
+
for (const n of headingNums) {
|
|
236
|
+
const id = `ADR-${n}`;
|
|
237
|
+
const re = new RegExp(`ADR-0*${Number(n)}\\b`);
|
|
238
|
+
if (!re.test(plan)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it`);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
170
242
|
const m = f.match(/^(\d{3})-/); if (!m) { warnings.push(`C1: unparseable ADR filename ${safe(f)}`); continue; }
|
|
243
|
+
warnings.push(`C1: ${safe(f)} has no ADR-NNN heading — falling back to the filename prefix`);
|
|
171
244
|
const id = `ADR-${m[1]}`;
|
|
172
245
|
const re = new RegExp(`ADR-0*${Number(m[1])}\\b`);
|
|
173
246
|
if (!re.test(plan)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it`);
|
|
@@ -180,7 +253,21 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
|
|
|
180
253
|
const adr = readFileSync(join(adrDir, f), 'utf-8');
|
|
181
254
|
const confIdx = adr.search(/^##+\s*Confirmation/mi);
|
|
182
255
|
if (confIdx < 0) { failures.push(`C2: ${safe(f)} has no Confirmation section`); continue; }
|
|
183
|
-
|
|
256
|
+
// The section ENDS at the next heading of the same or higher level — it does not run to EOF.
|
|
257
|
+
// Slicing to EOF swallowed every later section, so a `Links` entry citing an existing test as
|
|
258
|
+
// PRECEDENT was read as this ADR's own Confirmation and demanded of the plan (measured
|
|
259
|
+
// 2026-08-31 on features/finding-identity-closure: reqe.test.ts, cited under
|
|
260
|
+
// "Implementation precedent", failed C2 while the real Confirmation test was named correctly).
|
|
261
|
+
// Deeper subsections stay inside: a Confirmation with ### children is one section.
|
|
262
|
+
const rest = adr.slice(confIdx);
|
|
263
|
+
const confLevel = (rest.match(/^(#+)/) || ['', '##'])[1].length;
|
|
264
|
+
// Search AFTER the Confirmation heading's own line — searching from the heading itself matches
|
|
265
|
+
// it and collapses the section to nothing, which turns a scoping fix into a gate that reports
|
|
266
|
+
// "Confirmation names no test file paths" for every ADR (caught by running it).
|
|
267
|
+
const bodyStart = rest.indexOf('\n') + 1;
|
|
268
|
+
const afterHeading = rest.slice(bodyStart);
|
|
269
|
+
const nextHeading = afterHeading.search(new RegExp('^#{1,' + confLevel + '}[^#]', 'm'));
|
|
270
|
+
const conf = nextHeading < 0 ? rest : rest.slice(0, bodyStart + nextHeading);
|
|
184
271
|
const paths = extractCandidatePaths(conf).filter(isTestPath);
|
|
185
272
|
// UNKNOWN ECOSYSTEM STAYS A FAILURE, never a WARN: a gate that downgrades itself on the one
|
|
186
273
|
// repo it cannot read is a gate that is off exactly where it is needed. The remedy is named
|
|
@@ -305,21 +392,7 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
305
392
|
// `## Amendments` could become the section heading, and a fenced example row could either open a
|
|
306
393
|
// phantom amendment or hand a real testless one someone else's marker. Third fence-blindness
|
|
307
394
|
// 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
|
-
}
|
|
395
|
+
const planLines = maskMarkdown(plan, { unclosed: 'mask' }).split('\n');
|
|
323
396
|
let sectionStart = -1, sectionEnd = -1, cursor = 0;
|
|
324
397
|
for (const pl of planLines) {
|
|
325
398
|
// The SAME heading shape amendment-trace.ts accepts: up to three leading spaces, two to four
|
|
@@ -431,6 +504,42 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
431
504
|
}
|
|
432
505
|
}
|
|
433
506
|
|
|
507
|
+
// ── C8 requirement-id coverage (FR-1/2/3, ADR-001 plan-inherits-requirements) ───────────────────
|
|
508
|
+
// Set difference over IDENTIFIERS, exactly like C1 — never text similarity. Declared id forms are
|
|
509
|
+
// measured from the corpus (see the USAGE-block comment above for the named C-1 limit): `FR-N`,
|
|
510
|
+
// `NFR-N`, `AC-N`, `C-N`, an optional letter group (`FR-AN`) and an optional fraction (`FR-N.N`),
|
|
511
|
+
// declared at the START of a line as a heading, a bold run, a list item, or a table cell.
|
|
512
|
+
{
|
|
513
|
+
const REQ_ID_DECL_RE = /^\s*(?:#{1,6}\s*|[-*]\s+|\|\s*)?\**\s*((?:FR|NFR|AC|C)-[A-Z]?\d+(?:\.\d+)?)\b/gm;
|
|
514
|
+
const escapeReqId = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
515
|
+
if (!existsSync(requirementsPath)) {
|
|
516
|
+
warnings.push('C8: 01_requirements.md is ABSENT — requirement coverage had no input to read');
|
|
517
|
+
} else {
|
|
518
|
+
// Fix round 1 (2026-09-16): masked THROUGH maskMarkdown first, exactly like C1's heading scan —
|
|
519
|
+
// an id inside a ```-fenced example or an HTML comment is not a declaration.
|
|
520
|
+
const requirementsText = maskDeclarations(readFileSync(requirementsPath, 'utf-8'), 'C8: 01_requirements.md');
|
|
521
|
+
const reqIds = [...new Set([...requirementsText.matchAll(REQ_ID_DECL_RE)].map((mm) => mm[1]))];
|
|
522
|
+
if (reqIds.length === 0) {
|
|
523
|
+
warnings.push('C8: 01_requirements.md declares NO requirement ids in the contract shapes (FR-N / NFR-N / AC-N / C-N at line start) — nothing to cover');
|
|
524
|
+
} else {
|
|
525
|
+
const missing = reqIds.filter((id) => !new RegExp('\\b' + escapeReqId(id) + '\\b').test(plan));
|
|
526
|
+
if (missing.length === 0) {
|
|
527
|
+
// Silence is not a verdict (K7): a check that ran and found nothing wrong must still print,
|
|
528
|
+
// or a reader cannot tell "C8 ran clean" from "C8 never ran". Deliberately NOT a PASS/FAIL/
|
|
529
|
+
// NOT-ESTABLISHED word — parsePlanGateVerdict anchors on the LAST such word, and a stray one
|
|
530
|
+
// mid-stream is exactly the G-F1 forgery class this script's `safe()` already defends against.
|
|
531
|
+
out(`NOTE C8: all ${reqIds.length} requirement ids referenced`);
|
|
532
|
+
} else if (REQUIRE_REQUIREMENTS) {
|
|
533
|
+
for (const id of missing) failures.push(`C8: ${id} (01_requirements.md) is not referenced by the plan`);
|
|
534
|
+
} else {
|
|
535
|
+
const shown = missing.slice(0, 12);
|
|
536
|
+
const more = missing.length > 12 ? `, …and ${missing.length - 12} more` : '';
|
|
537
|
+
warnings.push(`C8: ${missing.length} of ${reqIds.length} requirement ids are not referenced by the plan: ${shown.join(', ')}${more}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
434
543
|
// C5 — Inputs read line
|
|
435
544
|
if (!/Inputs read:/i.test(plan)) warnings.push('C5: no "Inputs read:" line (wave-2 seam, WARN only)');
|
|
436
545
|
else for (const need of ['03_adr','05_architecture']) if (!plan.includes(need)) warnings.push(`C5: Inputs read line missing ${need}`);
|
|
@@ -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
|
+
}
|
|
@@ -144,7 +144,27 @@ const CJM_META = {
|
|
|
144
144
|
// Lock result (✅)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
**Design
|
|
147
|
+
**Design: откуда берётся облик. Порядок приоритета, и он не симметричен.**
|
|
148
|
+
|
|
149
|
+
Прежде чем открывать таблицу ниже, ответьте на один вопрос: **воспроизводит ли проект НАЗВАННЫЙ
|
|
150
|
+
существующий продукт, и снят ли его облик?**
|
|
151
|
+
|
|
152
|
+
| Состояние источника | Откуда берётся палитра/типографика/тон |
|
|
153
|
+
|---|---|
|
|
154
|
+
| источник назван, облик **СНЯТ** | **из снятого облика источника** — таблица ниже НЕ применяется |
|
|
155
|
+
| источник назван, облик **НЕ ИЗМЕРЕН** (не удалось снять) | таблица ниже — как **ФОЛБЭК**, и он подписывается словом «фолбэк» вместе с причиной, по которой облик не сняли |
|
|
156
|
+
| **источника нет** (продукт не воспроизводится) | таблица ниже — как **ФОЛБЭК** по отрасли, и он тоже подписывается |
|
|
157
|
+
|
|
158
|
+
Почему это не две равноправные ветви. Отраслевая палитра — это догадка о том, как продукт мог бы
|
|
159
|
+
выглядеть. Когда есть живой источник, догадка не «одна из двух опций»: она хуже измерения ровно
|
|
160
|
+
настолько, насколько выдуманное отличается от наблюдённого. Прототип, который тихо подставил
|
|
161
|
+
Emerald, потому что проект про здоровье, читается точно так же, как прототип, у которого Emerald
|
|
162
|
+
измерен у источника — и именно эта неразличимость и есть дефект.
|
|
163
|
+
|
|
164
|
+
Подпись обязательна во всех состояниях, включая «источника нет»: неподписанная палитра и
|
|
165
|
+
подписанная выглядят одинаково для следующего читателя, а значат разное.
|
|
166
|
+
|
|
167
|
+
**Design адаптация под {INDUSTRY} — ФОЛБЭК, применяется только в двух нижних состояниях:**
|
|
148
168
|
|
|
149
169
|
| Industry | Палитра | Иконки | Тон |
|
|
150
170
|
|----------|---------|--------|-----|
|