@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.
@@ -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
- const UNMEASURED_SKILL_SURFACES_BEFORE = { files: 60, bytes: 642574 };
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
- assert.deepEqual(unregisteredUnitTests(unitFiles, pkg.scripts['test:unit']), [],
234
- 'test:unit has a file on disk that it never executes');
235
- assert.deepEqual(unregisteredUnitTests(unitFiles, pkg.scripts.test), [],
236
- 'test has a unit file on disk that it never executes');
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], pkg.scripts['test:unit']),
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
+ });