@nitra/cursor 1.8.105 → 1.8.106

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.
@@ -70,79 +70,109 @@ function verifyUkApostropheRuleParagraph(filePath, body, failFn, passFn) {
70
70
  }
71
71
 
72
72
  /**
73
- * Перевіряє відповідність проєкту правилам text.mdc (oxfmt, cspell, markdownlint через bunx, v8r)
74
- * @returns {Promise<number>} 0 все OK, 1 є проблеми
73
+ * Перевіряє .v8rignore.
74
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
75
+ * @param {(msg: string) => void} failFn callback при помилці
75
76
  */
76
- export async function check() {
77
- const reporter = createCheckReporter()
78
- const { pass, fail } = reporter
79
-
80
- const v8rIgnoreRequired = ['.vscode/extensions.json', '.vscode/settings.json']
81
- if (existsSync('.v8rignore')) {
82
- const raw = await readFile('.v8rignore', 'utf8')
83
- const lines = new Set(
84
- raw
85
- .split('\n')
86
- .map(l => l.trim())
87
- .filter(l => l.length > 0 && !l.startsWith('#'))
88
- )
89
- for (const path of v8rIgnoreRequired) {
90
- if (lines.has(path)) {
91
- pass(`.v8rignore містить ${path}`)
92
- } else {
93
- fail(`.v8rignore: додай рядок "${path}" (JSON без схеми в Schema Store — див. n-text.mdc)`)
94
- }
77
+ async function checkV8rIgnore(passFn, failFn) {
78
+ const required = ['.vscode/extensions.json', '.vscode/settings.json']
79
+ if (!existsSync('.v8rignore')) {
80
+ failFn('.v8rignore не існує — створи згідно n-text.mdc (мінімум .vscode/extensions.json і .vscode/settings.json)')
81
+ return
82
+ }
83
+ const raw = await readFile('.v8rignore', 'utf8')
84
+ const lines = new Set(
85
+ raw
86
+ .split('\n')
87
+ .map(l => l.trim())
88
+ .filter(l => l.length > 0 && !l.startsWith('#'))
89
+ )
90
+ for (const path of required) {
91
+ if (lines.has(path)) {
92
+ passFn(`.v8rignore містить ${path}`)
93
+ } else {
94
+ failFn(`.v8rignore: додай рядок "${path}" (JSON без схеми в Schema Store — див. n-text.mdc)`)
95
95
  }
96
- } else {
97
- fail('.v8rignore не існує — створи згідно n-text.mdc (мінімум .vscode/extensions.json і .vscode/settings.json)')
98
96
  }
97
+ }
99
98
 
100
- if (existsSync('.vscode/extensions.json')) {
101
- try {
102
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
103
- const rec = ext.recommendations
104
- if (Array.isArray(rec) && rec.includes('DavidAnson.vscode-markdownlint')) {
105
- pass('extensions.json містить DavidAnson.vscode-markdownlint')
106
- } else {
107
- fail('extensions.json: додай "DavidAnson.vscode-markdownlint" у recommendations (див. n-text.mdc)')
108
- }
109
- if (Array.isArray(rec) && rec.includes('oxc.oxc-vscode')) {
110
- pass('extensions.json містить oxc.oxc-vscode')
99
+ /**
100
+ * Перевіряє VSCode extensions.json для текстового стека.
101
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
102
+ * @param {(msg: string) => void} failFn callback при помилці
103
+ */
104
+ async function checkVscodeTextExtensions(passFn, failFn) {
105
+ if (!existsSync('.vscode/extensions.json')) {
106
+ failFn('.vscode/extensions.json не існує створи з recommendations згідно n-text.mdc')
107
+ return
108
+ }
109
+ try {
110
+ const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
111
+ const rec = ext.recommendations
112
+ for (const id of ['DavidAnson.vscode-markdownlint', 'oxc.oxc-vscode']) {
113
+ if (Array.isArray(rec) && rec.includes(id)) {
114
+ passFn(`extensions.json містить ${id}`)
111
115
  } else {
112
- fail('extensions.json: додай "oxc.oxc-vscode" у recommendations (див. n-text.mdc)')
116
+ failFn(`extensions.json: додай "${id}" у recommendations (див. n-text.mdc)`)
113
117
  }
114
- } catch {
115
- fail('.vscode/extensions.json — невалідний JSON')
116
118
  }
117
- } else {
118
- fail('.vscode/extensions.json не існує створи з recommendations згідно n-text.mdc')
119
+ } catch {
120
+ failFn('.vscode/extensions.json — невалідний JSON')
119
121
  }
122
+ }
120
123
 
121
- if (existsSync('.vscode/settings.json')) {
122
- try {
123
- const settings = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
124
- if (settings['editor.formatOnSave'] === true) {
125
- pass('settings.json: editor.formatOnSave увімкнено')
124
+ /**
125
+ * Перевіряє VSCode settings.json для текстового стека.
126
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
127
+ * @param {(msg: string) => void} failFn callback при помилці
128
+ */
129
+ async function checkVscodeTextSettings(passFn, failFn) {
130
+ if (!existsSync('.vscode/settings.json')) {
131
+ failFn('.vscode/settings.json не існує — створи згідно n-text.mdc')
132
+ return
133
+ }
134
+ try {
135
+ const settings = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
136
+ if (settings['editor.formatOnSave'] === true) {
137
+ passFn('settings.json: editor.formatOnSave увімкнено')
138
+ } else {
139
+ failFn('settings.json: editor.formatOnSave має бути true')
140
+ }
141
+ for (const t of ['javascript', 'typescript', 'json', 'vue', 'css', 'html']) {
142
+ const key = `[${t}]`
143
+ if (settings[key]?.['editor.defaultFormatter'] === 'oxc.oxc-vscode') {
144
+ passFn(`settings.json: ${key} використовує oxc.oxc-vscode`)
126
145
  } else {
127
- fail('settings.json: editor.formatOnSave має бути true')
128
- }
129
- const fmtTypes = ['javascript', 'typescript', 'json', 'vue', 'css', 'html']
130
- for (const t of fmtTypes) {
131
- const key = `[${t}]`
132
- if (settings[key]?.['editor.defaultFormatter'] === 'oxc.oxc-vscode') {
133
- pass(`settings.json: ${key} використовує oxc.oxc-vscode`)
134
- } else {
135
- fail(`settings.json: ${key} має використовувати oxc.oxc-vscode як defaultFormatter`)
136
- }
146
+ failFn(`settings.json: ${key} має використовувати oxc.oxc-vscode як defaultFormatter`)
137
147
  }
138
- } catch {
139
- fail('.vscode/settings.json — невалідний JSON')
140
148
  }
141
- } else {
142
- fail('.vscode/settings.json не існує створи згідно n-text.mdc')
149
+ } catch {
150
+ failFn('.vscode/settings.json — невалідний JSON')
143
151
  }
152
+ }
153
+
154
+ /**
155
+ * Перевіряє VSCode extensions.json та settings.json для текстового стека.
156
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
157
+ * @param {(msg: string) => void} failFn callback при помилці
158
+ */
159
+ async function checkVscodeText(passFn, failFn) {
160
+ await checkVscodeTextExtensions(passFn, failFn)
161
+ await checkVscodeTextSettings(passFn, failFn)
162
+ }
144
163
 
145
- const expectedOxfmtKeys = [
164
+ /**
165
+ * Перевіряє .oxfmtrc.json.
166
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
167
+ * @param {(msg: string) => void} failFn callback при помилці
168
+ */
169
+ async function checkOxfmtRc(passFn, failFn) {
170
+ if (!existsSync('.oxfmtrc.json')) {
171
+ failFn('.oxfmtrc.json не існує — створи його')
172
+ return
173
+ }
174
+ const cfg = JSON.parse(await readFile('.oxfmtrc.json', 'utf8'))
175
+ const requiredKeys = [
146
176
  'arrowParens',
147
177
  'printWidth',
148
178
  'bracketSpacing',
@@ -153,188 +183,238 @@ export async function check() {
153
183
  'trailingComma',
154
184
  'useTabs'
155
185
  ]
156
- if (existsSync('.oxfmtrc.json')) {
157
- const cfg = JSON.parse(await readFile('.oxfmtrc.json', 'utf8'))
158
- const missing = expectedOxfmtKeys.filter(k => !(k in cfg))
159
- if (missing.length === 0) {
160
- pass('.oxfmtrc.json містить всі обовʼязкові ключі')
161
- } else {
162
- fail(`.oxfmtrc.json відсутні ключі: ${missing.join(', ')}`)
163
- }
164
- if (cfg.semi !== false) fail('.oxfmtrc.json: semi має бути false')
165
- if (cfg.singleQuote !== true) fail('.oxfmtrc.json: singleQuote має бути true')
166
- if (cfg.tabWidth !== 2) fail('.oxfmtrc.json: tabWidth має бути 2')
167
- if (cfg.useTabs !== false) fail('.oxfmtrc.json: useTabs має бути false')
168
- if (cfg.printWidth !== 120) fail('.oxfmtrc.json: printWidth має бути 120')
186
+ const missing = requiredKeys.filter(k => !(k in cfg))
187
+ if (missing.length === 0) {
188
+ passFn('.oxfmtrc.json містить всі обовʼязкові ключі')
189
+ } else {
190
+ failFn(`.oxfmtrc.json відсутні ключі: ${missing.join(', ')}`)
191
+ }
192
+ if (cfg.semi !== false) failFn('.oxfmtrc.json: semi має бути false')
193
+ if (cfg.singleQuote !== true) failFn('.oxfmtrc.json: singleQuote має бути true')
194
+ if (cfg.tabWidth !== 2) failFn('.oxfmtrc.json: tabWidth має бути 2')
195
+ if (cfg.useTabs !== false) failFn('.oxfmtrc.json: useTabs має бути false')
196
+ if (cfg.printWidth !== 120) failFn('.oxfmtrc.json: printWidth має бути 120')
169
197
 
170
- if (Array.isArray(cfg.ignorePatterns)) {
171
- const set = new Set(cfg.ignorePatterns)
172
- const missing = OXFMT_REQUIRED_IGNORE_PATTERNS.filter(p => !set.has(p))
173
- if (missing.length === 0) {
174
- pass('.oxfmtrc.json: ignorePatterns містить hasura/metadata та schema.graphql')
175
- } else {
176
- fail(
177
- `.oxfmtrc.json ignorePatterns: додай відсутні елементи: ${missing.join(', ')} (канонічний приклад у text.mdc)`
178
- )
179
- }
198
+ if (Array.isArray(cfg.ignorePatterns)) {
199
+ const set = new Set(cfg.ignorePatterns)
200
+ const missingPatterns = OXFMT_REQUIRED_IGNORE_PATTERNS.filter(p => !set.has(p))
201
+ if (missingPatterns.length === 0) {
202
+ passFn('.oxfmtrc.json: ignorePatterns містить hasura/metadata та schema.graphql')
180
203
  } else {
181
- fail(`.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`)
204
+ failFn(
205
+ `.oxfmtrc.json ignorePatterns: додай відсутні елементи: ${missingPatterns.join(', ')} (канонічний приклад у text.mdc)`
206
+ )
182
207
  }
183
208
  } else {
184
- fail('.oxfmtrc.json не існує створи його')
209
+ failFn(`.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`)
185
210
  }
211
+ }
186
212
 
187
- const prettierFiles = ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']
188
- for (const f of prettierFiles) {
189
- if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} видали його`)
213
+ /**
214
+ * Перевіряє залежності package.json для текстового стека.
215
+ * @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string>, prettier?: unknown }} pkg розібраний package.json
216
+ * @param {Record<string, string>} devDeps devDependencies з package.json
217
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
218
+ * @param {(msg: string) => void} failFn callback при помилці
219
+ */
220
+ function checkPackageJsonTextDepsUsage(pkg, devDeps, passFn, failFn) {
221
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
222
+ for (const dep of ['prettier', '@nitra/prettier-config']) {
223
+ if (allDeps[dep]) failFn(`package.json містить залежність ${dep} — видали її`)
190
224
  }
225
+ if (pkg.prettier) failFn('package.json містить поле "prettier" — видали його')
191
226
 
192
- if (existsSync('.markdownlint-cli2.jsonc')) {
193
- try {
194
- const ml = JSON.parse(await readFile('.markdownlint-cli2.jsonc', 'utf8'))
195
- pass('.markdownlint-cli2.jsonc існує і є валідним JSON')
196
- if (ml.gitignore === true) {
197
- pass('.markdownlint-cli2.jsonc: gitignore увімкнено')
198
- } else {
199
- fail('.markdownlint-cli2.jsonc: додай на верхньому рівні "gitignore": true (див. n-text.mdc)')
200
- }
201
- } catch {
202
- fail('.markdownlint-cli2.jsonc — невалідний JSON; перевір синтаксис')
203
- }
227
+ const nonNitraDev = Object.keys(devDeps).filter(n => !isAllowedRootDevDependency(n))
228
+ if (nonNitraDev.length > 0) {
229
+ failFn(
230
+ `Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${nonNitraDev.join(', ')} (bun.mdc)`
231
+ )
204
232
  } else {
205
- fail('.markdownlint-cli2.jsonc не існує — створи згідно n-text.mdc')
233
+ passFn('Кореневі devDependencies лише @nitra/*')
206
234
  }
207
235
 
208
- if (existsSync('.cspell.json')) {
209
- const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
236
+ const cspellRange = devDeps['@nitra/cspell-dict']
237
+ if (!cspellRange) {
238
+ failFn('@nitra/cspell-dict у devDependencies обовʼязковий для cspell — bun add -d @nitra/cspell-dict@^2.0.0')
239
+ } else if (cspellDictVersionAtLeast200(cspellRange)) {
240
+ passFn('@nitra/cspell-dict ^2.0.0+')
241
+ } else {
242
+ failFn('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
243
+ }
210
244
 
211
- if (cfg.version === '0.2') {
212
- pass('.cspell.json version: 0.2')
213
- } else {
214
- fail('.cspell.json version має бути "0.2"')
215
- }
245
+ if (devDeps['markdownlint-cli2'] || (pkg.dependencies || {})['markdownlint-cli2']) {
246
+ failFn(
247
+ 'markdownlint-cli2 не додавай у dependencies/devDependencies — лише bunx у lint-text (n-text.mdc); прибери з package.json і bun i'
248
+ )
249
+ }
250
+ }
216
251
 
217
- if (cfg.language) {
218
- pass(`.cspell.json language: "${cfg.language}"`)
219
- } else {
220
- fail('.cspell.json не містить поле language')
221
- }
252
+ /**
253
+ * Перевіряє відсутність прямих імпортів `@cspell/dict-*` у .cspell.json.
254
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
255
+ * @param {(msg: string) => void} failFn callback при помилці
256
+ */
257
+ async function checkCspellJsonDictImports(passFn, failFn) {
258
+ if (!existsSync('.cspell.json')) return
259
+ const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
260
+ const dictImports = (cfg.import || []).filter(i => typeof i === 'string' && i.includes('@cspell/dict-'))
261
+ if (dictImports.length > 0) {
262
+ failFn(
263
+ `.cspell.json не має імпортувати @cspell/dict-* (${dictImports.join(', ')}) — використовуй лише @nitra/cspell-dict/cspell-ext.json`
264
+ )
265
+ } else {
266
+ passFn('.cspell.json без прямих імпортів @cspell/dict-*')
267
+ }
268
+ }
222
269
 
223
- const imports = cfg.import || []
224
- if (imports.some(i => i.includes('@nitra/cspell-dict'))) {
225
- pass('.cspell.json імпортує @nitra/cspell-dict')
226
- } else {
227
- fail('.cspell.json не імпортує @nitra/cspell-dict/cspell-ext.json')
228
- }
270
+ /**
271
+ * Перевіряє package.json для текстового стека.
272
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
273
+ * @param {(msg: string) => void} failFn callback при помилці
274
+ */
275
+ async function checkPackageJsonText(passFn, failFn) {
276
+ if (!existsSync('package.json')) return
277
+ const pkg = JSON.parse(await readFile('package.json', 'utf8'))
278
+ const devDeps = pkg.devDependencies || {}
279
+
280
+ checkPackageJsonTextDepsUsage(pkg, devDeps, passFn, failFn)
281
+ checkLintTextScript(pkg.scripts?.['lint-text'], passFn, failFn)
229
282
 
230
- if (Array.isArray(cfg.ignorePaths)) {
231
- pass('.cspell.json містить ignorePaths')
283
+ if (existsSync('.github/workflows/lint-text.yml')) {
284
+ const wf = await readFile('.github/workflows/lint-text.yml', 'utf8')
285
+ const root = parseWorkflowYaml(wf)
286
+ const ok = root ? anyRunStepIncludes(root, 'bun run lint-text') : wf.includes('bun run lint-text')
287
+ if (ok) {
288
+ passFn('lint-text.yml викликає bun run lint-text')
232
289
  } else {
233
- fail('.cspell.json не містить ignorePaths')
290
+ failFn('lint-text.yml має містити крок bun run lint-text')
234
291
  }
235
292
  } else {
236
- fail('.cspell.json не існує — створи його')
293
+ failFn('.github/workflows/lint-text.yml не існує — створи згідно n-text.mdc')
237
294
  }
238
295
 
239
- const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(p))
240
- if (textRulePaths.length === 0) {
241
- pass('n-text.mdc / npm/mdc/text.mdc відсутні — перевірку абзацу про апостроф пропущено')
296
+ await checkCspellJsonDictImports(passFn, failFn)
297
+ }
298
+
299
+ /**
300
+ * Перевіряє скрипт lint-text на коректність v8r-виклику.
301
+ * @param {unknown} lintText параметр lintText
302
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
303
+ * @param {(msg: string) => void} failFn callback при помилці
304
+ */
305
+ function checkLintTextScript(lintText, passFn, failFn) {
306
+ const lt = typeof lintText === 'string' ? lintText : ''
307
+ const v8rCalls = (lt.match(/bunx v8r/g) || []).length
308
+ const quietCalls = (lt.match(/run-v8r?\.mjs/g) || []).length
309
+ const eq98Hints = (lt.match(/eq 98/g) || []).length
310
+ const legacyV8r = v8rCalls >= 4 && eq98Hints >= 4
311
+ const quietBundled = quietCalls === 1
312
+ const quietLegacy4x = quietCalls >= 4
313
+ const v8rTextOk = legacyV8r || quietBundled || quietLegacy4x
314
+ const globsRequired = legacyV8r || quietLegacy4x
315
+ const globsOk =
316
+ lt.includes('**/*.json') && lt.includes('**/*.yml') && lt.includes('**/*.yaml') && lt.includes('**/*.toml')
317
+ const ok =
318
+ lt &&
319
+ lt.includes('cspell') &&
320
+ lt.includes('bunx markdownlint-cli2') &&
321
+ lt.includes('**/*.mdc') &&
322
+ v8rTextOk &&
323
+ (!globsRequired || globsOk)
324
+ if (ok) {
325
+ passFn('package.json: lint-text — v8r: run-v8r.mjs (один виклик або чотири) або чотири bunx v8r з || [ $? -eq 98 ]')
242
326
  } else {
243
- for (const p of textRulePaths) {
244
- const body = await readFile(p, 'utf8')
245
- verifyUkApostropheRuleParagraph(p, body, fail, pass)
246
- }
327
+ failFn(
328
+ 'package.json: lint-text v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) для json/yml/yaml/toml (див. n-text.mdc)'
329
+ )
247
330
  }
331
+ }
248
332
 
249
- if (existsSync('package.json')) {
250
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
251
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
252
- for (const dep of ['prettier', '@nitra/prettier-config']) {
253
- if (allDeps[dep]) fail(`package.json містить залежність ${dep} — видали її`)
254
- }
255
- if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
256
-
257
- const devDeps = pkg.devDependencies || {}
258
- const nonNitraDev = Object.keys(devDeps).filter(n => !isAllowedRootDevDependency(n))
259
- if (nonNitraDev.length > 0) {
260
- fail(
261
- `Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${nonNitraDev.join(', ')} (bun.mdc)`
262
- )
333
+ /**
334
+ * Перевіряє .markdownlint-cli2.jsonc.
335
+ * @param {(msg: string) => void} pass callback при успішній перевірці
336
+ * @param {(msg: string) => void} fail callback при помилці
337
+ */
338
+ async function checkMarkdownlintConfig(pass, fail) {
339
+ if (!existsSync('.markdownlint-cli2.jsonc')) {
340
+ fail('.markdownlint-cli2.jsonc не існує — створи згідно n-text.mdc')
341
+ return
342
+ }
343
+ try {
344
+ const ml = JSON.parse(await readFile('.markdownlint-cli2.jsonc', 'utf8'))
345
+ pass('.markdownlint-cli2.jsonc існує і є валідним JSON')
346
+ if (ml.gitignore === true) {
347
+ pass('.markdownlint-cli2.jsonc: gitignore увімкнено')
263
348
  } else {
264
- pass('Кореневі devDependencies лише @nitra/*')
349
+ fail('.markdownlint-cli2.jsonc: додай на верхньому рівні "gitignore": true (див. n-text.mdc)')
265
350
  }
351
+ } catch {
352
+ fail('.markdownlint-cli2.jsonc — невалідний JSON; перевір синтаксис')
353
+ }
354
+ }
266
355
 
267
- const cspellRange = devDeps['@nitra/cspell-dict']
268
- if (!cspellRange) {
269
- fail('@nitra/cspell-dict у devDependencies обовʼязковий для cspell bun add -d @nitra/cspell-dict@^2.0.0')
270
- } else if (cspellDictVersionAtLeast200(cspellRange)) {
271
- pass('@nitra/cspell-dict ^2.0.0+')
272
- } else {
273
- fail('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
274
- }
356
+ /**
357
+ * Перевіряє .cspell.json на версію, мову, імпорт і ignorePaths.
358
+ * @param {(msg: string) => void} pass callback при успішній перевірці
359
+ * @param {(msg: string) => void} fail callback при помилці
360
+ */
361
+ async function checkCspellConfig(pass, fail) {
362
+ if (!existsSync('.cspell.json')) {
363
+ fail('.cspell.json не існує — створи його')
364
+ return
365
+ }
366
+ const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
367
+ if (cfg.version === '0.2') {
368
+ pass('.cspell.json version: 0.2')
369
+ } else {
370
+ fail('.cspell.json version має бути "0.2"')
371
+ }
372
+ if (cfg.language) {
373
+ pass(`.cspell.json language: "${cfg.language}"`)
374
+ } else {
375
+ fail('.cspell.json не містить поле language')
376
+ }
377
+ if ((cfg.import || []).some(i => i.includes('@nitra/cspell-dict'))) {
378
+ pass('.cspell.json імпортує @nitra/cspell-dict')
379
+ } else {
380
+ fail('.cspell.json не імпортує @nitra/cspell-dict/cspell-ext.json')
381
+ }
382
+ if (Array.isArray(cfg.ignorePaths)) {
383
+ pass('.cspell.json містить ignorePaths')
384
+ } else {
385
+ fail('.cspell.json не містить ignorePaths')
386
+ }
387
+ }
275
388
 
276
- const rootDeps = pkg.dependencies || {}
277
- if (devDeps['markdownlint-cli2'] || rootDeps['markdownlint-cli2']) {
278
- fail(
279
- 'markdownlint-cli2 не додавай у dependencies/devDependencies — лише bunx у lint-text (n-text.mdc); прибери з package.json і bun i'
280
- )
281
- }
389
+ /**
390
+ * Перевіряє відповідність проєкту правилам text.mdc (oxfmt, cspell, markdownlint через bunx, v8r)
391
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
392
+ */
393
+ export async function check() {
394
+ const reporter = createCheckReporter()
395
+ const { pass, fail } = reporter
282
396
 
283
- const lintText = pkg.scripts?.['lint-text']
284
- const v8rCalls = typeof lintText === 'string' ? (lintText.match(/bunx v8r/g) || []).length : 0
285
- const quietCalls = typeof lintText === 'string' ? (lintText.match(/run-v8r?\.mjs/g) || []).length : 0
286
- const eq98Hints = typeof lintText === 'string' ? (lintText.match(/eq 98/g) || []).length : 0
287
- const globsOk =
288
- typeof lintText === 'string' &&
289
- lintText.includes('**/*.json') &&
290
- lintText.includes('**/*.yml') &&
291
- lintText.includes('**/*.yaml') &&
292
- lintText.includes('**/*.toml')
293
- const legacyV8r = v8rCalls >= 4 && eq98Hints >= 4
294
- const quietBundled = quietCalls === 1
295
- const quietLegacy4x = quietCalls >= 4
296
- const v8rTextOk = legacyV8r || quietBundled || quietLegacy4x
297
- const globsRequired = legacyV8r || quietLegacy4x
298
- if (
299
- typeof lintText === 'string' &&
300
- lintText.includes('cspell') &&
301
- lintText.includes('bunx markdownlint-cli2') &&
302
- lintText.includes('**/*.mdc') &&
303
- v8rTextOk &&
304
- (!globsRequired || globsOk)
305
- ) {
306
- pass('package.json: lint-text — v8r: run-v8r.mjs (один виклик або чотири) або чотири bunx v8r з || [ $? -eq 98 ]')
307
- } else {
308
- fail(
309
- 'package.json: lint-text — v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) для json/yml/yaml/toml (див. n-text.mdc)'
310
- )
311
- }
397
+ await checkV8rIgnore(pass, fail)
398
+ await checkVscodeText(pass, fail)
399
+ await checkOxfmtRc(pass, fail)
312
400
 
313
- if (existsSync('.github/workflows/lint-text.yml')) {
314
- const wf = await readFile('.github/workflows/lint-text.yml', 'utf8')
315
- const root = parseWorkflowYaml(wf)
316
- const ok = root ? anyRunStepIncludes(root, 'bun run lint-text') : wf.includes('bun run lint-text')
317
- if (ok) {
318
- pass('lint-text.yml викликає bun run lint-text')
319
- } else {
320
- fail('lint-text.yml має містити крок bun run lint-text')
321
- }
322
- } else {
323
- fail('.github/workflows/lint-text.yml не існує — створи згідно n-text.mdc')
324
- }
401
+ for (const f of ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']) {
402
+ if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} — видали його`)
403
+ }
325
404
 
326
- if (existsSync('.cspell.json')) {
327
- const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
328
- const dictImports = (cfg.import || []).filter(i => typeof i === 'string' && i.includes('@cspell/dict-'))
329
- if (dictImports.length > 0) {
330
- fail(
331
- `.cspell.json не має імпортувати @cspell/dict-* (${dictImports.join(', ')})використовуй лише @nitra/cspell-dict/cspell-ext.json`
332
- )
333
- } else {
334
- pass('.cspell.json без прямих імпортів @cspell/dict-*')
335
- }
405
+ await checkMarkdownlintConfig(pass, fail)
406
+ await checkCspellConfig(pass, fail)
407
+
408
+ const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(p))
409
+ if (textRulePaths.length === 0) {
410
+ pass('n-text.mdc / npm/mdc/text.mdc відсутніперевірку абзацу про апостроф пропущено')
411
+ } else {
412
+ for (const p of textRulePaths) {
413
+ verifyUkApostropheRuleParagraph(p, await readFile(p, 'utf8'), fail, pass)
336
414
  }
337
415
  }
338
416
 
417
+ await checkPackageJsonText(pass, fail)
418
+
339
419
  return reporter.getExitCode()
340
420
  }