claudeos-core 2.2.0 → 2.3.0
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/CHANGELOG.md +1649 -907
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +32 -0
- package/README.es.md +32 -0
- package/README.fr.md +32 -0
- package/README.hi.md +32 -0
- package/README.ja.md +32 -0
- package/README.ko.md +1018 -986
- package/README.md +1020 -987
- package/README.ru.md +32 -0
- package/README.vi.md +1019 -987
- package/README.zh-CN.md +32 -0
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1554
- package/bin/commands/lint.js +62 -0
- package/bin/commands/memory.js +438 -438
- package/bin/lib/cli-utils.js +206 -206
- package/claude-md-validator/index.js +184 -0
- package/claude-md-validator/reporter.js +66 -0
- package/claude-md-validator/structural-checks.js +528 -0
- package/content-validator/index.js +666 -441
- package/lib/expected-guides.js +23 -23
- package/lib/expected-outputs.js +90 -90
- package/lib/language-config.js +35 -35
- package/lib/memory-scaffold.js +1058 -1054
- package/lib/plan-parser.js +165 -165
- package/lib/staged-rules.js +118 -118
- package/manifest-generator/index.js +174 -174
- package/package.json +90 -87
- package/pass-json-validator/index.js +337 -337
- package/pass-prompts/templates/common/claude-md-scaffold.md +52 -10
- package/pass-prompts/templates/common/pass3-footer.md +402 -224
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -305
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -137
- package/plan-installer/prompt-generator.js +188 -145
- package/plan-installer/scanners/scan-frontend.js +505 -473
- package/plan-installer/scanners/scan-java.js +226 -226
- package/plan-installer/scanners/scan-node.js +57 -57
- package/plan-installer/scanners/scan-python.js +85 -85
- package/plan-installer/stack-detector.js +482 -482
- package/plan-installer/structure-scanner.js +65 -65
- package/sync-checker/index.js +177 -177
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* structural-checks.js — Language-invariant structural validation for CLAUDE.md.
|
|
3
|
+
*
|
|
4
|
+
* WHY LANGUAGE-INVARIANT:
|
|
5
|
+
* claudeos-core generates CLAUDE.md in 10 different output languages. Any
|
|
6
|
+
* validation that relies on matching translated natural-language strings
|
|
7
|
+
* (forbidden section titles, heading text, etc.) must maintain a separate
|
|
8
|
+
* blocklist per language — an unbounded maintenance burden and a common
|
|
9
|
+
* source of false negatives (a new phrasing slips through because its
|
|
10
|
+
* translation wasn't listed).
|
|
11
|
+
*
|
|
12
|
+
* This module uses only structural signals that survive translation:
|
|
13
|
+
* - Markdown syntax: `^## `, `^### `, `^#### `, `^``` — not localized.
|
|
14
|
+
* - File names: `decision-log.md`, `failure-patterns.md`, etc. — literal
|
|
15
|
+
* identifiers that stay the same in every language.
|
|
16
|
+
* - Counts and positions: section count, sub-section count per section,
|
|
17
|
+
* number of times a given memory file is referenced.
|
|
18
|
+
*
|
|
19
|
+
* All checks here pass or fail identically regardless of whether the
|
|
20
|
+
* CLAUDE.md was generated in English, Korean, Japanese, Vietnamese, etc.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
const EXPECTED_H2_COUNT = 8;
|
|
26
|
+
|
|
27
|
+
// Memory file names are literal identifiers and never translated.
|
|
28
|
+
// Used as language-invariant markers for the L4 Memory sub-section.
|
|
29
|
+
const MEMORY_FILES = [
|
|
30
|
+
"decision-log.md",
|
|
31
|
+
"failure-patterns.md",
|
|
32
|
+
"compaction.md",
|
|
33
|
+
"auto-rule-update.md",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Canonical section headings. Every generated CLAUDE.md, regardless of
|
|
37
|
+
// output language, must contain the English canonical phrase in each
|
|
38
|
+
// section's heading. Translation may be appended in parentheses.
|
|
39
|
+
//
|
|
40
|
+
// This enforces cross-repo discoverability: a multi-project search like
|
|
41
|
+
// `grep "## 7. DO NOT Read"` must match every sibling CLAUDE.md in an
|
|
42
|
+
// organization. Without this invariant, one project's §7 reads
|
|
43
|
+
// "DO NOT Read (...)" and another's reads "읽지 말 것 (DO NOT Read)" —
|
|
44
|
+
// structurally identical but cosmetically divergent, breaking grep.
|
|
45
|
+
//
|
|
46
|
+
// Each entry's value is a case-insensitive substring that MUST appear
|
|
47
|
+
// in the heading line. The substrings are chosen to be unambiguous and
|
|
48
|
+
// resistant to translators rearranging punctuation or spacing.
|
|
49
|
+
const CANONICAL_HEADING_TOKEN = {
|
|
50
|
+
1: "Role Definition",
|
|
51
|
+
2: "Project Overview",
|
|
52
|
+
3: "Build", // matches "Build & Run Commands", "Build and Run", etc.
|
|
53
|
+
4: "Core Architecture",
|
|
54
|
+
5: "Directory Structure",
|
|
55
|
+
6: "Standard", // matches "Standard / Rules / Skills Reference" family
|
|
56
|
+
7: "DO NOT Read",
|
|
57
|
+
8: "Memory", // matches "Common Rules & Memory (L4)" family
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Per-section sub-section (###) count invariants from the scaffold.
|
|
61
|
+
// Keys are 1-based section indices.
|
|
62
|
+
const H3_COUNT_BY_SECTION = {
|
|
63
|
+
4: { min: 3, max: 4, name: "Core Architecture" }, // Overall / Data Flow / Core Patterns [/ Absent]
|
|
64
|
+
6: { exact: 3, name: "Standard / Rules / Skills Reference" },
|
|
65
|
+
8: { exact: 2, name: "Common Rules & Memory (L4)" },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Per-section sub-sub-section (####) count invariants.
|
|
69
|
+
const H4_COUNT_BY_SECTION = {
|
|
70
|
+
8: { exact: 2, name: "L4 Memory Files + Memory Workflow" },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Split markdown content into sections keyed by top-level (## ) headings.
|
|
75
|
+
*
|
|
76
|
+
* Respects fenced code blocks: `## ` inside a fenced block is NOT a heading.
|
|
77
|
+
* (Scaffold templates contain example `##` lines inside ```markdown blocks
|
|
78
|
+
* that must not be counted as real headings.)
|
|
79
|
+
*
|
|
80
|
+
* Handles BOM (U+FEFF) at the start of the file: some Windows editors and
|
|
81
|
+
* cross-platform generators prepend a BOM to UTF-8 files. Without stripping
|
|
82
|
+
* it, the first `##` line reads as `\ufeff##` and never matches the heading
|
|
83
|
+
* regex, silently under-counting sections by 1.
|
|
84
|
+
*
|
|
85
|
+
* Returns: Array<{ index, title, body, startLine, endLine }>
|
|
86
|
+
* - index: 0-based section index (Section 1 = index 0)
|
|
87
|
+
* - title: heading text with `## ` stripped
|
|
88
|
+
* - body: lines between this heading and the next `## ` heading (exclusive)
|
|
89
|
+
* - startLine / endLine: 1-based line numbers for error reporting
|
|
90
|
+
*/
|
|
91
|
+
function splitByH2(content) {
|
|
92
|
+
// Strip UTF-8 BOM if present — it otherwise prevents the first ## from
|
|
93
|
+
// matching `^## ` because the line starts with \ufeff instead.
|
|
94
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
95
|
+
content = content.slice(1);
|
|
96
|
+
}
|
|
97
|
+
const lines = content.split(/\r?\n/);
|
|
98
|
+
const sections = [];
|
|
99
|
+
let current = null;
|
|
100
|
+
let inFence = false;
|
|
101
|
+
let fenceMarker = null;
|
|
102
|
+
|
|
103
|
+
lines.forEach((line, idx) => {
|
|
104
|
+
const trimmed = line.trimStart();
|
|
105
|
+
// Track fenced code blocks so ## inside them is ignored.
|
|
106
|
+
// Markdown allows both ``` and ~~~ fences.
|
|
107
|
+
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
|
|
108
|
+
if (fenceMatch) {
|
|
109
|
+
if (!inFence) {
|
|
110
|
+
inFence = true;
|
|
111
|
+
fenceMarker = fenceMatch[1][0]; // ` or ~
|
|
112
|
+
} else if (trimmed.startsWith(fenceMarker)) {
|
|
113
|
+
inFence = false;
|
|
114
|
+
fenceMarker = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!inFence && /^## (?!#)/.test(line)) {
|
|
119
|
+
// Close previous section
|
|
120
|
+
if (current) {
|
|
121
|
+
current.endLine = idx; // line before the new heading
|
|
122
|
+
sections.push(current);
|
|
123
|
+
}
|
|
124
|
+
current = {
|
|
125
|
+
index: sections.length,
|
|
126
|
+
title: line.replace(/^## /, "").trim(),
|
|
127
|
+
body: [],
|
|
128
|
+
startLine: idx + 1,
|
|
129
|
+
endLine: null,
|
|
130
|
+
};
|
|
131
|
+
} else if (current) {
|
|
132
|
+
current.body.push(line);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (current) {
|
|
137
|
+
current.endLine = lines.length;
|
|
138
|
+
sections.push(current);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return sections;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Count lines matching a regex within a section's body, skipping fenced
|
|
146
|
+
* code blocks.
|
|
147
|
+
*/
|
|
148
|
+
function countInSection(section, regex) {
|
|
149
|
+
let count = 0;
|
|
150
|
+
let inFence = false;
|
|
151
|
+
let fenceMarker = null;
|
|
152
|
+
|
|
153
|
+
for (const line of section.body) {
|
|
154
|
+
const trimmed = line.trimStart();
|
|
155
|
+
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
|
|
156
|
+
if (fenceMatch) {
|
|
157
|
+
if (!inFence) {
|
|
158
|
+
inFence = true;
|
|
159
|
+
fenceMarker = fenceMatch[1][0];
|
|
160
|
+
} else if (trimmed.startsWith(fenceMarker)) {
|
|
161
|
+
inFence = false;
|
|
162
|
+
fenceMarker = null;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!inFence && regex.test(line)) count++;
|
|
167
|
+
}
|
|
168
|
+
return count;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function countH3InSection(section) {
|
|
172
|
+
return countInSection(section, /^### (?!#)/);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function countH4InSection(section) {
|
|
176
|
+
return countInSection(section, /^#### (?!#)/);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Individual checks ────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* S1 — Top-level section count must be exactly 8.
|
|
183
|
+
*/
|
|
184
|
+
function checkH2Count(sections) {
|
|
185
|
+
const actual = sections.length;
|
|
186
|
+
return {
|
|
187
|
+
id: "S1",
|
|
188
|
+
pass: actual === EXPECTED_H2_COUNT,
|
|
189
|
+
actual,
|
|
190
|
+
expected: EXPECTED_H2_COUNT,
|
|
191
|
+
message:
|
|
192
|
+
actual === EXPECTED_H2_COUNT
|
|
193
|
+
? null
|
|
194
|
+
: `Expected exactly ${EXPECTED_H2_COUNT} top-level (## ) sections, found ${actual}.`,
|
|
195
|
+
severity: "error",
|
|
196
|
+
remediation:
|
|
197
|
+
actual > EXPECTED_H2_COUNT
|
|
198
|
+
? `Remove surplus section(s) starting at section ${EXPECTED_H2_COUNT + 1}. ` +
|
|
199
|
+
`Merge content into the appropriate Section 1-8 or move to .claude/rules/*.`
|
|
200
|
+
: actual < EXPECTED_H2_COUNT
|
|
201
|
+
? `Add the missing section(s). Canonical order: Role Definition, Project Overview, ` +
|
|
202
|
+
`Build & Run Commands, Core Architecture, Directory Structure, ` +
|
|
203
|
+
`Standard / Rules / Skills Reference, DO NOT Read, Common Rules & Memory (L4).`
|
|
204
|
+
: null,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* S3-S5 — Sub-section (### ) count per section.
|
|
210
|
+
* Checks Section 4, 6, 8 against H3_COUNT_BY_SECTION.
|
|
211
|
+
*/
|
|
212
|
+
function checkH3Counts(sections) {
|
|
213
|
+
const results = [];
|
|
214
|
+
for (const [sectionNumStr, rule] of Object.entries(H3_COUNT_BY_SECTION)) {
|
|
215
|
+
const sectionNum = parseInt(sectionNumStr, 10);
|
|
216
|
+
const sectionIdx = sectionNum - 1;
|
|
217
|
+
if (sectionIdx >= sections.length) continue;
|
|
218
|
+
|
|
219
|
+
const section = sections[sectionIdx];
|
|
220
|
+
const actual = countH3InSection(section);
|
|
221
|
+
let pass = false;
|
|
222
|
+
let expected = "";
|
|
223
|
+
|
|
224
|
+
if (rule.exact !== undefined) {
|
|
225
|
+
pass = actual === rule.exact;
|
|
226
|
+
expected = String(rule.exact);
|
|
227
|
+
} else if (rule.min !== undefined || rule.max !== undefined) {
|
|
228
|
+
pass =
|
|
229
|
+
(rule.min === undefined || actual >= rule.min) &&
|
|
230
|
+
(rule.max === undefined || actual <= rule.max);
|
|
231
|
+
expected = `${rule.min || 0}-${rule.max || "∞"}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
results.push({
|
|
235
|
+
id: `S-H3-${sectionNum}`,
|
|
236
|
+
pass,
|
|
237
|
+
actual,
|
|
238
|
+
expected,
|
|
239
|
+
section: sectionNum,
|
|
240
|
+
sectionName: rule.name,
|
|
241
|
+
message: pass
|
|
242
|
+
? null
|
|
243
|
+
: `Section ${sectionNum} (${rule.name}) must have ${expected} ### sub-sections, found ${actual}.`,
|
|
244
|
+
severity: "error",
|
|
245
|
+
remediation: pass
|
|
246
|
+
? null
|
|
247
|
+
: actual > (rule.max || rule.exact)
|
|
248
|
+
? `Remove or merge surplus ### sub-section(s) within Section ${sectionNum}.`
|
|
249
|
+
: `Add missing required ### sub-section(s) within Section ${sectionNum}.`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return results;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* S-H4 — #### sub-sub-section count per section.
|
|
257
|
+
* Section 8 must have exactly 2 #### headings (L4 Memory Files + Memory Workflow).
|
|
258
|
+
*/
|
|
259
|
+
function checkH4Counts(sections) {
|
|
260
|
+
const results = [];
|
|
261
|
+
for (const [sectionNumStr, rule] of Object.entries(H4_COUNT_BY_SECTION)) {
|
|
262
|
+
const sectionNum = parseInt(sectionNumStr, 10);
|
|
263
|
+
const sectionIdx = sectionNum - 1;
|
|
264
|
+
if (sectionIdx >= sections.length) continue;
|
|
265
|
+
|
|
266
|
+
const section = sections[sectionIdx];
|
|
267
|
+
const actual = countH4InSection(section);
|
|
268
|
+
const pass = actual === rule.exact;
|
|
269
|
+
|
|
270
|
+
results.push({
|
|
271
|
+
id: `S-H4-${sectionNum}`,
|
|
272
|
+
pass,
|
|
273
|
+
actual,
|
|
274
|
+
expected: rule.exact,
|
|
275
|
+
section: sectionNum,
|
|
276
|
+
sectionName: rule.name,
|
|
277
|
+
message: pass
|
|
278
|
+
? null
|
|
279
|
+
: `Section ${sectionNum} must have exactly ${rule.exact} #### headings (${rule.name}), found ${actual}.`,
|
|
280
|
+
severity: "error",
|
|
281
|
+
remediation: pass
|
|
282
|
+
? null
|
|
283
|
+
: `Adjust #### headings within Section ${sectionNum}. Expected: L4 Memory Files, Memory Workflow.`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Count how many times a memory filename appears as the primary cell of
|
|
291
|
+
* a table row — i.e., a row whose first non-whitespace table cell is
|
|
292
|
+
* the filename (usually wrapped in backticks and preceded by the memory
|
|
293
|
+
* directory path).
|
|
294
|
+
*
|
|
295
|
+
* Pattern: `| \`claudeos-core/memory/<filename>\` | ... | ... |`
|
|
296
|
+
*
|
|
297
|
+
* Fence-aware: table-row lines inside fenced code blocks (```, ~~~) are
|
|
298
|
+
* NOT counted. This handles the legitimate case where scaffold examples
|
|
299
|
+
* or user documentation show a sample L4 Memory Files table inside a
|
|
300
|
+
* code block — those are illustrations, not declarations.
|
|
301
|
+
*
|
|
302
|
+
* This distinguishes the canonical "one row per memory file" declaration
|
|
303
|
+
* from incidental prose mentions in workflow text (e.g., "3. append to
|
|
304
|
+
* `decision-log.md`"), which are expected to appear multiple times in
|
|
305
|
+
* normal Section 8 content.
|
|
306
|
+
*/
|
|
307
|
+
function countMemoryFileTableRows(content, filename) {
|
|
308
|
+
const escaped = filename.replace(/\./g, "\\.");
|
|
309
|
+
// Matches a markdown table row whose first cell references the memory
|
|
310
|
+
// file path. Tolerates leading whitespace and either `memory/filename`
|
|
311
|
+
// or just `filename` wrapped in backticks.
|
|
312
|
+
const rowRegex = new RegExp(`^\\s*\\|\\s*\`[^\`]*${escaped}\`\\s*\\|`);
|
|
313
|
+
|
|
314
|
+
let count = 0;
|
|
315
|
+
let inFence = false;
|
|
316
|
+
let fenceMarker = null;
|
|
317
|
+
for (const line of content.split(/\r?\n/)) {
|
|
318
|
+
const trimmed = line.trimStart();
|
|
319
|
+
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
|
|
320
|
+
if (fenceMatch) {
|
|
321
|
+
if (!inFence) {
|
|
322
|
+
inFence = true;
|
|
323
|
+
fenceMarker = fenceMatch[1][0];
|
|
324
|
+
} else if (trimmed.startsWith(fenceMarker)) {
|
|
325
|
+
inFence = false;
|
|
326
|
+
fenceMarker = null;
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!inFence && rowRegex.test(line)) count++;
|
|
331
|
+
}
|
|
332
|
+
return count;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* M1-M4 — Each memory file appears in EXACTLY ONE table row.
|
|
337
|
+
*
|
|
338
|
+
* This is the core detector for the §9 re-declaration anti-pattern.
|
|
339
|
+
*
|
|
340
|
+
* Why table-row matching instead of raw mention counting:
|
|
341
|
+
* The scaffold's Section 8 prose (workflow steps, remediation guidance)
|
|
342
|
+
* legitimately mentions memory filenames multiple times. What must not
|
|
343
|
+
* appear twice is the TABLE ROW declaring the file — the anti-pattern
|
|
344
|
+
* is a duplicate L4 Memory Files table, not prose references.
|
|
345
|
+
*
|
|
346
|
+
* Language-invariance: filenames (`decision-log.md`, etc.) and markdown
|
|
347
|
+
* table syntax (`| ... |`) are not translated. This check behaves
|
|
348
|
+
* identically in every output language.
|
|
349
|
+
*/
|
|
350
|
+
function checkMemoryFileUniqueness(content) {
|
|
351
|
+
return MEMORY_FILES.map((filename) => {
|
|
352
|
+
const actual = countMemoryFileTableRows(content, filename);
|
|
353
|
+
const pass = actual === 1;
|
|
354
|
+
|
|
355
|
+
let message = null;
|
|
356
|
+
let remediation = null;
|
|
357
|
+
if (!pass) {
|
|
358
|
+
if (actual > 1) {
|
|
359
|
+
message =
|
|
360
|
+
`Memory file "${filename}" appears in ${actual} table rows (expected 1). ` +
|
|
361
|
+
`This indicates L4 memory table re-declaration (v2.2.0 anti-pattern).`;
|
|
362
|
+
remediation =
|
|
363
|
+
`Keep only the table row in Section 8 sub-section 2 (L4 Memory). ` +
|
|
364
|
+
`Delete any duplicate rows — often found in a surplus §9 section.`;
|
|
365
|
+
} else {
|
|
366
|
+
message =
|
|
367
|
+
`Memory file "${filename}" is missing from the L4 Memory Files table.`;
|
|
368
|
+
remediation =
|
|
369
|
+
`Add a row for "${filename}" to the L4 Memory Files table in ` +
|
|
370
|
+
`Section 8, sub-section 2.`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
id: `M-${filename}`,
|
|
376
|
+
pass,
|
|
377
|
+
actual,
|
|
378
|
+
expected: 1,
|
|
379
|
+
file: filename,
|
|
380
|
+
message,
|
|
381
|
+
severity: "error",
|
|
382
|
+
remediation,
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* F2 — Memory file table rows must be confined to Section 8.
|
|
389
|
+
*
|
|
390
|
+
* An L4 Memory Files table row appearing in Section 6, §9, or anywhere
|
|
391
|
+
* outside Section 8 sub-section 2 is the canonical §9 anti-pattern:
|
|
392
|
+
* the memory table has been re-declared in a surplus section.
|
|
393
|
+
*
|
|
394
|
+
* Only table rows are counted, not prose mentions. This is the same
|
|
395
|
+
* rationale as checkMemoryFileUniqueness — workflow text legitimately
|
|
396
|
+
* references memory filenames in multiple places.
|
|
397
|
+
*/
|
|
398
|
+
function checkMemoryScopedToSection8(sections, content) {
|
|
399
|
+
if (sections.length < 8) return [];
|
|
400
|
+
const s8 = sections[7];
|
|
401
|
+
const s8Body = s8.body.join("\n");
|
|
402
|
+
const results = [];
|
|
403
|
+
|
|
404
|
+
for (const filename of MEMORY_FILES) {
|
|
405
|
+
const totalRows = countMemoryFileTableRows(content, filename);
|
|
406
|
+
const s8Rows = countMemoryFileTableRows(s8Body, filename);
|
|
407
|
+
const outside = totalRows - s8Rows;
|
|
408
|
+
|
|
409
|
+
if (outside > 0) {
|
|
410
|
+
results.push({
|
|
411
|
+
id: `F2-${filename}`,
|
|
412
|
+
pass: false,
|
|
413
|
+
actual: outside,
|
|
414
|
+
expected: 0,
|
|
415
|
+
file: filename,
|
|
416
|
+
message:
|
|
417
|
+
`Memory file "${filename}" has ${outside} table row(s) outside Section 8.`,
|
|
418
|
+
severity: "error",
|
|
419
|
+
remediation:
|
|
420
|
+
`The L4 Memory Files table belongs only in Section 8 sub-section 2. ` +
|
|
421
|
+
`Remove the surplus row(s) (typically in a duplicate §9 section).`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return results;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* S2 — Each ## section has non-trivial body content (at least 2 non-empty lines).
|
|
431
|
+
*
|
|
432
|
+
* Catches the case where a section heading exists but body was never filled.
|
|
433
|
+
*/
|
|
434
|
+
function checkSectionsHaveContent(sections) {
|
|
435
|
+
return sections.map((section, i) => {
|
|
436
|
+
const nonEmpty = section.body.filter((l) => l.trim().length > 0).length;
|
|
437
|
+
const pass = nonEmpty >= 2;
|
|
438
|
+
return {
|
|
439
|
+
id: `S2-${i + 1}`,
|
|
440
|
+
pass,
|
|
441
|
+
actual: nonEmpty,
|
|
442
|
+
expected: ">= 2",
|
|
443
|
+
section: i + 1,
|
|
444
|
+
sectionTitle: section.title,
|
|
445
|
+
message: pass
|
|
446
|
+
? null
|
|
447
|
+
: `Section ${i + 1} ("${section.title}") appears empty or near-empty (${nonEmpty} non-empty lines).`,
|
|
448
|
+
severity: "warning",
|
|
449
|
+
remediation: pass
|
|
450
|
+
? null
|
|
451
|
+
: `Populate Section ${i + 1} per the scaffold's per-section rules.`,
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* T1 — Each `## N.` section heading contains the English canonical token.
|
|
458
|
+
*
|
|
459
|
+
* Enforces the scaffold's "English canonical primary + translation
|
|
460
|
+
* parenthetical" rule. Without this check, two projects in the same
|
|
461
|
+
* organization can end up with §7 headings like:
|
|
462
|
+
*
|
|
463
|
+
* project-A: ## 7. DO NOT Read (직접 읽지 말아야 할 파일)
|
|
464
|
+
* project-B: ## 7. 읽지 말 것 (Files Not to Be Read Directly)
|
|
465
|
+
*
|
|
466
|
+
* Both are "equivalent in meaning" (what the old scaffold asked for) but
|
|
467
|
+
* multi-repo grep breaks: `grep "## 7. DO NOT Read"` matches one and not
|
|
468
|
+
* the other. This check restores cross-repo discoverability while leaving
|
|
469
|
+
* the BODY of every section free to be written in any target language.
|
|
470
|
+
*
|
|
471
|
+
* The check is a case-insensitive substring match on the heading line.
|
|
472
|
+
* Short canonical tokens (e.g., "Build", "Standard", "Memory") are chosen
|
|
473
|
+
* so translators can reorder or rephrase freely without breaking them.
|
|
474
|
+
*/
|
|
475
|
+
function checkCanonicalHeadings(sections) {
|
|
476
|
+
const results = [];
|
|
477
|
+
// Check only as many sections as we have — if the section count is
|
|
478
|
+
// off, checkH2Count (S1) already reports that; don't stack errors.
|
|
479
|
+
const upTo = Math.min(sections.length, EXPECTED_H2_COUNT);
|
|
480
|
+
for (let i = 0; i < upTo; i++) {
|
|
481
|
+
const sectionNum = i + 1;
|
|
482
|
+
const expectedToken = CANONICAL_HEADING_TOKEN[sectionNum];
|
|
483
|
+
const heading = sections[i].title || "";
|
|
484
|
+
const pass = heading.toLowerCase().includes(expectedToken.toLowerCase());
|
|
485
|
+
results.push({
|
|
486
|
+
id: `T1-${sectionNum}`,
|
|
487
|
+
pass,
|
|
488
|
+
actual: heading,
|
|
489
|
+
expected: `heading containing "${expectedToken}"`,
|
|
490
|
+
section: sectionNum,
|
|
491
|
+
sectionTitle: heading,
|
|
492
|
+
message: pass
|
|
493
|
+
? null
|
|
494
|
+
: `Section ${sectionNum} heading must contain the English canonical ` +
|
|
495
|
+
`token "${expectedToken}". Found: "${heading}".`,
|
|
496
|
+
severity: "error",
|
|
497
|
+
remediation: pass
|
|
498
|
+
? null
|
|
499
|
+
: `Rewrite the heading with the English canonical phrase as primary ` +
|
|
500
|
+
`text. Translation may be appended in parentheses. ` +
|
|
501
|
+
`Example: "## ${sectionNum}. ${expectedToken}" or ` +
|
|
502
|
+
`"## ${sectionNum}. ${expectedToken} (translation)".`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return results;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
module.exports = {
|
|
509
|
+
// High-level API
|
|
510
|
+
splitByH2,
|
|
511
|
+
// Individual checks
|
|
512
|
+
checkH2Count,
|
|
513
|
+
checkH3Counts,
|
|
514
|
+
checkH4Counts,
|
|
515
|
+
checkMemoryFileUniqueness,
|
|
516
|
+
checkMemoryScopedToSection8,
|
|
517
|
+
checkSectionsHaveContent,
|
|
518
|
+
checkCanonicalHeadings,
|
|
519
|
+
// Utilities (exported for tests)
|
|
520
|
+
countH3InSection,
|
|
521
|
+
countH4InSection,
|
|
522
|
+
// Constants
|
|
523
|
+
EXPECTED_H2_COUNT,
|
|
524
|
+
MEMORY_FILES,
|
|
525
|
+
H3_COUNT_BY_SECTION,
|
|
526
|
+
H4_COUNT_BY_SECTION,
|
|
527
|
+
CANONICAL_HEADING_TOKEN,
|
|
528
|
+
};
|