@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
|
@@ -62,7 +62,29 @@ const ESTIMATED_TRIGGER = 4037.75;
|
|
|
62
62
|
// three /feature report gates; invocation-time material, the gates are the machine half) and the
|
|
63
63
|
// Traceability floor row + FR-009 worked case in references/scoring-system.md. Not a relocation:
|
|
64
64
|
// the always-loaded corpus measured by P5 GREW in the same change (feature.md gates), it did not shrink.
|
|
65
|
-
|
|
65
|
+
// ПЕРЕЗАКРЕПЛЕНО 2026-09-03: 642574 → 645401 байт при неизменных 60 файлах.
|
|
66
|
+
//
|
|
67
|
+
// ЧТО ЭТОТ СТОРОЖ ЛОВИТ, и почему перезакрепление здесь законно, а поднятие допуска — нет.
|
|
68
|
+
// Он ловит ПЕРЕЕЗД всегда-загружаемого текста из измеряемого корпуса в неизмеряемые поверхности
|
|
69
|
+
// навыков: так «сжатие корпуса» выглядит достигнутым, а контекст сессии не уменьшается ни на байт.
|
|
70
|
+
// Признак переезда — рост здесь ПРИ УМЕНЬШЕНИИ там.
|
|
71
|
+
//
|
|
72
|
+
// ПРОВЕРЕНО ПЕРЕД ПЕРЕЗАКРЕПЛЕНИЕМ: `git diff --numstat` по правкам этого захода даёт в измеряемом
|
|
73
|
+
// корпусе (`rules/`, `commands/`) 28 вставок и НОЛЬ удалений. Текст ниоткуда не уехал — это новое
|
|
74
|
+
// содержание: правило S-08 о типе изображения по содержимому и шесть строк к S-01/S-02 о молчаливых
|
|
75
|
+
// способах обойти изоляцию арендаторов, обе вставки в `references/security-patterns-library.md`.
|
|
76
|
+
//
|
|
77
|
+
// ТОЧНОСТЬ УЧЁТА, исправлена после проверки дискриминацией: сторож считает ТОЛЬКО `modules/` и
|
|
78
|
+
// `references/` внутри навыков (строка 242), а `resources/` не считает вовсе. Первая редакция этого
|
|
79
|
+
// комментария приписывала часть роста правкам в `resources/assessment-rubrics.md` — неверно, они в
|
|
80
|
+
// базу не входят. Прирост 2827 байт объясняется одним файлом: 2483 байта дала правка
|
|
81
|
+
// `security-patterns-library.md`, остальное — перенос строк внутри неё. Проверено пробой: приписка
|
|
82
|
+
// к считаемому файлу красит тест, приписка к `resources/` — нет.
|
|
83
|
+
//
|
|
84
|
+
// ДОПУСК ОСТАЁТСЯ НУЛЁМ И НЕ ПОДНИМАЕТСЯ НИ ПРИ КАКОМ РЕЗУЛЬТАТЕ. Разница между двумя числами
|
|
85
|
+
// принципиальная: база — это ИЗМЕРЕНИЕ, и переизмерить её с названной причиной честно; допуск —
|
|
86
|
+
// это РАЗРЕШЕНИЕ расти молча, и любое его значение выше нуля превращает сторожа в квоту.
|
|
87
|
+
const UNMEASURED_SKILL_SURFACES_BEFORE = { files: 60, bytes: 645401 };
|
|
66
88
|
const UNMEASURED_SKILL_SURFACES_GROWTH_ALLOWANCE = 0;
|
|
67
89
|
const SEAMS = [
|
|
68
90
|
['commands/feature.md', 'both'],
|
|
@@ -38,7 +38,7 @@ const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
|
|
|
38
38
|
/** Is a usable python3 present? Reported honestly rather than silently skipped past. */
|
|
39
39
|
function python() {
|
|
40
40
|
for (const bin of ['python3', 'python']) {
|
|
41
|
-
const probe = spawnSync(bin, ['--version'], { encoding: 'utf8' });
|
|
41
|
+
const probe = spawnSync(bin, ['--version'], { encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
42
42
|
if (probe.status === 0) return bin;
|
|
43
43
|
}
|
|
44
44
|
return null;
|
|
@@ -61,7 +61,7 @@ function gate(reportText) {
|
|
|
61
61
|
const facts = path.join(dir, 'facts.json');
|
|
62
62
|
fs.writeFileSync(report, reportText);
|
|
63
63
|
fs.writeFileSync(facts, ledger());
|
|
64
|
-
const r = spawnSync(PY, [GATE, '--report', report, '--facts', facts, '--json'], { encoding: 'utf8' });
|
|
64
|
+
const r = spawnSync(PY, [GATE, '--report', report, '--facts', facts, '--json'], { encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
65
65
|
const raw = (r.stdout || '').trim();
|
|
66
66
|
let parsed = null;
|
|
67
67
|
try { parsed = JSON.parse(raw); } catch { /* left null, asserted by the caller */ }
|
|
@@ -231,7 +231,7 @@ describe('отрицательный вывод — the second direction of the
|
|
|
231
231
|
const report = path.join(dir, 'report.md');
|
|
232
232
|
fs.writeFileSync(report, BASE + NEGATIVE);
|
|
233
233
|
const r = spawnSync(PY, [GATE, '--report', report, '--facts', path.join(dir, 'absent.json'),
|
|
234
|
-
'--json'], { encoding: 'utf8' });
|
|
234
|
+
'--json'], { encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
235
235
|
assert.equal(r.status, 2, 'a gate that could not read its inputs has cleared nothing');
|
|
236
236
|
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
237
237
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A document may be retired from the required set only WITH A DATED MEASUREMENT beside it.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A TEST AND NOT A CONVENTION. Retiring a document is the cheapest way to make a
|
|
7
|
+
* pipeline look faster, and it is invisible afterwards: nobody notices a check that stopped
|
|
8
|
+
* running. The package already ships the honest form of this move — `Final_Summary.md` carries
|
|
9
|
+
* `{ optional: true, expected: true }` plus a comment naming WHAT was measured, WHEN, and WHY the
|
|
10
|
+
* evidence is not yet enough to decide. That is reversible and it leaves a receipt.
|
|
11
|
+
*
|
|
12
|
+
* This test makes the receipt mandatory. `optional: true` without a `MEASURED YYYY-MM-DD` comment
|
|
13
|
+
* above it turns red, so the next person who wants to drop a document must either measure or argue
|
|
14
|
+
* in the open. The guard is deliberately about the RECEIPT, not about which documents are optional:
|
|
15
|
+
* deciding that is a design call, and this file does not pretend to make it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { test, describe } = require('node:test');
|
|
19
|
+
const assert = require('node:assert/strict');
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const HOOK = path.join(__dirname, '..', '..', 'templates', '.claude', 'hooks', 'check-docs-complete.cjs');
|
|
24
|
+
const DATE = /MEASURED \d{4}-\d{2}-\d{2}/;
|
|
25
|
+
|
|
26
|
+
/** Lines of the DOCS array, paired with the comment block immediately above each entry. */
|
|
27
|
+
function optionalEntriesWithContext(source) {
|
|
28
|
+
const lines = source.split('\n');
|
|
29
|
+
const out = [];
|
|
30
|
+
lines.forEach((line, i) => {
|
|
31
|
+
// Match an ENTRY of the DOCS array, not any prose that happens to contain the words. The first
|
|
32
|
+
// version of this parser matched a doc-comment at :29 that merely EXPLAINS the flag, which
|
|
33
|
+
// would have made the guard permanently red for a reason unrelated to any real receipt.
|
|
34
|
+
if (!/^\s*\{\s*file:\s*'[^']+'.*optional:\s*true/.test(line)) return;
|
|
35
|
+
// Walk up through the contiguous comment block directly above this entry.
|
|
36
|
+
let j = i - 1;
|
|
37
|
+
const comment = [];
|
|
38
|
+
while (j >= 0 && /^\s*\/\//.test(lines[j])) { comment.unshift(lines[j]); j -= 1; }
|
|
39
|
+
out.push({ line: i + 1, entry: line.trim(), comment: comment.join('\n') });
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('retiring a document leaves a dated receipt', () => {
|
|
45
|
+
test('every `optional: true` entry carries a MEASURED date above it', () => {
|
|
46
|
+
const source = fs.readFileSync(HOOK, 'utf8');
|
|
47
|
+
const entries = optionalEntriesWithContext(source);
|
|
48
|
+
assert.ok(entries.length > 0, 'the fixture assumes at least one optional document exists');
|
|
49
|
+
|
|
50
|
+
const undated = entries.filter((e) => !DATE.test(e.comment));
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
undated.map((e) => `${HOOK}:${e.line} ${e.entry}`),
|
|
53
|
+
[],
|
|
54
|
+
'a document was made optional without a dated measurement — say what was measured and when',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the guard fires on an injected undated entry', () => {
|
|
59
|
+
// Discrimination: without this, a guard that found nothing would look identical to a guard
|
|
60
|
+
// that cannot see anything. Inject the exact shape it must catch.
|
|
61
|
+
const injected = [
|
|
62
|
+
"const DOCS = [",
|
|
63
|
+
" { file: 'Honest.md' },",
|
|
64
|
+
" // a comment with no date at all",
|
|
65
|
+
" { file: 'Sneaky.md', optional: true },",
|
|
66
|
+
"];",
|
|
67
|
+
].join('\n');
|
|
68
|
+
const found = optionalEntriesWithContext(injected).filter((e) => !DATE.test(e.comment));
|
|
69
|
+
assert.equal(found.length, 1, 'the parser must catch an undated optional entry');
|
|
70
|
+
assert.match(found[0].entry, /Sneaky\.md/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a dated entry passes, so the guard is not simply always-red', () => {
|
|
74
|
+
const injected = [
|
|
75
|
+
"const DOCS = [",
|
|
76
|
+
" // MEASURED 2026-08-27 against a real project: produced 8 of 9 promised documents.",
|
|
77
|
+
" { file: 'Final_Summary.md', optional: true, expected: true },",
|
|
78
|
+
"];",
|
|
79
|
+
].join('\n');
|
|
80
|
+
const found = optionalEntriesWithContext(injected).filter((e) => !DATE.test(e.comment));
|
|
81
|
+
assert.deepEqual(found, [], 'a dated entry must pass');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -15,6 +15,10 @@ function runPython(source, args = []) {
|
|
|
15
15
|
const result = spawnSync("python3", ["-c", source, ...args], {
|
|
16
16
|
cwd: scriptsDir,
|
|
17
17
|
encoding: "utf8",
|
|
18
|
+
// PYTHONDONTWRITEBYTECODE: a test run must not leave __pycache__/*.pyc inside templates/.
|
|
19
|
+
// Measured 2026-09-02: six .pyc files were signed into a package manifest and packed for
|
|
20
|
+
// publication because `npm test` spawned python3 here without it.
|
|
21
|
+
env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
|
|
18
22
|
});
|
|
19
23
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
20
24
|
return result.stdout.trim();
|
|
@@ -230,14 +230,28 @@ describe('the per-feature traceability gap is a named negative fixture (PR-021)'
|
|
|
230
230
|
const pkg = JSON.parse(read(PACKAGE_JSON));
|
|
231
231
|
const unitFiles = fs.readdirSync(path.join(PACKAGE_ROOT, 'tests', 'unit'));
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
// Registration is checked against the UNION of the declared lanes, not against `test:unit`
|
|
234
|
+
// alone. A unit file may legitimately live in a slower lane — `test:browser` drives a real
|
|
235
|
+
// browser and ran for over 6m40s inside the 9m20s `npm test`, pushing the whole suite against
|
|
236
|
+
// the 10-minute call ceiling (measured 2026-09-03). What must NEVER happen is a file that runs
|
|
237
|
+
// in NO lane, because that is how "we made the suite fast" turns into "we stopped testing it".
|
|
238
|
+
// So the union is what the guard accepts, and the lane list itself is asserted below: dropping
|
|
239
|
+
// `test:browser` from package.json turns this test red rather than silently shrinking coverage.
|
|
240
|
+
const LANES = ['test:unit', 'test:browser'];
|
|
241
|
+
for (const lane of LANES) {
|
|
242
|
+
assert.ok(typeof pkg.scripts[lane] === 'string' && pkg.scripts[lane].length > 0,
|
|
243
|
+
`lane ${lane} must exist in package.json — a lane that vanishes takes its tests with it`);
|
|
244
|
+
}
|
|
245
|
+
const unionScript = LANES.map((l) => pkg.scripts[l]).join(' ');
|
|
246
|
+
|
|
247
|
+
assert.deepEqual(unregisteredUnitTests(unitFiles, unionScript), [],
|
|
248
|
+
'a unit file on disk runs in NO declared lane');
|
|
249
|
+
assert.deepEqual(unregisteredUnitTests(unitFiles, `${pkg.scripts.test} ${pkg.scripts['test:browser']}`), [],
|
|
250
|
+
'a unit file runs in neither `test` nor `test:browser`');
|
|
237
251
|
|
|
238
252
|
const probe = '__unregistered-probe.test.js';
|
|
239
253
|
assert.deepEqual(
|
|
240
|
-
unregisteredUnitTests([...unitFiles, probe],
|
|
254
|
+
unregisteredUnitTests([...unitFiles, probe], unionScript),
|
|
241
255
|
[probe],
|
|
242
256
|
'the registration guard must fire on a real injected unregistered filename',
|
|
243
257
|
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* «НЕ УСТАНОВЛЕНО» / «НЕ ИЗМЕРЕНО» mean REFUSAL here, and no future text may reuse them to mean
|
|
5
|
+
* "carry on".
|
|
6
|
+
*
|
|
7
|
+
* WHY A TEST AND NOT A STYLE NOTE. The toolkit already ships a three-valued verdict whose third
|
|
8
|
+
* value BLOCKS: `commands/feature.md` («`2` ПРОВЕРКА НЕ ВЫПОЛНЕНА, и это никогда не «всё чисто»»),
|
|
9
|
+
* `hooks/capture-source-path.cjs` («единственная дверь к коду 2»), `hooks/check-look-trace.cjs`.
|
|
10
|
+
* A proposal reviewed on 2026-09-02 reused the SAME words for a passing outcome — a row written by
|
|
11
|
+
* the author saying "not established, moving on". Two opposite meanings behind one phrase is worse
|
|
12
|
+
* than a new phrase: a reader who learned the blocking sense would read a pass as a refusal, and a
|
|
13
|
+
* reader who learned the passing sense would ignore a real block.
|
|
14
|
+
*
|
|
15
|
+
* The guard is deliberately narrow. It does NOT try to parse intent. It asserts that every file
|
|
16
|
+
* introducing these terms as a VERDICT also contains a non-zero exit, i.e. the vocabulary and the
|
|
17
|
+
* refusal live together. Prose that merely mentions the phrase in passing is not a verdict and is
|
|
18
|
+
* excluded by requiring the term to appear in a verdict-shaped context.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { test, describe } = require('node:test');
|
|
22
|
+
const assert = require('node:assert/strict');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const HOOKS = path.join(__dirname, '..', '..', 'templates', '.claude', 'hooks');
|
|
27
|
+
const TERMS = /НЕ УСТАНОВЛЕНО|НЕ ИЗМЕРЕНО|NOT-ESTABLISHED/;
|
|
28
|
+
// Three shapes of refusal, all of them real in this package and the first version of this predicate
|
|
29
|
+
// only saw the first. `check-dangling-refs.cjs` computes its code in `main()` and calls
|
|
30
|
+
// `process.exit(main(argv))`; the guard called that "cannot refuse" and fired on a hook that
|
|
31
|
+
// refuses perfectly well. A guard whose predicate is narrower than the thing it guards produces
|
|
32
|
+
// false accusations, which cost more trust than the misses they were meant to prevent.
|
|
33
|
+
const NONZERO_EXIT = new RegExp([
|
|
34
|
+
'process\\.exit\\(\\s*[1-9]\\d*\\s*\\)', // literal: process.exit(2)
|
|
35
|
+
'exitCode\\s*=\\s*[1-9]', // assigned: process.exitCode = 1
|
|
36
|
+
'process\\.exit\\(\\s*[A-Za-z_$]', // computed: process.exit(main(argv))
|
|
37
|
+
].join('|'));
|
|
38
|
+
|
|
39
|
+
function hookFiles() {
|
|
40
|
+
return fs.readdirSync(HOOKS).filter((f) => f.endsWith('.cjs')).map((f) => path.join(HOOKS, f));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('the refusal vocabulary always sits next to a refusal', () => {
|
|
44
|
+
test('every hook using the terms can also exit non-zero', () => {
|
|
45
|
+
const offenders = [];
|
|
46
|
+
for (const file of hookFiles()) {
|
|
47
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
48
|
+
if (!TERMS.test(src)) continue;
|
|
49
|
+
if (!NONZERO_EXIT.test(src)) offenders.push(path.basename(file));
|
|
50
|
+
}
|
|
51
|
+
assert.deepEqual(offenders, [],
|
|
52
|
+
'a hook speaks the refusal vocabulary but has no way to refuse — the words promise a block the code cannot deliver');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('the guard fires on a hook that speaks the words without refusing', () => {
|
|
56
|
+
// Discrimination on the predicate itself: a guard that found nothing must be shown to be
|
|
57
|
+
// capable of finding something. Without this, an empty offender list is indistinguishable
|
|
58
|
+
// from a broken matcher.
|
|
59
|
+
const speaksButCannotRefuse = "// НЕ ИЗМЕРЕНО\nconsole.log('ok');\nprocess.exit(0);\n";
|
|
60
|
+
assert.equal(TERMS.test(speaksButCannotRefuse), true, 'the term matcher must see the phrase');
|
|
61
|
+
assert.equal(NONZERO_EXIT.test(speaksButCannotRefuse), false, 'exit(0) is not a refusal');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('at least one hook really does carry both, so the guard is not vacuous', () => {
|
|
65
|
+
const both = hookFiles().filter((f) => {
|
|
66
|
+
const src = fs.readFileSync(f, 'utf8');
|
|
67
|
+
return TERMS.test(src) && NONZERO_EXIT.test(src);
|
|
68
|
+
});
|
|
69
|
+
assert.ok(both.length >= 2,
|
|
70
|
+
'the fixture assumes the toolkit really does use this vocabulary with real refusals');
|
|
71
|
+
});
|
|
72
|
+
});
|