@nitra/cursor 1.8.104 → 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,190 +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(
182
- `.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`
204
+ failFn(
205
+ `.oxfmtrc.json ignorePatterns: додай відсутні елементи: ${missingPatterns.join(', ')} (канонічний приклад у text.mdc)`
183
206
  )
184
207
  }
185
208
  } else {
186
- fail('.oxfmtrc.json не існує створи його')
209
+ failFn(`.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`)
187
210
  }
211
+ }
188
212
 
189
- const prettierFiles = ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']
190
- for (const f of prettierFiles) {
191
- 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} — видали її`)
192
224
  }
225
+ if (pkg.prettier) failFn('package.json містить поле "prettier" — видали його')
193
226
 
194
- if (existsSync('.markdownlint-cli2.jsonc')) {
195
- try {
196
- const ml = JSON.parse(await readFile('.markdownlint-cli2.jsonc', 'utf8'))
197
- pass('.markdownlint-cli2.jsonc існує і є валідним JSON')
198
- if (ml.gitignore === true) {
199
- pass('.markdownlint-cli2.jsonc: gitignore увімкнено')
200
- } else {
201
- fail('.markdownlint-cli2.jsonc: додай на верхньому рівні "gitignore": true (див. n-text.mdc)')
202
- }
203
- } catch {
204
- fail('.markdownlint-cli2.jsonc — невалідний JSON; перевір синтаксис')
205
- }
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
+ )
206
232
  } else {
207
- fail('.markdownlint-cli2.jsonc не існує — створи згідно n-text.mdc')
233
+ passFn('Кореневі devDependencies лише @nitra/*')
208
234
  }
209
235
 
210
- if (existsSync('.cspell.json')) {
211
- 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
+ }
212
244
 
213
- if (cfg.version === '0.2') {
214
- pass('.cspell.json version: 0.2')
215
- } else {
216
- fail('.cspell.json version має бути "0.2"')
217
- }
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
+ }
218
251
 
219
- if (cfg.language) {
220
- pass(`.cspell.json language: "${cfg.language}"`)
221
- } else {
222
- fail('.cspell.json не містить поле language')
223
- }
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
+ }
224
269
 
225
- const imports = cfg.import || []
226
- if (imports.some(i => i.includes('@nitra/cspell-dict'))) {
227
- pass('.cspell.json імпортує @nitra/cspell-dict')
228
- } else {
229
- fail('.cspell.json не імпортує @nitra/cspell-dict/cspell-ext.json')
230
- }
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 || {}
231
279
 
232
- if (Array.isArray(cfg.ignorePaths)) {
233
- pass('.cspell.json містить ignorePaths')
280
+ checkPackageJsonTextDepsUsage(pkg, devDeps, passFn, failFn)
281
+ checkLintTextScript(pkg.scripts?.['lint-text'], passFn, failFn)
282
+
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')
234
289
  } else {
235
- fail('.cspell.json не містить ignorePaths')
290
+ failFn('lint-text.yml має містити крок bun run lint-text')
236
291
  }
237
292
  } else {
238
- fail('.cspell.json не існує — створи його')
293
+ failFn('.github/workflows/lint-text.yml не існує — створи згідно n-text.mdc')
239
294
  }
240
295
 
241
- const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(p))
242
- if (textRulePaths.length === 0) {
243
- 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 ]')
244
326
  } else {
245
- for (const p of textRulePaths) {
246
- const body = await readFile(p, 'utf8')
247
- verifyUkApostropheRuleParagraph(p, body, fail, pass)
248
- }
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
+ )
249
330
  }
331
+ }
250
332
 
251
- if (existsSync('package.json')) {
252
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
253
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
254
- for (const dep of ['prettier', '@nitra/prettier-config']) {
255
- if (allDeps[dep]) fail(`package.json містить залежність ${dep} — видали її`)
256
- }
257
- if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
258
-
259
- const devDeps = pkg.devDependencies || {}
260
- const nonNitraDev = Object.keys(devDeps).filter(n => !isAllowedRootDevDependency(n))
261
- if (nonNitraDev.length > 0) {
262
- fail(
263
- `Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${nonNitraDev.join(', ')} (bun.mdc)`
264
- )
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 увімкнено')
265
348
  } else {
266
- pass('Кореневі devDependencies лише @nitra/*')
349
+ fail('.markdownlint-cli2.jsonc: додай на верхньому рівні "gitignore": true (див. n-text.mdc)')
267
350
  }
351
+ } catch {
352
+ fail('.markdownlint-cli2.jsonc — невалідний JSON; перевір синтаксис')
353
+ }
354
+ }
268
355
 
269
- const cspellRange = devDeps['@nitra/cspell-dict']
270
- if (!cspellRange) {
271
- fail('@nitra/cspell-dict у devDependencies обовʼязковий для cspell bun add -d @nitra/cspell-dict@^2.0.0')
272
- } else if (cspellDictVersionAtLeast200(cspellRange)) {
273
- pass('@nitra/cspell-dict ^2.0.0+')
274
- } else {
275
- fail('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
276
- }
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
+ }
277
388
 
278
- const rootDeps = pkg.dependencies || {}
279
- if (devDeps['markdownlint-cli2'] || rootDeps['markdownlint-cli2']) {
280
- fail(
281
- 'markdownlint-cli2 не додавай у dependencies/devDependencies — лише bunx у lint-text (n-text.mdc); прибери з package.json і bun i'
282
- )
283
- }
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
284
396
 
285
- const lintText = pkg.scripts?.['lint-text']
286
- const v8rCalls = typeof lintText === 'string' ? (lintText.match(/bunx v8r/g) || []).length : 0
287
- const quietCalls = typeof lintText === 'string' ? (lintText.match(/run-v8r?\.mjs/g) || []).length : 0
288
- const eq98Hints = typeof lintText === 'string' ? (lintText.match(/eq 98/g) || []).length : 0
289
- const globsOk =
290
- typeof lintText === 'string' &&
291
- lintText.includes('**/*.json') &&
292
- lintText.includes('**/*.yml') &&
293
- lintText.includes('**/*.yaml') &&
294
- lintText.includes('**/*.toml')
295
- const legacyV8r = v8rCalls >= 4 && eq98Hints >= 4
296
- const quietBundled = quietCalls === 1
297
- const quietLegacy4x = quietCalls >= 4
298
- const v8rTextOk = legacyV8r || quietBundled || quietLegacy4x
299
- const globsRequired = legacyV8r || quietLegacy4x
300
- if (
301
- typeof lintText === 'string' &&
302
- lintText.includes('cspell') &&
303
- lintText.includes('bunx markdownlint-cli2') &&
304
- lintText.includes('**/*.mdc') &&
305
- v8rTextOk &&
306
- (!globsRequired || globsOk)
307
- ) {
308
- pass('package.json: lint-text — v8r: run-v8r.mjs (один виклик або чотири) або чотири bunx v8r з || [ $? -eq 98 ]')
309
- } else {
310
- fail(
311
- 'package.json: lint-text — v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) для json/yml/yaml/toml (див. n-text.mdc)'
312
- )
313
- }
397
+ await checkV8rIgnore(pass, fail)
398
+ await checkVscodeText(pass, fail)
399
+ await checkOxfmtRc(pass, fail)
314
400
 
315
- if (existsSync('.github/workflows/lint-text.yml')) {
316
- const wf = await readFile('.github/workflows/lint-text.yml', 'utf8')
317
- const root = parseWorkflowYaml(wf)
318
- const ok = root ? anyRunStepIncludes(root, 'bun run lint-text') : wf.includes('bun run lint-text')
319
- if (ok) {
320
- pass('lint-text.yml викликає bun run lint-text')
321
- } else {
322
- fail('lint-text.yml має містити крок bun run lint-text')
323
- }
324
- } else {
325
- fail('.github/workflows/lint-text.yml не існує — створи згідно n-text.mdc')
326
- }
401
+ for (const f of ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']) {
402
+ if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} — видали його`)
403
+ }
327
404
 
328
- if (existsSync('.cspell.json')) {
329
- const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
330
- const dictImports = (cfg.import || []).filter(i => typeof i === 'string' && i.includes('@cspell/dict-'))
331
- if (dictImports.length > 0) {
332
- fail(
333
- `.cspell.json не має імпортувати @cspell/dict-* (${dictImports.join(', ')})використовуй лише @nitra/cspell-dict/cspell-ext.json`
334
- )
335
- } else {
336
- pass('.cspell.json без прямих імпортів @cspell/dict-*')
337
- }
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)
338
414
  }
339
415
  }
340
416
 
417
+ await checkPackageJsonText(pass, fail)
418
+
341
419
  return reporter.getExitCode()
342
420
  }