@dzhechkov/p-replicator 1.12.0 → 1.13.2
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 +281 -65
- package/CHANGELOG.md +148 -1
- package/LICENSE +21 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +2 -2
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/05_architecture.md +1 -1
- package/README/eng/README.md +2 -1
- package/README/ru/01_quickstart.md +2 -2
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +8 -8
- package/README.md +150 -9
- package/bin/cli.js +0 -0
- package/package.json +10 -11
- package/sbom.json +604 -64
- package/scripts/check-pipeline-gaps.sh +510 -21
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +5 -1
- package/src/utils.js +33 -3
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +72 -6
- package/templates/.claude/commands/go.md +6 -8
- package/templates/.claude/commands/harvest.md +5 -7
- package/templates/.claude/commands/replicate.md +169 -44
- package/templates/.claude/commands/start.md +28 -7
- package/templates/.claude/hooks/capture-source-path.cjs +795 -0
- package/templates/.claude/hooks/check-canon.cjs +493 -0
- package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
- package/templates/.claude/hooks/check-external-deps.cjs +288 -0
- package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
- package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
- package/templates/.claude/hooks/check-job-contract.cjs +501 -0
- package/templates/.claude/hooks/check-look-origin.cjs +240 -0
- package/templates/.claude/hooks/check-look-trace.cjs +385 -0
- package/templates/.claude/hooks/check-metric-source.cjs +296 -0
- package/templates/.claude/hooks/check-model-cost.cjs +470 -0
- package/templates/.claude/hooks/check-ports.cjs +27 -6
- package/templates/.claude/hooks/check-review-contract.cjs +205 -0
- package/templates/.claude/hooks/check-source-version.cjs +312 -0
- package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
- package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
- package/templates/.claude/hooks/statusline.cjs +2 -2
- package/templates/.claude/rules/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +5 -6
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- package/templates/.claude/rules/long-running-job.md +73 -0
- package/templates/.claude/rules/model-call-cost.md +85 -0
- package/templates/.claude/rules/replicate-pipeline.md +122 -52
- package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
- package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
- package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
- package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
- package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
- package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
- package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
- package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
- package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
- package/templates/.claude/skills/requirements-validator/SKILL.md +18 -5
- package/templates/.claude/skills/requirements-validator/references/feature-report-contracts.md +67 -0
- package/templates/.claude/skills/requirements-validator/references/scoring-system.md +15 -6
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +173 -725
- package/tests/e2e/feature-contour.test.js +176 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/01_specification.md +15 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/02_pseudocode.md +19 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/03_architecture.md +3 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/04_refinement.md +3 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/05_completion.md +7 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/review-report.md +11 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/validation-report.md +10 -0
- package/tests/fixtures/feature-contour/tests/demo.test.js +16 -0
- package/tests/snapshot/baseline.json +64 -40
- package/tests/unit/capture-source-path.test.js +492 -0
- package/tests/unit/check-canon.test.js +403 -0
- package/tests/unit/check-embed-contract.test.js +422 -0
- package/tests/unit/check-external-deps.test.js +363 -0
- package/tests/unit/check-file-ownership.test.js +388 -0
- package/tests/unit/check-handoff-manifest.test.js +410 -0
- package/tests/unit/check-job-contract.test.js +514 -0
- package/tests/unit/check-look-origin.test.js +180 -0
- package/tests/unit/check-look-trace.test.js +420 -0
- package/tests/unit/check-metric-source.test.js +325 -0
- package/tests/unit/check-model-cost.test.js +425 -0
- package/tests/unit/check-ports.test.js +46 -2
- package/tests/unit/check-review-contract.test.js +181 -0
- package/tests/unit/check-source-version.test.js +344 -0
- package/tests/unit/check-swarm-receipts.test.js +231 -0
- package/tests/unit/check-webhook-contract.test.js +536 -0
- package/tests/unit/db-port-rule.test.js +8 -2
- package/tests/unit/detection-ladder-registry.test.js +2 -2
- package/tests/unit/generator-swarm-contract.test.js +287 -0
- package/tests/unit/guard-honest-input-meta.test.js +64 -0
- package/tests/unit/honest-failure-rules.test.js +106 -9
- package/tests/unit/look-phase-contract.test.js +231 -0
- package/tests/unit/negative-conclusion-gate.test.js +300 -0
- package/tests/unit/quote-provenance.test.js +122 -0
- package/tests/unit/sync-templates-guard.test.js +46 -3
- package/tests/unit/traceability-completion-gate.test.js +267 -0
- package/tests/unit/utils.test.js +40 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* check-metric-source.cjs — метрика называет, ОТКУДА берётся её значение?
|
|
6
|
+
*
|
|
7
|
+
* NOT an event hook — this package's hooks are NON-BLOCKING by contract (pinned by
|
|
8
|
+
* tests/unit/hooks-project-anchored.test.js, which requires exit 0), so a hook could only print.
|
|
9
|
+
* Invoke it, in the form of its siblings:
|
|
10
|
+
*
|
|
11
|
+
* node .claude/hooks/check-metric-source.cjs [path-to-project]
|
|
12
|
+
*
|
|
13
|
+
* WHY IT EXISTS — the failure, before the technology.
|
|
14
|
+
*
|
|
15
|
+
* The Success Metrics table has three columns: Metric, Target, Timeline. There is no column for
|
|
16
|
+
* where the number comes from. The completion checklist asks only whether «метрики успеха измеримы»
|
|
17
|
+
* — that is, whether a NUMBER is present. And the single validator lens that looks OUTSIDE the
|
|
18
|
+
* documents reads exactly one thing, `Architecture.md` → `## External Dependencies`, so a metric
|
|
19
|
+
* living in the PRD never reaches it: there is no route, not a weak one.
|
|
20
|
+
*
|
|
21
|
+
* THE MECHANISM: measurability is decided by the SHAPE OF A NUMBER, never by the system's ability to
|
|
22
|
+
* OBTAIN that number. «Доля дошедших до публикации отзыва — 40% — неделя 1» passes completeness,
|
|
23
|
+
* passes measurability and passes consistency, on a platform whose API exposes no reviews at all.
|
|
24
|
+
* The feature is then designed around a promise nobody can keep, and nothing in the pipeline is
|
|
25
|
+
* capable of noticing, because every check it passes was asking a different question.
|
|
26
|
+
*
|
|
27
|
+
* MEASURED on a fixture tree (2026-09-01): a project carrying that exact metric line and a
|
|
28
|
+
* requirement resting on a non-existent `reviews.publish` method passes ALL SEVEN shipped guards —
|
|
29
|
+
* three answer 0 and four answer 2. Not one of them is capable of naming the metric. They are not
|
|
30
|
+
* broken; nothing was asking this.
|
|
31
|
+
*
|
|
32
|
+
* THE CURE, and the reason the fourth column is a CLOSED list. An open «Source» column would be
|
|
33
|
+
* filled with «из аналитики», which is a genre of place, not a place — the same defect as a spend
|
|
34
|
+
* ceiling named «разумный», or evidence that is a URL nobody opened. A closed list forces the author
|
|
35
|
+
* to land on one of four DIFFERENT commitments, and each of them can be argued with:
|
|
36
|
+
*
|
|
37
|
+
* наш журнал we already write this; the number exists the moment we look
|
|
38
|
+
* наша БД we already store this; a query produces it
|
|
39
|
+
* внешний API: X somebody else must hand it to us, THROUGH A NAMED METHOD
|
|
40
|
+
* ручное измерение: X a person will count it, and HOW is stated
|
|
41
|
+
*
|
|
42
|
+
* MANUAL MEASUREMENT IS A LEGITIMATE ANSWER, and refusing it would be the wrong kind of strictness —
|
|
43
|
+
* it would push people to dress a hand count as instrumentation. What is refused is manual
|
|
44
|
+
* measurement PRESENTED AS instrumented: «сверка пяти карточек глазами» is a fine source and a
|
|
45
|
+
* terrible dashboard.
|
|
46
|
+
*
|
|
47
|
+
* THE LOAD-BEARING CONSEQUENCE. A metric whose source is an EXTERNAL API must produce a row in the
|
|
48
|
+
* external-dependency inventory. That is not bookkeeping: it is the only route by which a metric
|
|
49
|
+
* ever comes under the one lens that looks outside the documents at all. Without it the metric's
|
|
50
|
+
* feasibility is never established by anything, however many green verdicts the run collects.
|
|
51
|
+
*
|
|
52
|
+
* WHAT THIS FILE CAN AND CANNOT DECIDE — read before trusting exit 0.
|
|
53
|
+
*
|
|
54
|
+
* It settles that every metric names a source from the closed four, that an external API names a
|
|
55
|
+
* METHOD rather than a vendor, and that such a metric has a matching inventory row. It does NOT call
|
|
56
|
+
* the API, does not know whether the method exists, and cannot tell a real log from a planned one.
|
|
57
|
+
* Whether the number can actually be obtained stays layer 3 — but it is now ASKED, and an
|
|
58
|
+
* unanswerable metric can be argued about before the feature is designed around it.
|
|
59
|
+
*
|
|
60
|
+
* Exit codes — three, and the third is the point:
|
|
61
|
+
* 0 every metric names a source from the closed list, and every external-API metric has its
|
|
62
|
+
* inventory row
|
|
63
|
+
* 1 a defect is PROVEN and NAMED: a metric with no source column at all, an empty source, a
|
|
64
|
+
* source outside the closed list, an external API named without a method, a manual measurement
|
|
65
|
+
* that does not say how, or an external-API metric with no row in the inventory
|
|
66
|
+
* 2 THE CHECK DID NOT RUN — no document carrying `## Success Metrics`, a section holding only
|
|
67
|
+
* the shipped template row, or duplicate metric rows
|
|
68
|
+
*
|
|
69
|
+
* A checker that answers "clean" when it could not look converts an unknown into a reassurance.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
const fs = require('node:fs');
|
|
73
|
+
const path = require('node:path');
|
|
74
|
+
|
|
75
|
+
/** Where a Success Metrics table may live. Both, because the skill declares it in both places. */
|
|
76
|
+
const DOCS = [path.join('docs', 'PRD.md'), path.join('docs', 'Final_Summary.md')];
|
|
77
|
+
const ARCH = path.join('docs', 'Architecture.md');
|
|
78
|
+
|
|
79
|
+
const METRICS_HEADING = /^#{2,6}\s+Success Metrics\s*$/i;
|
|
80
|
+
const DEPS_HEADING = /^#{2,6}\s+External Dependencies\s*$/i;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The closed four. Each is a DIFFERENT commitment, and that is the whole reason the list is closed:
|
|
84
|
+
* an open column collects «из аналитики», which names a genre of place rather than a place.
|
|
85
|
+
*/
|
|
86
|
+
const SOURCES = [
|
|
87
|
+
{ key: 'наш журнал', kind: 'log', needsDetail: false },
|
|
88
|
+
{ key: 'наша бд', kind: 'db', needsDetail: false },
|
|
89
|
+
{ key: 'внешний api', kind: 'external', needsDetail: true },
|
|
90
|
+
{ key: 'ручное измерение', kind: 'manual', needsDetail: true },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
function say(s) { process.stdout.write(s + '\n'); }
|
|
94
|
+
|
|
95
|
+
/** Exit 2 with a reason. Never merged with "clean": not-run and not-violated are different facts. */
|
|
96
|
+
function cannotCheck(reason, hint) {
|
|
97
|
+
say('⚠️ проверка НЕ выполнена: ' + reason);
|
|
98
|
+
if (hint) say(' ' + hint);
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Exit 1 with the defect NAMED. A violation that cannot be named is a 2, not a 1. */
|
|
103
|
+
function proven(title, lines, tail) {
|
|
104
|
+
say('❌ ' + title);
|
|
105
|
+
for (const line of lines) say(' • ' + line);
|
|
106
|
+
if (tail) say(' ' + tail);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The body of a section, or null when the heading is absent.
|
|
112
|
+
*
|
|
113
|
+
* Read PER SECTION, never globally: these documents are full of three-column tables (Timeline &
|
|
114
|
+
* Phases sits directly below Success Metrics and has exactly the same shape), and a global scan
|
|
115
|
+
* would read a phase row as a metric with no source — a defect reported where none exists.
|
|
116
|
+
* Fenced blocks are skipped so a `##` inside an example cannot invent a section.
|
|
117
|
+
*/
|
|
118
|
+
function section(text, heading) {
|
|
119
|
+
const lines = text.split('\n');
|
|
120
|
+
let fenced = false;
|
|
121
|
+
let start = -1;
|
|
122
|
+
let level = 0;
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
if (/^\s*(```|~~~)/.test(lines[i])) { fenced = !fenced; continue; }
|
|
125
|
+
if (fenced) continue;
|
|
126
|
+
if (start < 0) {
|
|
127
|
+
if (heading.test(lines[i].trim())) { start = i; level = /^(#+)/.exec(lines[i].trim())[1].length; }
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const m = /^(#{1,6})\s+\S/.exec(lines[i].trim());
|
|
131
|
+
if (m && m[1].length <= level) return lines.slice(start + 1, i).join('\n');
|
|
132
|
+
}
|
|
133
|
+
return start < 0 ? null : lines.slice(start + 1).join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Metric rows.
|
|
138
|
+
*
|
|
139
|
+
* `cells` is kept RAW rather than destructured into four names, because the defect this file exists
|
|
140
|
+
* for is a row with only THREE cells — a table that never had a provenance column at all. Reading it
|
|
141
|
+
* as «the fourth cell is empty» would report the wrong repair: the fix is a column, not a value.
|
|
142
|
+
*/
|
|
143
|
+
function metricRows(body) {
|
|
144
|
+
const out = [];
|
|
145
|
+
let templates = 0;
|
|
146
|
+
for (const raw of body.split('\n')) {
|
|
147
|
+
const line = raw.trim();
|
|
148
|
+
if (!line.startsWith('|')) continue;
|
|
149
|
+
const cells = line.split('|').map((c) => c.trim());
|
|
150
|
+
cells.shift();
|
|
151
|
+
if (cells.length && cells[cells.length - 1] === '') cells.pop();
|
|
152
|
+
if (cells.length < 3) continue;
|
|
153
|
+
if (cells.every((c) => /^:?-+:?$/.test(c))) continue;
|
|
154
|
+
const name = cells[0];
|
|
155
|
+
if (!name || /^metric$/i.test(name) || /^метрика$/i.test(name)) continue;
|
|
156
|
+
if (/^\[.*\]$/.test(name) || name === '...') { templates += 1; continue; }
|
|
157
|
+
out.push({ name, target: cells[1] || '', timeline: cells[2] || '', cells });
|
|
158
|
+
}
|
|
159
|
+
return { out, templates };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Which of the closed four a source cell names, plus whatever detail follows the colon. */
|
|
163
|
+
function classify(cell) {
|
|
164
|
+
const raw = String(cell || '').replace(/^[`«"]|[`»"]$/g, '').trim();
|
|
165
|
+
const lower = raw.toLowerCase();
|
|
166
|
+
for (const s of SOURCES) {
|
|
167
|
+
if (lower === s.key || lower.startsWith(s.key + ':') || lower.startsWith(s.key + ' :')) {
|
|
168
|
+
const detail = raw.slice(raw.indexOf(':') + 1).trim();
|
|
169
|
+
return { ...s, raw, detail: raw.includes(':') ? detail : '' };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { kind: 'unrecognised', raw, detail: '' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The inventory's own text, so an external-API metric can be looked for in it. */
|
|
176
|
+
function inventoryText(root) {
|
|
177
|
+
try {
|
|
178
|
+
const text = fs.readFileSync(path.join(root, ARCH), 'utf-8');
|
|
179
|
+
return section(text, DEPS_HEADING);
|
|
180
|
+
} catch { return null; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function main() {
|
|
184
|
+
const root = process.argv[2] || '.';
|
|
185
|
+
try { if (!fs.statSync(root).isDirectory()) cannotCheck('это не каталог: ' + root); }
|
|
186
|
+
catch { cannotCheck('путь не существует: ' + root); }
|
|
187
|
+
|
|
188
|
+
let body = null;
|
|
189
|
+
let where = null;
|
|
190
|
+
for (const rel of DOCS) {
|
|
191
|
+
let text;
|
|
192
|
+
try { text = fs.readFileSync(path.join(root, rel), 'utf-8'); } catch { continue; }
|
|
193
|
+
const found = section(text, METRICS_HEADING);
|
|
194
|
+
if (found !== null) { body = body === null ? found : body + '\n' + found; where = where ? where + ', ' + rel : rel; }
|
|
195
|
+
}
|
|
196
|
+
if (body === null) {
|
|
197
|
+
cannotCheck('ни в одном из документов нет раздела `## Success Metrics`: ' + DOCS.join(', '),
|
|
198
|
+
'метрики не написаны — это НЕ «у метрик всё в порядке с источником»');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { out: rows, templates } = metricRows(body);
|
|
202
|
+
if (rows.length === 0) {
|
|
203
|
+
cannotCheck(templates
|
|
204
|
+
? 'в таблице метрик только шаблонные строки (' + templates + ')'
|
|
205
|
+
: 'раздел `## Success Metrics` есть, но метрик в нём нет',
|
|
206
|
+
'проверять нечего; заполненная таблица — предусловие этого вопроса, а не его ответ');
|
|
207
|
+
}
|
|
208
|
+
const names = rows.map((r) => r.name.toLowerCase());
|
|
209
|
+
const dupes = [...new Set(names.filter((n, i) => names.indexOf(n) !== i))];
|
|
210
|
+
if (dupes.length) {
|
|
211
|
+
cannotCheck('в таблице метрик повторяются строки: ' + dupes.join(', '),
|
|
212
|
+
'одна метрика — одна строка; иначе один источник закрывает две разные');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// THE CORE DEFECT: a table that never had a fourth column. Reported as its own case, because the
|
|
216
|
+
// repair is a COLUMN, not a value — and telling the author to fill a cell that does not exist is
|
|
217
|
+
// the kind of message people ignore.
|
|
218
|
+
const noColumn = rows.filter((r) => r.cells.length < 4);
|
|
219
|
+
if (noColumn.length) {
|
|
220
|
+
proven('в таблице метрик нет колонки «Источник значения»',
|
|
221
|
+
noColumn.map((r) => r.name + ' → ' + (r.target || '(без цели)')),
|
|
222
|
+
'измеримость сейчас проверяется формой ЧИСЛА, а не способностью системы это число добыть: '
|
|
223
|
+
+ 'недобываемая метрика проходит полноту, измеримость и непротиворечивость с отличием. '
|
|
224
|
+
+ 'Добавьте четвёртую колонку: наш журнал | наша БД | внешний API: <метод> | '
|
|
225
|
+
+ 'ручное измерение: <как>.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const classified = rows.map((r) => ({ ...r, source: classify(r.cells[3]) }));
|
|
229
|
+
|
|
230
|
+
const empty = classified.filter((r) => r.source.raw === '' || r.source.raw === '—');
|
|
231
|
+
if (empty.length) {
|
|
232
|
+
proven('источник значения не назван', empty.map((r) => r.name),
|
|
233
|
+
'пустая клетка — блокер, а не примечание: она означает, что вопрос «а мы вообще сможем это '
|
|
234
|
+
+ 'посчитать?» не задавали.');
|
|
235
|
+
}
|
|
236
|
+
const unrecognised = classified.filter((r) => r.source.kind === 'unrecognised');
|
|
237
|
+
if (unrecognised.length) {
|
|
238
|
+
proven('источник значения вне закрытого списка',
|
|
239
|
+
unrecognised.map((r) => r.name + ' → «' + r.source.raw + '»'),
|
|
240
|
+
'допустимы ровно: наш журнал | наша БД | внешний API: <метод> | ручное измерение: <как>. '
|
|
241
|
+
+ '«Из аналитики» — жанр места, а не место: спросить у него ничего нельзя, ровно как у '
|
|
242
|
+
+ 'предела «разумный» или у ссылки, которую никто не открывал.');
|
|
243
|
+
}
|
|
244
|
+
const vague = classified.filter((r) => r.source.needsDetail && !r.source.detail);
|
|
245
|
+
if (vague.length) {
|
|
246
|
+
proven('источник назван, но не сказано ЧЕМ именно',
|
|
247
|
+
vague.map((r) => r.name + ' → «' + r.source.raw + '»'),
|
|
248
|
+
'у внешнего API обязан быть назван МЕТОД, а у ручного измерения — способ. Поставщик без '
|
|
249
|
+
+ 'метода не проверяем: у одного и того же сервиса одна способность есть, а соседней нет. '
|
|
250
|
+
+ 'Ручное измерение — законный ответ, но названный: «сверка пяти карточек глазами» — '
|
|
251
|
+
+ 'хороший источник и плохая приборная панель.');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// THE LOAD-BEARING CONSEQUENCE: an external-API metric must reach the one lens that looks outside
|
|
255
|
+
// the documents, and an inventory row is the only route there is.
|
|
256
|
+
const external = classified.filter((r) => r.source.kind === 'external');
|
|
257
|
+
if (external.length) {
|
|
258
|
+
const inventory = inventoryText(root);
|
|
259
|
+
const haystack = (inventory || '').toLowerCase();
|
|
260
|
+
const orphans = external.filter((r) => {
|
|
261
|
+
const token = r.source.detail.toLowerCase().replace(/^[`«"]|[`»"]$/g, '').trim();
|
|
262
|
+
return !token || !haystack.includes(token);
|
|
263
|
+
});
|
|
264
|
+
if (orphans.length) {
|
|
265
|
+
proven('метрика опирается на внешний API, которого нет в инвентаре внешних зависимостей',
|
|
266
|
+
orphans.map((r) => r.name + ' → ' + r.source.raw),
|
|
267
|
+
inventory === null
|
|
268
|
+
? 'в ' + ARCH + ' нет раздела `## External Dependencies` вовсе. Инвентарь — ЕДИНСТВЕННЫЙ '
|
|
269
|
+
+ 'маршрут, по которому метрика попадает под линзу, смотрящую НАРУЖУ документов: без '
|
|
270
|
+
+ 'строки её осуществимость не устанавливается ничем, сколько бы зелёных вердиктов '
|
|
271
|
+
+ 'прогон ни собрал.'
|
|
272
|
+
: 'инвентарь есть, но названного метода в нём нет. Метрика, назвавшая внешний API, '
|
|
273
|
+
+ 'ОБЯЗАНА породить строку инвентаря — иначе она никогда не попадёт под шестую линзу, '
|
|
274
|
+
+ 'и её осуществимость не проверит никто.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const byKind = (k) => classified.filter((r) => r.source.kind === k).length;
|
|
279
|
+
say('✅ ' + rows.length + ' метрик(и) в ' + where + ' называют источник значения из закрытого '
|
|
280
|
+
+ 'списка (журнал ' + byKind('log') + ', БД ' + byKind('db') + ', внешний API ' + byKind('external')
|
|
281
|
+
+ ', вручную ' + byKind('manual') + ')');
|
|
282
|
+
if (external.length) {
|
|
283
|
+
say(' Каждая метрика на внешнем API имеет строку инвентаря — только так она попадает под '
|
|
284
|
+
+ 'линзу, смотрящую наружу документов.');
|
|
285
|
+
}
|
|
286
|
+
say(' Ограничение: проверка НЕ ЗОВЁТ API и не знает, существует ли метод; она доказывает, что '
|
|
287
|
+
+ 'вопрос «сможем ли мы это добыть» ЗАДАН и получил один из четырёх ответов (слой 3 — сам ответ).');
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
main();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// Even an unexpected failure must not read as "clean".
|
|
295
|
+
cannotCheck('внутренняя ошибка проверки: ' + String((err && err.message) || err));
|
|
296
|
+
}
|