@dzhechkov/p-replicator 1.13.2 → 1.13.4
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 +56 -24
- package/bin/cli.js +0 -0
- package/package.json +11 -10
- package/sbom.json +103 -23
- package/scripts/check-pipeline-gaps.sh +0 -0
- 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/check-external-deps.cjs +18 -3
- 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/dz-availability.js +34 -0
- package/tests/e2e/packed-insights-writer.test.js +5 -1
- package/tests/npm-cli-resolver.js +366 -0
- package/tests/npm-resolver-cases.json +128 -0
- package/tests/snapshot/baseline.json +13 -12
- package/tests/unit/capture-source-path.test.js +73 -24
- package/tests/unit/check-dangling-refs.test.js +91 -0
- package/tests/unit/check-external-deps.test.js +26 -2
- package/tests/unit/detection-ladder-contract.test.js +20 -10
- package/tests/unit/guard-honest-input-meta.test.js +49 -0
- package/tests/unit/honest-failure-rules.test.js +23 -1
- package/tests/unit/insights-writer.test.js +5 -1
- package/tests/unit/negative-conclusion-gate.test.js +3 -3
- package/tests/unit/npm-cli-resolver.test.js +314 -0
- 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
- package/LICENSE +0 -21
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* check-dangling-refs.cjs — текст ссылается на файл, которого нет?
|
|
5
|
+
*
|
|
6
|
+
* NOT an event hook. Как `check-look-trace.cjs` и соседи, лежит здесь потому, что каталог уже несёт
|
|
7
|
+
* простые утилиты на Node; в `settings.json` он не зарегистрирован и потому вправе отказывать
|
|
8
|
+
* ненулевым кодом.
|
|
9
|
+
*
|
|
10
|
+
* ─── ЗАЧЕМ ───────────────────────────────────────────────────────────────────
|
|
11
|
+
* Один класс дефекта ловился вручную минимум четыре раза: список хуков называл 4 из 8; таблица
|
|
12
|
+
* говорила «Rules 5» при шести; справка обещала 18 видов отказа при 17 в массиве; путь вывода
|
|
13
|
+
* называл каталог, которого не бывает. Форма всегда одна — ТЕКСТ ССЫЛАЕТСЯ НА ОБЪЕКТ, КОТОРОГО НЕТ,
|
|
14
|
+
* и ни одна застава об этом не спрашивала. `verify` спросить не может по построению: он обходит
|
|
15
|
+
* зарегистрированные КОМПОНЕНТЫ и проверяет их наличие, то есть идёт от объекта к тексту, а не от
|
|
16
|
+
* текста к объекту.
|
|
17
|
+
*
|
|
18
|
+
* ─── ЧТО ОН ДЕЛАЕТ, И ЧЕГО НЕ ДЕЛАЕТ ─────────────────────────────────────────
|
|
19
|
+
* Обходит отгружаемые `*.md`, вытаскивает ссылки вида `` `.claude/<путь>` `` на файлы с известными
|
|
20
|
+
* расширениями и проверяет существование цели. Он НЕ разбирает прозу и НЕ угадывает намерение:
|
|
21
|
+
* ссылка засчитывается, только если она в обратных кавычках и оканчивается на расширение из списка.
|
|
22
|
+
* Ссылка на каталог не проверяется вовсе — каталог может создаваться в работе.
|
|
23
|
+
*
|
|
24
|
+
* ─── ПОЧЕМУ БАЗА, А НЕ ПРОСТО ОТКАЗ ──────────────────────────────────────────
|
|
25
|
+
* ИЗМЕРЕНО 2026-09-03 на свежем дереве: 58 висячих ссылок в 23 файлах, и они не появляются после
|
|
26
|
+
* `init` — проверено на пустом проекте. Отказать на всех значило бы отказать каждому проекту прямо
|
|
27
|
+
* сейчас, то есть выключить заставу в первый же день. Поэтому база ЗАКРЕПЛЕНА числом и может только
|
|
28
|
+
* УМЕНЬШАТЬСЯ: новая висячая ссылка даёт отказ, а починка старой обязана уменьшить базу, иначе
|
|
29
|
+
* тест краснеет. Это тот же приём, которым в пакете уже ретирован `Final_Summary.md`: сообщаем,
|
|
30
|
+
* пока не решили, но фиксируем состояние датой и не даём ему ухудшаться.
|
|
31
|
+
*
|
|
32
|
+
* Коды: 0 — не хуже базы · 1 — база превышена (названы новые) · 2 — не удалось установить.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
|
|
38
|
+
/** ИЗМЕРЕНО 2026-09-03 на снимке дерева; может только уменьшаться. */
|
|
39
|
+
const BASELINE = 58;
|
|
40
|
+
|
|
41
|
+
const REF = /`(\.claude\/[A-Za-z0-9_\-./]+\.(?:cjs|mjs|js|sh|md|json|yaml|yml))`/g;
|
|
42
|
+
|
|
43
|
+
function walk(dir) {
|
|
44
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
45
|
+
const p = path.join(dir, e.name);
|
|
46
|
+
return e.isDirectory() ? walk(p) : [p];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Pure half: given a root holding `.claude/`, return every dangling reference found. */
|
|
51
|
+
function danglingRefs(root) {
|
|
52
|
+
const base = path.join(root, '.claude');
|
|
53
|
+
if (!fs.existsSync(base)) return null; // не установлено — код 2, а не «чисто»
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const file of walk(base)) {
|
|
56
|
+
if (!file.endsWith('.md')) continue;
|
|
57
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
58
|
+
let m;
|
|
59
|
+
while ((m = REF.exec(src))) {
|
|
60
|
+
const target = path.join(root, m[1]);
|
|
61
|
+
if (!fs.existsSync(target)) out.push({ from: path.relative(root, file), to: m[1] });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function main(argv) {
|
|
68
|
+
const root = argv[2] || process.cwd();
|
|
69
|
+
const found = danglingRefs(root);
|
|
70
|
+
if (found === null) {
|
|
71
|
+
console.error(`НЕ УСТАНОВЛЕНО: каталог .claude не найден в ${root} — проверка не выполнялась, и это не «чисто»`);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const unique = [...new Set(found.map((f) => `${f.from} → ${f.to}`))];
|
|
75
|
+
if (unique.length > BASELINE) {
|
|
76
|
+
console.error(`❌ висячих ссылок ${unique.length} при базе ${BASELINE} — текст обещает файлы, которых нет:`);
|
|
77
|
+
unique.slice(0, 20).forEach((u) => console.error(` ${u}`));
|
|
78
|
+
if (unique.length > 20) console.error(` … и ещё ${unique.length - 20}`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
console.log(`✅ висячих ссылок ${unique.length}, база ${BASELINE} — не хуже. Проверено файлов: ${walk(path.join(root, '.claude')).filter((f) => f.endsWith('.md')).length}`);
|
|
82
|
+
if (unique.length < BASELINE) {
|
|
83
|
+
console.log(` База устарела в лучшую сторону: опустите BASELINE до ${unique.length}, чтобы достижение закрепилось.`);
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { danglingRefs, BASELINE, REF };
|
|
89
|
+
if (require.main === module) process.exit(main(process.argv));
|
|
@@ -43,7 +43,14 @@ const DOCS = [
|
|
|
43
43
|
// practice — and blocking on it would have refused every project that ran like that one.
|
|
44
44
|
// The discrepancy is filed; until it is settled this reports rather than refuses.
|
|
45
45
|
{ file: 'Final_Summary.md', optional: true, expected: true },
|
|
46
|
+
// Both entries below were made optional with no recorded reason — the gap the dated-receipt
|
|
47
|
+
// guard (`tests/unit/optional-doc-idiom.test.js`) found. What would SETTLE it is unmeasured:
|
|
48
|
+
// whether real projects produce them. So `expected` is deliberately NOT set on either — calling
|
|
49
|
+
// them expected asserts something nobody measured, calling them dispensable retires a promise
|
|
50
|
+
// silently. The receipts record the state; they do not resolve it.
|
|
51
|
+
// MEASURED 2026-09-03: promised twice by `commands/replicate.md`, required by nothing here.
|
|
46
52
|
{ file: 'C4_Diagrams.md', optional: true },
|
|
53
|
+
// MEASURED 2026-09-03: promised SEVEN times by `commands/replicate.md`, required by nothing here.
|
|
47
54
|
{ file: 'ADR.md', optional: true },
|
|
48
55
|
];
|
|
49
56
|
|
|
@@ -139,12 +139,26 @@ function section(text) {
|
|
|
139
139
|
* an untouched template look like a filled-in inventory, which is the same substitution as an
|
|
140
140
|
* absent section, one level in.
|
|
141
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* A markdown separator row: every cell is dashes with optional colons (`|---|:--:|`). The row
|
|
144
|
+
* directly ABOVE it is the table header — in ANY language. MEASURED 2026-09-02 (backlog b8a7669d):
|
|
145
|
+
* recognising the header by the English word `capability` alone turned the Russian header
|
|
146
|
+
* «| Возможность | Провайдер | … |» into a data row «без вердикта», and that false finding fired
|
|
147
|
+
* first and masked the real one. Position is the markdown fact; the word was a guess.
|
|
148
|
+
*/
|
|
149
|
+
function isSeparatorRow(line) {
|
|
150
|
+
if (!line || !line.startsWith('|')) return false;
|
|
151
|
+
const cells = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
152
|
+
return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
|
|
153
|
+
}
|
|
154
|
+
|
|
142
155
|
function rows(body) {
|
|
143
156
|
const out = [];
|
|
144
157
|
let templates = 0;
|
|
145
158
|
let fenced = false;
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
const lines = body.split('\n').map((raw) => raw.trim());
|
|
160
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
161
|
+
const line = lines[i];
|
|
148
162
|
if (/^(```|~~~)/.test(line)) { fenced = !fenced; continue; }
|
|
149
163
|
if (fenced) continue;
|
|
150
164
|
if (!line.startsWith('|')) continue;
|
|
@@ -152,7 +166,8 @@ function rows(body) {
|
|
|
152
166
|
if (cells.length < 7) continue; // '' + 5 columns + ''
|
|
153
167
|
const capability = cells[1];
|
|
154
168
|
if (!capability || /^:?-+:?$/.test(capability)) continue;
|
|
155
|
-
if (
|
|
169
|
+
if (isSeparatorRow(lines[i + 1])) continue; // header row, by POSITION (any language)
|
|
170
|
+
if (/^capability/i.test(capability)) continue; // header of a table WITHOUT a separator row (belt)
|
|
156
171
|
if (/^\[.*\]$/.test(capability) || capability === '...') { templates += 1; continue; }
|
|
157
172
|
out.push({
|
|
158
173
|
capability,
|
|
@@ -245,7 +245,7 @@ function parseExpectedToolkit() {
|
|
|
245
245
|
commandsExpected: 11,
|
|
246
246
|
agentsExpected: 4, // pre-shipped only (project agents are extra)
|
|
247
247
|
rulesExpected: 13, // pre-shipped only (project rules are extra)
|
|
248
|
-
hooksExpected:
|
|
248
|
+
hooksExpected: 26, // 4 event hooks + statusline + state-update + writer + 15 checks + 1 capture
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -62,9 +62,9 @@ directly—for example, a static grep cannot establish runtime resilience.
|
|
|
62
62
|
Every safeguard must connect the reason for the property to an observable signal, a recurring trigger,
|
|
63
63
|
and a response. Record it with this shape:
|
|
64
64
|
|
|
65
|
-
| Cause / property | Observable signal | Check kind | Layer | Trigger / cadence | Reaction | Owner |
|
|
66
|
-
|
|
67
|
-
| Why the constraint exists | What changes when it is violated | How it is observed | Where it is enforced | When it runs | What happens on failure | Who acts |
|
|
65
|
+
| Cause / property | Observable signal | Check kind | Layer | Scope — what it does NOT cover | Trigger / cadence | Reaction | Owner |
|
|
66
|
+
|---|---|---|---|---|---|---|---|
|
|
67
|
+
| Why the constraint exists | What changes when it is violated | How it is observed | Where it is enforced | The boundary, stated so nobody has to infer it | When it runs | What happens on failure | | Who acts |
|
|
68
68
|
|
|
69
69
|
**Reaction must name a concrete action.** Valid reactions include: block or return the change, repair
|
|
70
70
|
the practice or implementation, escalate to the named owner, or revisit the decision explicitly.
|
|
@@ -77,7 +77,12 @@ A blank cell, “note the warning,” or “the reviewer decides” does not clo
|
|
|
77
77
|
3. Select a check kind that can observe that signal.
|
|
78
78
|
4. Place the check on the strongest layer that can express it reliably.
|
|
79
79
|
5. Define its trigger or cadence, concrete Reaction, and owner.
|
|
80
|
-
6. Test that the safeguard
|
|
80
|
+
6. Test that the safeguard FIRES on a deliberately bad input — and, in the same breath, that it
|
|
81
|
+
stays CLEAN on a correct and COMPLETE one. Both halves or neither: a guard proven only to fire is
|
|
82
|
+
indistinguishable from one that fires at everything, and a guard proven only to pass is
|
|
83
|
+
indistinguishable from one that cannot see. The two cases are cheap together and worthless apart.
|
|
84
|
+
7. State the check's SCOPE — what it does not look at. Every guard has a boundary, and a boundary
|
|
85
|
+
left unsaid is read by the next person as "covers everything".
|
|
81
86
|
|
|
82
87
|
## Anti-pattern: “the critic/reviewer will catch it”
|
|
83
88
|
|
|
@@ -94,3 +99,31 @@ may complement the check for semantics; it must not carry a deterministic invari
|
|
|
94
99
|
The same property written only as “remember to include the file” would sit on Layer 5 and could fail
|
|
95
100
|
silently. The artifact test observes the real distribution boundary and defines what happens when it
|
|
96
101
|
breaks.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## A guard that cannot fail is not on Layer 1 — it is decoration
|
|
106
|
+
|
|
107
|
+
A check earns its layer by what it does on BAD input, not by existing. A guard nobody ever saw refuse
|
|
108
|
+
is indistinguishable from a guard that cannot refuse, and the two are told apart by one act: feed it
|
|
109
|
+
the thing it exists to catch and watch it go red.
|
|
110
|
+
|
|
111
|
+
**The rule.** Every guard you add ships with a case that makes it FAIL. Not a case that exercises it —
|
|
112
|
+
a case that would pass if the guard were deleted, and fails because it is there.
|
|
113
|
+
|
|
114
|
+
Three ways a guard silently cannot fail, all of them observed:
|
|
115
|
+
|
|
116
|
+
| How it dies | What it looks like | How to tell |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| Its predicate matches nothing | green forever, no findings ever | inject the exact shape it names; it must go red |
|
|
119
|
+
| It is never invoked | green because it never ran | grep the pipeline for its invocation and for consumption of its exit code |
|
|
120
|
+
| It refuses on absence | red always, for a reason unrelated to the defect | run it on known-good input; it must go green |
|
|
121
|
+
|
|
122
|
+
**The honest half of this rule.** No mechanism in this package enforces it. There is no meta-test
|
|
123
|
+
walking every guard and demanding a failing case beside it. Until such a test exists, this rule lives
|
|
124
|
+
on **Layer 4** — it works when someone reads it and applies it, and lapses silently when nobody does.
|
|
125
|
+
Writing it here does not make it Layer 1; only the meta-test would.
|
|
126
|
+
|
|
127
|
+
That admission is the point. A rule that overstates its own layer is exactly the defect it warns
|
|
128
|
+
about: a Layer-4 habit wearing Layer-1 clothes, which is the worst outcome the ladder describes,
|
|
129
|
+
because it buys the confidence of a deterministic check while keeping the reliability of a reminder.
|
|
@@ -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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Backlog 1b4857a6. Several cases here need a REAL `dz` on PATH — they delete or read the `.dz`
|
|
5
|
+
* state that only dz creates. The insight writer already reports the two ways that can go wrong
|
|
6
|
+
* with DIFFERENT words: `absent` is ENOENT on the spawn (no dz anywhere), `failed` is dz running
|
|
7
|
+
* and erroring. Conflating them is what made these cases green on a developer machine, where a
|
|
8
|
+
* global dz happens to be installed, and red on a CI runner, where nothing installs one:
|
|
9
|
+
* MEASURED 2026-09-19 (runs 35433442559, 35434913155, 35444026540) — `'absent' !== 'ok'`.
|
|
10
|
+
*
|
|
11
|
+
* So a MISSING TOOL is an audible skip and a BROKEN TOOL stays red. The skip is printed as well as
|
|
12
|
+
* registered, because a silent skip is how a test stops proving anything without anyone noticing —
|
|
13
|
+
* the same contract `announceNpmSkip` keeps in `tests/npm-cli-resolver.js`.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately self-contained: `tests/` is published with this package (see `files` in
|
|
16
|
+
* package.json), so nothing here may reach outside it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{skip:(reason:string)=>void}} t — the node:test context
|
|
21
|
+
* @param {string} teachState — the writer's `teach.state`
|
|
22
|
+
* @param {string} caseNote — what this case needs dz FOR, in one clause
|
|
23
|
+
* @returns {boolean} true when the case must stop (dz is absent and the skip is registered)
|
|
24
|
+
*/
|
|
25
|
+
function skipWhenDzAbsent(t, teachState, caseNote) {
|
|
26
|
+
if (teachState !== 'absent') return false;
|
|
27
|
+
const reason = `SKIPPED — this case needs a real \`dz\` on PATH (${caseNote}); the writer `
|
|
28
|
+
+ 'reported teach.state=absent, which is ENOENT on the spawn — no dz was found, not dz failing.';
|
|
29
|
+
process.stdout.write(`${reason}\n`);
|
|
30
|
+
t.skip(reason);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { skipWhenDzAbsent };
|
|
@@ -7,6 +7,7 @@ const crypto = require('node:crypto');
|
|
|
7
7
|
const fs = require('node:fs');
|
|
8
8
|
const os = require('node:os');
|
|
9
9
|
const path = require('node:path');
|
|
10
|
+
const { skipWhenDzAbsent } = require('../dz-availability.js');
|
|
10
11
|
|
|
11
12
|
const PKG = path.resolve(__dirname, '..', '..');
|
|
12
13
|
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
@@ -220,12 +221,15 @@ describe('PR-022 exact packed artifact', () => {
|
|
|
220
221
|
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
221
222
|
});
|
|
222
223
|
|
|
223
|
-
test('P17 - deleting .dz retains the packed Markdown record and local delivery', () => {
|
|
224
|
+
test('P17 - deleting .dz retains the packed Markdown record and local delivery', (t) => {
|
|
224
225
|
const { consumer } = installConsumer();
|
|
225
226
|
try {
|
|
226
227
|
const record = insight({ title: LOCAL_PACKED });
|
|
227
228
|
const written = parseReceipt(runPackedWriter(consumer, record, { path: process.env.PATH }));
|
|
228
229
|
assert.equal(written.status, 'created');
|
|
230
|
+
// Backlog 1b4857a6: a MISSING dz is an audible skip, a BROKEN dz stays red. The rule and
|
|
231
|
+
// its reasoning live once, in tests/dz-availability.js.
|
|
232
|
+
if (skipWhenDzAbsent(t, written.teach.state, 'it deletes the .dz state that only dz creates')) return;
|
|
229
233
|
assert.equal(written.teach.state, 'ok');
|
|
230
234
|
|
|
231
235
|
const query = run('dz', ['recall', '--all', '--json', '--project', consumer], {
|