@dzhechkov/p-replicator 1.13.2 → 1.13.3
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 +35 -19
- package/package.json +5 -4
- package/sbom.json +58 -18
- package/src/utils.js +1 -0
- package/templates/.claude/commands/replicate.md +9 -1
- package/templates/.claude/hooks/check-dangling-refs.cjs +89 -0
- package/templates/.claude/hooks/check-docs-complete.cjs +7 -0
- package/templates/.claude/hooks/statusline.cjs +1 -1
- package/templates/.claude/rules/cost-of-detection-ladder.md +37 -4
- package/templates/.claude/rules/docker-ports.md +28 -0
- package/templates/.claude/rules/feature-lifecycle.md +21 -0
- package/templates/.claude/rules/replicate-pipeline.md +3 -3
- package/templates/.claude/skills/brutal-honesty-review/resources/assessment-rubrics.md +12 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md +45 -0
- package/tests/snapshot/baseline.json +12 -11
- package/tests/unit/capture-source-path.test.js +73 -24
- package/tests/unit/check-dangling-refs.test.js +91 -0
- package/tests/unit/guard-honest-input-meta.test.js +49 -0
- package/tests/unit/honest-failure-rules.test.js +23 -1
- package/tests/unit/negative-conclusion-gate.test.js +3 -3
- package/tests/unit/optional-doc-idiom.test.js +83 -0
- package/tests/unit/quote-provenance.test.js +4 -0
- package/tests/unit/traceability-negative-fixture.test.js +19 -5
- package/tests/unit/verdict-vocabulary.test.js +72 -0
|
@@ -123,6 +123,34 @@ bindings/Compose-метки и только там проверяет runtime-а
|
|
|
123
123
|
**Чего она НЕ делает.** Она не привязана ни к какому событию — её надо позвать. Не выводите из
|
|
124
124
|
присутствия правила или последней зелёной квитанции, что кто-то продолжает смотреть за машиной.
|
|
125
125
|
|
|
126
|
+
## Тестовый compose: две вещи, которые ломают не тест, а прод
|
|
127
|
+
|
|
128
|
+
Обе — обобщённые из отклонённой заявки на `compose.test.yml`; правила ниже generic, самого файла в
|
|
129
|
+
пакете нет и не будет.
|
|
130
|
+
|
|
131
|
+
**1. Пароли тестового стека задаются через `${VAR:?}` — без значения по умолчанию.**
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
environment:
|
|
135
|
+
POSTGRES_PASSWORD: ${TEST_DB_PASSWORD:?переменная обязана быть задана}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Запись `${VAR:-по-умолчанию}` кажется удобной и делает ровно одну вещь: превращает забытую
|
|
139
|
+
переменную в ТИХО РАБОТАЮЩИЙ стек с известным паролем. Форма `:?` останавливает запуск с
|
|
140
|
+
названной причиной, то есть переводит отказ из молчаливого в громкий. Пароль по умолчанию в
|
|
141
|
+
тестовом стеке — это пароль по умолчанию, который однажды переедет в прод вместе с файлом.
|
|
142
|
+
|
|
143
|
+
**2. Тестовый compose обязан задавать своё `name:`.**
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
name: myproject-test
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Без него docker выводит имя проекта из ИМЕНИ КАТАЛОГА, поэтому тестовый и рабочий стек в одном
|
|
150
|
+
каталоге получают одно имя. Практическое следствие: `docker compose -f compose.test.yml up`
|
|
151
|
+
останавливает и пересоздаёт контейнеры рабочего стека, потому что для docker это тот же проект.
|
|
152
|
+
Отказ здесь не в тесте — он в том, что тест сносит то, что тестом не является.
|
|
153
|
+
|
|
126
154
|
## Быстрая самопроверка
|
|
127
155
|
|
|
128
156
|
```bash
|
|
@@ -96,6 +96,27 @@ After 3 retries with 🔴, halt and surface to user.
|
|
|
96
96
|
|
|
97
97
|
**Quality gate:** tests pass, lint clean, build succeeds.
|
|
98
98
|
|
|
99
|
+
### Comment density — match the host project, not this toolkit
|
|
100
|
+
|
|
101
|
+
**Правило одной строкой: комментарий пишется только для ограничения, которое КОД НЕ МОЖЕТ ПОКАЗАТЬ
|
|
102
|
+
САМ, а плотность комментариев берётся от целевого репозитория, а не от этого тулкита.**
|
|
103
|
+
|
|
104
|
+
Что считается таким ограничением: измеренная причина («порог 2, потому что при 1 счётчик указывает
|
|
105
|
+
на одну запись»), внешний контракт, который нельзя вывести из кода, ссылка на инцидент, объясняющая
|
|
106
|
+
неочевидную защиту. Что им НЕ является: пересказ того, что делает следующая строка; заголовок
|
|
107
|
+
раздела; преамбула о замысле функции, чьё имя уже это говорит.
|
|
108
|
+
|
|
109
|
+
**Почему это правило здесь.** [FIELD, 2026-08-30] Коллега владельца о сгенерированном коде:
|
|
110
|
+
«пишется очень много документации, в коде в основном одни комментарии, это переполняет контекст».
|
|
111
|
+
ИЗМЕРЕНО 2026-09-03 на отгружаемых шаблонах этого пакета: проза составляет 70,3% их объёма
|
|
112
|
+
(1 105 072 байта разметки против 466 914 байт кода), а внутри самого кода комментарии занимают
|
|
113
|
+
33,6% (156 660 байт). Это стиль ЭТОГО репозитория, и в нём он оправдан: код тут читают агенты,
|
|
114
|
+
которым неоткуда узнать историю. В чужом проекте он и чужая конвенция, и налог на контекст каждого
|
|
115
|
+
будущего чтения.
|
|
116
|
+
|
|
117
|
+
**Практическая проверка перед тем, как оставить комментарий:** удалите его мысленно и спросите,
|
|
118
|
+
теряется ли при этом факт, который нельзя восстановить из кода. Нет — удаляйте по-настоящему.
|
|
119
|
+
|
|
99
120
|
### Positive file receipt (required)
|
|
100
121
|
|
|
101
122
|
Each unit gets a unique `WORK_UNIT_ID` and unique absolute `TRACE_PATH`. Its worker MUST write a
|
|
@@ -215,14 +215,14 @@ are project-agnostic and can be enhanced (read by Phase 3) but never recreated.
|
|
|
215
215
|
[`incoming-webhooks`](incoming-webhooks.md), [`long-running-job`](long-running-job.md),
|
|
216
216
|
[`model-call-cost`](model-call-cost.md)
|
|
217
217
|
|
|
218
|
-
**Hooks (
|
|
218
|
+
**Hooks (26 files in `.claude/hooks/`, cross-platform Node).** Only four are wired to an
|
|
219
219
|
event in `.claude/settings.json`; the rest are utilities you invoke deliberately, and the
|
|
220
220
|
difference matters — a hook of this package is NON-BLOCKING by contract and can only print.
|
|
221
221
|
|
|
222
222
|
*Wired to an event (4):* `session-insights.cjs` (SessionStart) · `autocommit-roadmap.cjs`,
|
|
223
223
|
`autocommit-insights.cjs`, `autocommit-plans.cjs` (Stop)
|
|
224
224
|
|
|
225
|
-
*Invoked deliberately, wired to nothing (
|
|
225
|
+
*Invoked deliberately, wired to nothing (22):* `statusline.cjs` (a statusLine, not a hook) ·
|
|
226
226
|
`state-update.cjs` (argv utility) · `write-insight.cjs` (harvest carrier writer) ·
|
|
227
227
|
`check-ports.cjs` (docker-ports Правило №0, exits 0/1/2) ·
|
|
228
228
|
`check-docs-complete.cjs` (are the Phase-1 documents written, exits 0/1/2) ·
|
|
@@ -236,7 +236,7 @@ safe against reordering, exits 0/1/2)
|
|
|
236
236
|
`check-job-contract.cjs` (does long-running work have a handle, three states and a resuming retry, exits 0/1/2)
|
|
237
237
|
`check-model-cost.cjs` (does every external model call name a binding spend ceiling, exits 0/1/2) ·
|
|
238
238
|
`check-review-contract.cjs` (does review-report.md answer every AC id and name the spec revision it judged, exits 0/1/2)
|
|
239
|
-
`check-canon.cjs` (before a WRITING fan-out: is the shared canon named and pinned, exits 0/1/2)
|
|
239
|
+
`check-canon.cjs` · `check-dangling-refs.cjs` (before a WRITING fan-out: is the shared canon named and pinned, exits 0/1/2)
|
|
240
240
|
`check-file-ownership.cjs` (one writer per file, and a split-born file owned at creation, exits 0/1/2)
|
|
241
241
|
`check-source-version.cjs` (does every edit and verdict declare the source version it was built on, exits 0/1/2)
|
|
242
242
|
`check-handoff-manifest.cjs` (did every enumerated Phase-0 output get an answer from Phase 1, exits 0/1/2)
|
|
@@ -21,12 +21,22 @@
|
|
|
21
21
|
|-------|----------|---------|
|
|
22
22
|
| 🔴 **Failing** | Crashes on invalid input | Uncaught exceptions, panics |
|
|
23
23
|
| 🟡 **Passing** | Returns error codes/exceptions | `try/catch`, error returns |
|
|
24
|
-
| 🟢 **Excellent** | Graceful degradation + logging | Circuit breakers, retry logic |
|
|
24
|
+
| 🟢 **Excellent** | Graceful degradation + logging — **но НЕ для значения, которое продукт ОТДАЁТ наружу** | Circuit breakers, retry logic |
|
|
25
|
+
|
|
26
|
+
> **Оговорка к высшей оценке, и она несущая.** Мягкая деградация заслуживает 🟢 для НЕДОСТУПНОСТИ
|
|
27
|
+
> (сервис не ответил — вернём кэш, попробуем позже) и заслуживает 🔴 для ЗНАЧЕНИЯ, которое уходит
|
|
28
|
+
> потребителю (цена, остаток, право доступа, результат расчёта). Подставить приблизительное вместо
|
|
29
|
+
> точного и записать это в журнал — значит выдать неверный ответ и назвать это устойчивостью.
|
|
30
|
+
> Правильный исход для значения — ОТКАЗ С НАЗВАННОЙ ПРИЧИНОЙ, а не правдоподобная замена.
|
|
31
|
+
>
|
|
32
|
+
> Оговорка появилась потому, что прежняя формулировка прямо ПООЩРЯЛА механизм, который в разборе
|
|
33
|
+
> реальных отказов назван причиной каждого происшествия высшей категории: подстановка запасного
|
|
34
|
+
> значения там, где честный ответ — «не знаю».
|
|
25
35
|
|
|
26
36
|
### Concurrency Safety
|
|
27
37
|
| Level | Criteria | Example |
|
|
28
38
|
|-------|----------|---------|
|
|
29
|
-
| 🔴 **Failing** | Race conditions, deadlocks | Shared mutable state, no locks |
|
|
39
|
+
| 🔴 **Failing** | Race conditions, deadlocks; **а также: последовательный тест, поданный как доказательство параллельной безопасности** | Shared mutable state, no locks; «тест проходит» при одном писателе |
|
|
30
40
|
| 🟡 **Passing** | Thread-safe with locks | Proper mutex usage |
|
|
31
41
|
| 🟢 **Excellent** | Lock-free or proven safe | Immutable data, atomic operations |
|
|
32
42
|
|
|
@@ -49,6 +49,28 @@ ALWAYS use parameterized set_config():
|
|
|
49
49
|
`SET LOCAL key = '${value}'`. Applies to all session-level config.
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
**Шесть строк к S-01 и S-02, каждая ловит отдельный способ обойти изоляцию арендаторов:**
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
RULE: BYPASSRLS on the service role is NOT a safety net — it is the removal of one.
|
|
56
|
+
Under it a FORGOTTEN tenant filter returns other tenants' rows SILENTLY,
|
|
57
|
+
with no error to notice. Reserve it for migrations, never for request paths.
|
|
58
|
+
RULE: SET LOCAL ROLE, never SET ROLE. Plain SET ROLE outlives the transaction and
|
|
59
|
+
leaks into whatever the pooled connection serves next.
|
|
60
|
+
RULE: current_setting('app.tenant_id', true) — the second argument makes a missing
|
|
61
|
+
setting return NULL instead of raising. Without it an unset tenant is an
|
|
62
|
+
exception you will catch and swallow; with it, it is a value you can test for.
|
|
63
|
+
RULE: Tests MUST NOT run as a superuser. A superuser bypasses RLS unconditionally,
|
|
64
|
+
so every policy test passes and proves nothing.
|
|
65
|
+
RULE: A table carrying a policy but no CROSS-TENANT test counts as UNPROTECTED.
|
|
66
|
+
The policy is a claim; the test is the evidence.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Почему они здесь, а не отдельным разделом.** Каждая — способ, которым изоляция ЕСТЬ в коде и
|
|
70
|
+
НЕ РАБОТАЕТ на прогоне. Все пять отказывают молча: под `BYPASSRLS` нет ошибки, у суперпользователя
|
|
71
|
+
нет ошибки, у утёкшей роли нет ошибки. Молчаливый отказ защиты — единственный вид, который доживает
|
|
72
|
+
до продакшена, потому что громкий чинят в первый же день.
|
|
73
|
+
|
|
52
74
|
---
|
|
53
75
|
|
|
54
76
|
### S-03: Fail-Fast Secret Validation at Startup
|
|
@@ -159,6 +181,29 @@ NEVER: expose stack traces in production error responses
|
|
|
159
181
|
|
|
160
182
|
---
|
|
161
183
|
|
|
184
|
+
### S-08: Image Type Comes From CONTENT, Never From `Content-Type`
|
|
185
|
+
|
|
186
|
+
**Pattern:**
|
|
187
|
+
```
|
|
188
|
+
RULE: An uploaded image's type is decided by INSPECTING ITS BYTES, never by the
|
|
189
|
+
Content-Type header or the file extension — both are attacker-supplied.
|
|
190
|
+
RULE: SVG is REJECTED WHOLESALE for user uploads. It is a script-bearing document
|
|
191
|
+
that happens to render as a picture; sanitising it is a losing arms race.
|
|
192
|
+
RULE: Serve user-supplied files with `X-Content-Type-Options: nosniff`, so a browser
|
|
193
|
+
cannot re-decide the type you already decided.
|
|
194
|
+
|
|
195
|
+
NEVER: trust `req.file.mimetype` as the type
|
|
196
|
+
NEVER: allow `image/svg+xml` through an "allowed image types" list
|
|
197
|
+
NEVER: serve uploads from the same origin as the application without nosniff
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Why all three, and not just the first.** Sniffing the bytes stops a `.png` that is really a
|
|
201
|
+
script. It does NOT stop SVG, because an SVG genuinely IS an image by content and genuinely CAN
|
|
202
|
+
carry script. And neither stops a browser that ignores your decision and sniffs for itself — that
|
|
203
|
+
is what the header is for. Drop any one of the three and the other two leave a path open.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
162
207
|
## Integration Patterns for Generated Toolkit
|
|
163
208
|
|
|
164
209
|
### How Patterns Map to Generated Files
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-09-
|
|
3
|
-
"totalFiles":
|
|
2
|
+
"generatedAt": "2026-09-03T05:28:07.394Z",
|
|
3
|
+
"totalFiles": 167,
|
|
4
4
|
"files": {
|
|
5
5
|
".claude/agents/doc-validator.md": "e57950758cd4764b2cb54b3f1905567e9dbc1ed20ac6ecd30998614894689f4f",
|
|
6
6
|
".claude/agents/harvest-coordinator.md": "71e524e8379df46bbf457dbc1d4480634e05c99b7d3a4f59a356b8d71a0a4166",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
".claude/commands/myinsights.md": "f31c5e133b5027197359e4179cbba1ab535f990634a69ea7c5c73bd2bc329d68",
|
|
15
15
|
".claude/commands/next.md": "1e4984306c3aa80dfc8734dc1c975dfe5fdf2cf6396ddd759e4c7635646107b8",
|
|
16
16
|
".claude/commands/plan.md": "6fef889f51168981bf4a98b8392a0d595e3d199e1cfaa8e08c0a181ec6c440e4",
|
|
17
|
-
".claude/commands/replicate.md": "
|
|
17
|
+
".claude/commands/replicate.md": "f176d8ec871459b5a8d2111b8527331f34340d6e169da8cc7792953e3e334774",
|
|
18
18
|
".claude/commands/run.md": "87144dfe8c56c15ba7c117daa544d8a08b84d7205f21dafcc48d27c6c3995059",
|
|
19
19
|
".claude/commands/start.md": "fb8a7762f4073b9e8689dec3830680907efb9ec68bb2faa39b8a547793d9a078",
|
|
20
20
|
".claude/hooks/autocommit-insights.cjs": "5e7cef87b24d457f25315d79fe15f0dfaf94d9269495d638817f0faee2593c6f",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
".claude/hooks/autocommit-roadmap.cjs": "f4dcc9ffd9a3aed91dd56e011e699fcf8af903cc81594e4ab1ef61e5432a36ad",
|
|
23
23
|
".claude/hooks/capture-source-path.cjs": "bed92548c3313d69110b6cc06e6b2b858d9d548b8c18c82750ae90d514212bf2",
|
|
24
24
|
".claude/hooks/check-canon.cjs": "99e6bff9b0078eb2ec23f7df98b7ac06c894f9018eba4a8c358e8f92e5ec7b06",
|
|
25
|
-
".claude/hooks/check-
|
|
25
|
+
".claude/hooks/check-dangling-refs.cjs": "8f012876b1af110a1093319d36a2f84208a15780dbb8abb5a43eb4aeb8f76865",
|
|
26
|
+
".claude/hooks/check-docs-complete.cjs": "0f516df0882c9da53d166a1b4e30ff1fc37bd5a2027c7e5fbdab6cba116ca828",
|
|
26
27
|
".claude/hooks/check-embed-contract.cjs": "9e963a07c12350b760264133021e75fb699439b1f060332e1ccdd962b645b4a0",
|
|
27
28
|
".claude/hooks/check-external-deps.cjs": "7bce49a744170a3193edf4b2238bcc5a6f1b0efa29333760a52e4fd145739d18",
|
|
28
29
|
".claude/hooks/check-file-ownership.cjs": "c6273e7b0817d15e375586131924a08095722aff7d0e6a6a04c857a480788e1f",
|
|
@@ -40,25 +41,25 @@
|
|
|
40
41
|
".claude/hooks/check-webhook-contract.cjs": "dfa27bd24cee8868fd60be5d24749c206f303fe4afcc1e72024d526324562a39",
|
|
41
42
|
".claude/hooks/session-insights.cjs": "31fbe90d81b01dfb56557ed5d68dbde2211a14d5469c5d3761f45fb63c07c3a7",
|
|
42
43
|
".claude/hooks/state-update.cjs": "47146a76b768ac2273abe8f2f017856a0e28b45afaf90048148da417dd19604e",
|
|
43
|
-
".claude/hooks/statusline.cjs": "
|
|
44
|
+
".claude/hooks/statusline.cjs": "bac385da01dd9f1548d0db48a2acdada5e89a7822a7f0ecfb61cb74ff4cda138",
|
|
44
45
|
".claude/hooks/write-insight.cjs": "743f2d85b037cf522dfaf4775947b0cd331e52f2f304e88360c160e0e8e9a589",
|
|
45
|
-
".claude/rules/cost-of-detection-ladder.md": "
|
|
46
|
-
".claude/rules/docker-ports.md": "
|
|
46
|
+
".claude/rules/cost-of-detection-ladder.md": "d2e7da3d56c8d4c6d1aa58e793fa0d5eecd65a5c01be38dfa0dc434273306838",
|
|
47
|
+
".claude/rules/docker-ports.md": "a9e5bb9a797ea4ef4c0671cee2cbec95f2557f8b9e1d20b84bbcee7ce4c21162",
|
|
47
48
|
".claude/rules/embeddable-widget.md": "0159224f8e9599b67a860738bb42e3e40adf598176c48ea6c86a01c891f0f18c",
|
|
48
|
-
".claude/rules/feature-lifecycle.md": "
|
|
49
|
+
".claude/rules/feature-lifecycle.md": "a7d0adf6bc63b942d3dec339b3ae68972c3dedc0b48a91d27653309c7da7f11f",
|
|
49
50
|
".claude/rules/git-workflow.md": "7d8d2ded47fcc0fb95da3dc226efa6cb3c80d6adf7296977588c181dfce20192",
|
|
50
51
|
".claude/rules/honest-configuration.md": "851bc28e9e2baf1043bc09b982f3df3431bc7af1e076f8ff9df3d6729835bad5",
|
|
51
52
|
".claude/rules/incoming-webhooks.md": "92f3de02814ed0610df644acf554f4b41b2bae2f9d281695e2e217a1d6558e84",
|
|
52
53
|
".claude/rules/insights-capture.md": "d529e6be79178636ca362d83366b1dd7a843371e8ba4f2b09d65c009fab52dc4",
|
|
53
54
|
".claude/rules/long-running-job.md": "e6bd0efff89338998d34440a94dc9288cf7bcf790b27bcaa51b83d43f57942ed",
|
|
54
55
|
".claude/rules/model-call-cost.md": "26c3b20ac3ed7f9cf6deb5e94302ade87f9e3e51fffca7ac2377ff31bc144f34",
|
|
55
|
-
".claude/rules/replicate-pipeline.md": "
|
|
56
|
+
".claude/rules/replicate-pipeline.md": "9c4bbaa9ac628ec5d4e44d3ace6eec0b5bd602b5dccc5d36f123d73be448f159",
|
|
56
57
|
".claude/rules/skill-interface-protocol.md": "c3f624d0a8a4c7e0f5744e529eb4486cc0589fdb732b9799bccc75091a6d63ca",
|
|
57
58
|
".claude/rules/swarm-file-evidence.md": "c71a216dbc3fd633c30949823ceac354f8439b96279a1a8dd606afb1b3ef5c45",
|
|
58
59
|
".claude/settings.json": "18db09827dce410667b93e2b1a41ef8e46d4431114733e4304a5fc2192b2fc66",
|
|
59
60
|
".claude/skills/brutal-honesty-review/evals/brutal-honesty-review.yaml": "855c155d55575e12aab23fb3a5fb2fada74d93974ff755dfedac6f2d5bd41f13",
|
|
60
61
|
".claude/skills/brutal-honesty-review/README.md": "2a026b4af50b21456e50424fbfa089cb983fbbaa0c68024c8c17c695bea25a57",
|
|
61
|
-
".claude/skills/brutal-honesty-review/resources/assessment-rubrics.md": "
|
|
62
|
+
".claude/skills/brutal-honesty-review/resources/assessment-rubrics.md": "b64894e1ddf8b9da7f2f146c737d35215e7281e3d8be10891d4a9b8aa2bd6d07",
|
|
62
63
|
".claude/skills/brutal-honesty-review/resources/review-template.md": "95182c8605b1d821360257e1c5dbac4300bda333d861785de417cbb84090b25b",
|
|
63
64
|
".claude/skills/brutal-honesty-review/schemas/output.json": "2e252a3497b86322e1a6e6bd1aca8708b44389db3a2d81a6f893d467b6351210",
|
|
64
65
|
".claude/skills/brutal-honesty-review/scripts/assess-code.sh": "2b10d2972b1379adde9a68ddf8bcdb399b7d65075e458a54523ffd8d99ddc746",
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
".claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md": "657e35db0cb77d3a9d1a4d01215a9b8d50441dd863593bee3a84f62aad8b00bc",
|
|
80
81
|
".claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md": "5e87c91956eabc70b4a1084b47b2c9a20ede18bb245735812ead956eafabf3af",
|
|
81
82
|
".claude/skills/cc-toolkit-generator-enhanced/references/maturity-integration.md": "aa4e743bcd0281525336a298d0489c31481f69ff1a534e1ed1d262d888937deb",
|
|
82
|
-
".claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md": "
|
|
83
|
+
".claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md": "4a7fae9ea1870c30b71c923fd5d3626a8926c5e960209fad709f4b8e0534fc80",
|
|
83
84
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md": "9ce3db4c3c5b9696a312152067c1869b1f421ce77fbcfdca83edaa797c5caf3f",
|
|
84
85
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md": "c91f81da197a072b57378fd0018eea26c35d909a558bdca6f070eb60153c97b3",
|
|
85
86
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md": "8398f2493434db101476300e514a34bf8124a51eb732cf81923f9a5b5a3d5db4",
|
|
@@ -104,19 +104,55 @@ function site({ robots = null, forbid = [] } = {}) {
|
|
|
104
104
|
if (!body) { res.writeHead(404); return res.end('nf'); }
|
|
105
105
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(body);
|
|
106
106
|
});
|
|
107
|
-
|
|
107
|
+
// `close()` НЕ закрывает сервер, пока жив хоть один keep-alive сокет: он ждёт их естественной
|
|
108
|
+
// смерти. ИЗМЕРЕНО 2026-09-03 — тест P20 после этого не завершался вовсе, и полоса вместе с ним
|
|
109
|
+
// висела дольше десятиминутного потолка вызова. `closeAllConnections()` рвёт живые сокеты, то
|
|
110
|
+
// есть закрывает сервер ДЕТЕРМИНИРОВАННО, а не при удаче. `shutdown()` возвращает обещание,
|
|
111
|
+
// поэтому вызывающий может дождаться закрытия вместо того, чтобы надеяться на него.
|
|
112
|
+
const shutdown = () => new Promise((r) => {
|
|
113
|
+
if (typeof srv.closeAllConnections === 'function') srv.closeAllConnections();
|
|
114
|
+
srv.close(() => r());
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
srv, hits, shutdown,
|
|
118
|
+
listen: () => new Promise((r) => srv.listen(0, '127.0.0.1', () => r(srv.address().port))),
|
|
119
|
+
};
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/** АСИНХРОННЫЙ запуск — см. измерение в шапке файла. */
|
|
123
|
+
/**
|
|
124
|
+
* Потолок времени на один запуск инструмента.
|
|
125
|
+
*
|
|
126
|
+
* ИЗМЕРЕНО 2026-09-03: без него полоса `npm run test:browser` не завершалась ВООБЩЕ — убита по
|
|
127
|
+
* внешнему таймауту дважды, на 400 с и на 560 с. Ждущий вечно тест не отличим от идущего долго, и
|
|
128
|
+
* снаружи оба выглядят как «ещё работает». Потолок превращает зависание в ИМЕНОВАННЫЙ отказ, то
|
|
129
|
+
* есть в результат, который можно прочитать.
|
|
130
|
+
*
|
|
131
|
+
* Значение выбрано не на глаз: браузерные сценарии этого файла на живой машине укладываются в
|
|
132
|
+
* единицы секунд, а сценарии ОТСУТСТВИЯ браузера обязаны отвечать почти мгновенно — им нечего
|
|
133
|
+
* запускать. 20 секунд оставляют десятикратный запас медленному сценарию и всё равно ловят
|
|
134
|
+
* зависание за время, которое человек готов ждать.
|
|
135
|
+
*/
|
|
136
|
+
const RUN_TIMEOUT_MS = 20_000;
|
|
137
|
+
|
|
111
138
|
function run(args, env) {
|
|
112
139
|
return new Promise((resolve) => {
|
|
113
140
|
const child = spawn(process.execPath, [TOOL, ...args], {
|
|
114
141
|
env: { ...process.env, ...(PW ? { PLAYWRIGHT_MODULE: PW } : {}), ...env },
|
|
115
142
|
});
|
|
116
143
|
let out = '';
|
|
144
|
+
let settled = false;
|
|
145
|
+
const finish = (code) => { if (settled) return; settled = true; clearTimeout(timer); resolve({ code, out }); };
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
out += `\n[ТАЙМАУТ ТЕСТА] инструмент не завершился за ${RUN_TIMEOUT_MS} мс и был убит; ` +
|
|
148
|
+
'это НЕ «не удалось проверить», а именованный отказ: зависание есть дефект.\n';
|
|
149
|
+
child.kill('SIGKILL');
|
|
150
|
+
finish('timeout');
|
|
151
|
+
}, RUN_TIMEOUT_MS);
|
|
117
152
|
child.stdout.on('data', (d) => { out += d; });
|
|
118
153
|
child.stderr.on('data', (d) => { out += d; });
|
|
119
|
-
child.on('close', (code) =>
|
|
154
|
+
child.on('close', (code) => finish(code));
|
|
155
|
+
child.on('error', (e) => { out += String(e && e.message); finish('spawn-error'); });
|
|
120
156
|
});
|
|
121
157
|
}
|
|
122
158
|
|
|
@@ -340,7 +376,7 @@ describe('ось «путь»: детерминированная половин
|
|
|
340
376
|
|
|
341
377
|
describe('ось «путь»: живые прогоны браузером по локальной оснастке', { skip: LIVE_SKIP }, () => {
|
|
342
378
|
test('P14 - СНЯТ: путь прокликан, строки выданы, нумерация продолжена', async () => {
|
|
343
|
-
const { srv, listen } = site();
|
|
379
|
+
const { srv, listen, shutdown } = site();
|
|
344
380
|
const port = await listen();
|
|
345
381
|
const dir = tmp();
|
|
346
382
|
try {
|
|
@@ -358,11 +394,11 @@ describe('ось «путь»: живые прогоны браузером по
|
|
|
358
394
|
// И измеренные закономерности стартового экрана, а не значения чужого оформления.
|
|
359
395
|
assert.match(r.out, /шаг сетки отступов: 4px/, 'сетка 4px обязана вывестись: ' + r.out);
|
|
360
396
|
assert.match(r.out, /брейкпоинты из настоящих @media: 768, 1024/, r.out);
|
|
361
|
-
} finally {
|
|
397
|
+
} finally { await shutdown(); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
362
398
|
});
|
|
363
399
|
|
|
364
400
|
test('P15 - доказательства помечены чужим материалом и закрыты своим .gitignore', async () => {
|
|
365
|
-
const { srv, listen } = site();
|
|
401
|
+
const { srv, listen, shutdown } = site();
|
|
366
402
|
const port = await listen();
|
|
367
403
|
const dir = tmp();
|
|
368
404
|
try {
|
|
@@ -379,11 +415,11 @@ describe('ось «путь»: живые прогоны браузером по
|
|
|
379
415
|
'DOM по умолчанию не сохраняется: ' + files.join(', '));
|
|
380
416
|
assert.ok(files.some((f) => f.endsWith('.aria.txt')), 'семантический слепок — наш вывод');
|
|
381
417
|
assert.ok(files.includes('capture.json'), 'измерения сохраняются');
|
|
382
|
-
} finally {
|
|
418
|
+
} finally { await shutdown(); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
383
419
|
});
|
|
384
420
|
|
|
385
421
|
test('P16 - ВЕЖЛИВОСТЬ: один поток, пауза выдержана, потолок страниц соблюдён', async () => {
|
|
386
|
-
const { srv, hits, listen } = site();
|
|
422
|
+
const { srv, hits, listen, shutdown } = site();
|
|
387
423
|
const port = await listen();
|
|
388
424
|
const dir = tmp();
|
|
389
425
|
try {
|
|
@@ -397,13 +433,17 @@ describe('ось «путь»: живые прогоны браузером по
|
|
|
397
433
|
'пауза между экранами обязана выдерживаться, измерено ' + (pages[1].t - pages[0].t) + ' мс');
|
|
398
434
|
assert.match(pages[0].ua, new RegExp(lib.ROBOTS_AGENT),
|
|
399
435
|
'User-Agent честный: маскировка под обычный браузер была бы обходом защиты');
|
|
400
|
-
} finally {
|
|
436
|
+
} finally { await shutdown(); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
401
437
|
});
|
|
402
438
|
|
|
403
439
|
test('P17 - robots.txt спрашивается ПЕРЕД обходом, и запрет — это исход', async () => {
|
|
440
|
+
// Закрытие серверов — в `finally`: упавшая проверка не должна превращаться в зависание
|
|
441
|
+
// (см. измерение в P18 ниже; там это стоило полосе всего её бюджета).
|
|
404
442
|
const dir = tmp();
|
|
443
|
+
const deny = site({ robots: 'User-agent: *\nDisallow: /\n' });
|
|
444
|
+
const broken = site({ robots: 500 });
|
|
445
|
+
const one = site({ robots: 'User-agent: *\nDisallow: /\n' });
|
|
405
446
|
try {
|
|
406
|
-
const deny = site({ robots: 'User-agent: *\nDisallow: /\n' });
|
|
407
447
|
const p1 = await deny.listen();
|
|
408
448
|
const r1 = await run([`http://127.0.0.1:${p1}/`, '--project', dir, '--out', 'e',
|
|
409
449
|
'--max-pages', '3']);
|
|
@@ -411,53 +451,62 @@ describe('ось «путь»: живые прогоны браузером по
|
|
|
411
451
|
assert.match(r1.out, /НЕ ИЗМЕРЕНО: robots-disallowed/, r1.out);
|
|
412
452
|
assert.equal(deny.hits.filter((h) => h.url !== '/robots.txt').length, 0,
|
|
413
453
|
'ни одной страницы не тронуто: robots.txt читается ДО обхода, а не после');
|
|
414
|
-
deny.srv.close();
|
|
415
454
|
|
|
416
455
|
// Нечитаемый robots.txt — запрет, а не разрешение: «не смогли спросить» ≠ «нам разрешили».
|
|
417
|
-
const broken = site({ robots: 500 });
|
|
418
456
|
const p2 = await broken.listen();
|
|
419
457
|
const r2 = await run([`http://127.0.0.1:${p2}/`, '--project', dir, '--out', 'e',
|
|
420
458
|
'--max-pages', '3']);
|
|
421
459
|
assert.equal(r2.code, 2, r2.out);
|
|
422
460
|
assert.match(r2.out, /robots-disallowed/, r2.out);
|
|
423
461
|
assert.match(r2.out, /трактуется как запрет/, r2.out);
|
|
424
|
-
broken.srv.close();
|
|
425
462
|
|
|
426
463
|
// ОДНА страница — не обход, robots.txt не запрашивается вовсе.
|
|
427
|
-
const one = site({ robots: 'User-agent: *\nDisallow: /\n' });
|
|
428
464
|
const p3 = await one.listen();
|
|
429
465
|
const r3 = await run([`http://127.0.0.1:${p3}/lonely`, '--project', dir, '--out', 'e',
|
|
430
466
|
'--max-pages', '1']);
|
|
431
467
|
assert.equal(one.hits.filter((h) => h.url === '/robots.txt').length, 0,
|
|
432
468
|
'обхода нет — robots.txt не спрашивается: ' + r3.out);
|
|
433
|
-
|
|
434
|
-
|
|
469
|
+
} finally {
|
|
470
|
+
await deny.shutdown();
|
|
471
|
+
await broken.shutdown();
|
|
472
|
+
await one.shutdown();
|
|
473
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
474
|
+
}
|
|
435
475
|
});
|
|
436
476
|
|
|
437
477
|
test('P18 - 403 и экран входа — исходы с НАЗВАННОЙ причиной, никогда не обход', async () => {
|
|
478
|
+
// ЗАКРЫТИЕ СЕРВЕРОВ СТОИТ В `finally`, И ЭТО НЕ СТИЛЬ, А ПОЧИНКА ЗАВИСАНИЯ.
|
|
479
|
+
// ИЗМЕРЕНО 2026-09-03: закрытие стояло ПОСЛЕ проверок, поэтому первая же упавшая проверка
|
|
480
|
+
// выбрасывала исключение до него, сервер оставался слушать, и `node --test` ждал опустошения
|
|
481
|
+
// цикла событий ВЕЧНО. Снаружи это выглядело как «полоса идёт долго»: она не шла, она висела —
|
|
482
|
+
// убита по внешнему потолку на 400 с и на 560 с. Сам P18 в одиночку отрабатывает за секунды.
|
|
483
|
+
// Общая форма: ресурс, удерживающий цикл событий, закрывается в `finally`, иначе падение
|
|
484
|
+
// проверки превращается в зависание, а зависание неотличимо от долгой работы.
|
|
438
485
|
const dir = tmp();
|
|
486
|
+
const blocked = site({ forbid: ['/'] });
|
|
487
|
+
const walled = site();
|
|
439
488
|
try {
|
|
440
|
-
const blocked = site({ forbid: ['/'] });
|
|
441
489
|
const p1 = await blocked.listen();
|
|
442
490
|
const r1 = await run([`http://127.0.0.1:${p1}/`, '--project', dir, '--out', 'e',
|
|
443
491
|
'--max-pages', '1']);
|
|
444
492
|
assert.equal(r1.code, 2, r1.out);
|
|
445
493
|
assert.match(r1.out, /НЕ ИЗМЕРЕНО: bot-protected/, r1.out);
|
|
446
494
|
assert.match(r1.out, /ЗАПРЕЩЕНО/, 'и запрет на обход защиты обязан быть сказан: ' + r1.out);
|
|
447
|
-
blocked.srv.close();
|
|
448
495
|
|
|
449
|
-
const walled = site();
|
|
450
496
|
const p2 = await walled.listen();
|
|
451
497
|
const r2 = await run([`http://127.0.0.1:${p2}/signup`, '--project', dir, '--out', 'e',
|
|
452
498
|
'--max-pages', '2']);
|
|
453
499
|
assert.equal(r2.code, 2, 'стартовый экран за входом — не «снято»: ' + r2.out);
|
|
454
500
|
assert.match(r2.out, /НЕ ИЗМЕРЕНО: auth-required/, r2.out);
|
|
455
|
-
|
|
456
|
-
|
|
501
|
+
} finally {
|
|
502
|
+
await blocked.shutdown();
|
|
503
|
+
await walled.shutdown();
|
|
504
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
505
|
+
}
|
|
457
506
|
});
|
|
458
507
|
|
|
459
508
|
test('P19 - ИСТОЧНИКА НЕТ: экран открылся, перехода нет — доказанный отрицательный ответ', async () => {
|
|
460
|
-
const { srv, listen } = site();
|
|
509
|
+
const { srv, listen, shutdown } = site();
|
|
461
510
|
const port = await listen();
|
|
462
511
|
const dir = tmp();
|
|
463
512
|
try {
|
|
@@ -468,13 +517,13 @@ describe('ось «путь»: живые прогоны браузером по
|
|
|
468
517
|
assert.match(r.out, /\*\*Статус съёмки \(путь\):\*\* ИСТОЧНИКА НЕТ/,
|
|
469
518
|
'и печатает готовую строку профиля: ' + r.out);
|
|
470
519
|
assert.ok(!/FR-LOOK-\d{3} \|/.test(r.out), 'строк не выпускается — записывать нечего: ' + r.out);
|
|
471
|
-
} finally {
|
|
520
|
+
} finally { await shutdown(); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
472
521
|
});
|
|
473
522
|
|
|
474
523
|
test('P20 - один двоичный файл выдаёт все три исхода в одном прогоне', async () => {
|
|
475
524
|
// Каждый случай выше утверждает ОДНО направление, поэтому реализация с постоянным ответом
|
|
476
525
|
// прошла бы подмножество. Тот же исполняемый файл обязан выдать 0, 1 и 2.
|
|
477
|
-
const { srv, listen } = site();
|
|
526
|
+
const { srv, listen, shutdown } = site();
|
|
478
527
|
const port = await listen();
|
|
479
528
|
const dir = tmp();
|
|
480
529
|
try {
|
|
@@ -487,6 +536,6 @@ describe('ось «путь»: живые прогоны браузером по
|
|
|
487
536
|
'--max-pages', '1'])).code,
|
|
488
537
|
];
|
|
489
538
|
assert.deepEqual(seen, [0, 1, 2], 'ожидались снято/пути-нет/не-измерено: ' + JSON.stringify(seen));
|
|
490
|
-
} finally {
|
|
539
|
+
} finally { await shutdown(); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
491
540
|
});
|
|
492
541
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Текст, ссылающийся на несуществующий файл, обязан быть замечен машиной.
|
|
5
|
+
*
|
|
6
|
+
* ПОЧЕМУ ЭТО ЕСТЬ. Этот класс дефекта ловился вручную минимум четыре раза: список хуков называл
|
|
7
|
+
* 4 из 8; таблица говорила «Rules 5» при шести; справка обещала 18 видов отказа при 17 в массиве;
|
|
8
|
+
* путь вывода называл несуществующий каталог. `verify` его не ловит ПО ПОСТРОЕНИЮ: он идёт от
|
|
9
|
+
* зарегистрированного объекта к его наличию, а этот дефект живёт в обратном направлении — от
|
|
10
|
+
* текста к объекту, которого нет.
|
|
11
|
+
*
|
|
12
|
+
* ПОЧЕМУ БАЗА, А НЕ НОЛЬ. Измерено 2026-09-03: 58 висячих ссылок в 23 файлах, и они не появляются
|
|
13
|
+
* после `init` (проверено на пустом проекте). Требовать ноль значило бы отказать каждому проекту в
|
|
14
|
+
* первый же день, то есть выключить заставу. База закреплена и может только уменьшаться.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { test, describe } = require('node:test');
|
|
18
|
+
const assert = require('node:assert/strict');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const { spawnSync } = require('node:child_process');
|
|
24
|
+
const HOOK = path.join(__dirname, '..', '..', 'templates', '.claude', 'hooks', 'check-dangling-refs.cjs');
|
|
25
|
+
const { danglingRefs, BASELINE } = require('../../templates/.claude/hooks/check-dangling-refs.cjs');
|
|
26
|
+
const TEMPLATES = path.join(__dirname, '..', '..', 'templates');
|
|
27
|
+
|
|
28
|
+
describe('висячие ссылки не могут стать хуже базы', () => {
|
|
29
|
+
test('текущее дерево не превышает закреплённую базу', () => {
|
|
30
|
+
const found = danglingRefs(TEMPLATES);
|
|
31
|
+
assert.notEqual(found, null, 'templates/.claude должен существовать');
|
|
32
|
+
const unique = new Set(found.map((f) => `${f.from} → ${f.to}`));
|
|
33
|
+
assert.ok(unique.size <= BASELINE,
|
|
34
|
+
`висячих ссылок ${unique.size} при базе ${BASELINE} — новая ссылка на несуществующий файл`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('база не завышена: она равна измеренному, а не взята с запасом', () => {
|
|
38
|
+
// Запас в базе — это тихое разрешение добавить ещё столько же. База обязана совпадать с
|
|
39
|
+
// измерением, иначе она перестаёт быть границей и становится квотой.
|
|
40
|
+
const found = danglingRefs(TEMPLATES);
|
|
41
|
+
const unique = new Set(found.map((f) => `${f.from} → ${f.to}`));
|
|
42
|
+
assert.equal(unique.size, BASELINE,
|
|
43
|
+
`база ${BASELINE} разошлась с измеренным ${unique.size}: если починили — опустите базу, если добавили — верните ссылку`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('сторож видит внедрённую висячую ссылку', () => {
|
|
47
|
+
// Дискриминация: застава, которая ничего не нашла, неотличима от заставы, которая ничего не
|
|
48
|
+
// умеет. Подкладываем ровно ту форму, которую она обязана поймать.
|
|
49
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dangl-'));
|
|
50
|
+
try {
|
|
51
|
+
fs.mkdirSync(path.join(dir, '.claude', 'rules'), { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(dir, '.claude', 'rules', 'a.md'),
|
|
53
|
+
'Смотри `.claude/hooks/nonexistent-guard.cjs` — его нет.\n');
|
|
54
|
+
const found = danglingRefs(dir);
|
|
55
|
+
assert.equal(found.length, 1);
|
|
56
|
+
assert.equal(found[0].to, '.claude/hooks/nonexistent-guard.cjs');
|
|
57
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('существующая цель висячей не считается', () => {
|
|
61
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dangl-ok-'));
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.join(dir, '.claude', 'hooks'), { recursive: true });
|
|
64
|
+
fs.writeFileSync(path.join(dir, '.claude', 'hooks', 'real.cjs'), '// real\n');
|
|
65
|
+
fs.writeFileSync(path.join(dir, '.claude', 'a.md'), 'Смотри `.claude/hooks/real.cjs`.\n');
|
|
66
|
+
assert.deepEqual(danglingRefs(dir), []);
|
|
67
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('честный вход: дерево без висячих ссылок даёт exits 0 и печатает измеренное', () => {
|
|
71
|
+
// Требование каталога хуков: у каждого check-*.cjs должен быть тест ЧЕСТНОГО ВХОДА — прогон на
|
|
72
|
+
// заведомо правильных данных. Без него застава, которая всегда красная, выглядела бы рабочей.
|
|
73
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dangl-honest-'));
|
|
74
|
+
try {
|
|
75
|
+
fs.mkdirSync(path.join(dir, '.claude', 'hooks'), { recursive: true });
|
|
76
|
+
fs.writeFileSync(path.join(dir, '.claude', 'hooks', 'real.cjs'), '// real\n');
|
|
77
|
+
fs.writeFileSync(path.join(dir, '.claude', 'ok.md'), 'Ссылка на `.claude/hooks/real.cjs` — она есть.\n');
|
|
78
|
+
const r = spawnSync(process.execPath, [HOOK, dir], { encoding: 'utf8' });
|
|
79
|
+
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
80
|
+
assert.match(r.stdout, /висячих ссылок 0/);
|
|
81
|
+
assert.match(r.stdout, /Проверено файлов: 1/);
|
|
82
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('отсутствие .claude — это НЕ УСТАНОВЛЕНО, а не «чисто»', () => {
|
|
86
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dangl-none-'));
|
|
87
|
+
try {
|
|
88
|
+
assert.equal(danglingRefs(dir), null, 'пустое дерево обязано дать null, а не пустой список');
|
|
89
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -62,3 +62,52 @@ test('honest-input catalog probe reports a fake check with no same-named test',
|
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* ВТОРОЙ УКУС — на ветку, которую первый не трогал.
|
|
67
|
+
*
|
|
68
|
+
* ИЗМЕРЕНО 2026-09-03: удаление проверки `missing-honest-input-title` из аудита оставляло ОБА
|
|
69
|
+
* метатеста зелёными. Первый укус подкладывает хук БЕЗ теста и потому доказывает только ветку
|
|
70
|
+
* `missing-test`; вторая ветка не проверялась ничем и могла быть удалена незаметно. Страж,
|
|
71
|
+
* половина которого удаляется без единого красного теста, охраняет ровно ту половину, что осталась.
|
|
72
|
+
*/
|
|
73
|
+
test('honest-input catalog probe reports a test file with no honest-input title', () => {
|
|
74
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-honest-meta2-')));
|
|
75
|
+
const hooksCopy = path.join(root, 'hooks');
|
|
76
|
+
const testsCopy = path.join(root, 'unit');
|
|
77
|
+
try {
|
|
78
|
+
fs.cpSync(HOOKS, hooksCopy, { recursive: true });
|
|
79
|
+
fs.cpSync(TESTS, testsCopy, { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(hooksCopy, 'check-yyy.cjs'), "'use strict';\n");
|
|
81
|
+
// Тест ЕСТЬ — и в нём нет ни одного заголовка честного входа. Это ровно та форма, которую
|
|
82
|
+
// первый укус пропускает: файл на месте, обязанность не выполнена.
|
|
83
|
+
fs.writeFileSync(path.join(testsCopy, 'check-yyy.test.js'),
|
|
84
|
+
"'use strict';\nconst { test } = require('node:test');\n" +
|
|
85
|
+
"test('refuses on a bad input', () => {});\n");
|
|
86
|
+
|
|
87
|
+
const result = auditHonestInputs(hooksCopy, testsCopy);
|
|
88
|
+
assert.deepEqual(result.issues.filter((issue) => issue.hook === 'check-yyy.cjs'), [{
|
|
89
|
+
hook: 'check-yyy.cjs', testFile: 'check-yyy.test.js', reason: 'missing-honest-input-title',
|
|
90
|
+
}], 'ветка отсутствующего заголовка обязана иметь собственный укус, иначе её удалят молча');
|
|
91
|
+
} finally {
|
|
92
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* ЧЕСТНАЯ ГРАНИЦА ЭТОГО ИНВАРИАНТА, названная здесь, чтобы её не пришлось выводить.
|
|
98
|
+
*
|
|
99
|
+
* Проверяется ЗАГОЛОВОК теста, а не его содержание. Заголовок «honest input exits 0» над пустым
|
|
100
|
+
* телом инвариант удовлетворяет. То есть страж ловит ЗАБЫВЧИВОСТЬ (никто не написал такого теста)
|
|
101
|
+
* и не ловит ОБМАН (написали заголовок, не написав проверки).
|
|
102
|
+
*
|
|
103
|
+
* Это осознанная граница, а не недосмотр: доказать по телу теста, что он действительно прогоняет
|
|
104
|
+
* честный вход, значит разбирать чужой код, а разбор кода регулярным выражением — тот самый
|
|
105
|
+
* механизм, который в этом пакете уже дважды давал ложные вердикты. Забывчивость при этом и есть
|
|
106
|
+
* массовый случай: заголовок пишут те, кто про обязанность помнит.
|
|
107
|
+
*/
|
|
108
|
+
test('the invariant is a TITLE check, and that boundary is stated rather than implied', () => {
|
|
109
|
+
const source = fs.readFileSync(__filename, 'utf8');
|
|
110
|
+
assert.match(source, /Проверяется ЗАГОЛОВОК теста, а не его содержание/,
|
|
111
|
+
'граница инварианта обязана быть написана в файле, а не подразумеваться');
|
|
112
|
+
});
|
|
113
|
+
|