@nitra/cursor 1.8.208 → 1.8.210

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.
@@ -30,6 +30,8 @@ import {
30
30
  findBunSqlPerRequestConnectionInText,
31
31
  findBunSqlPgLeftoverCallInText,
32
32
  findBunSqlUnsafeUseWithoutAllowMarkerInText,
33
+ findPgFormatLikeQueryWrapperInText,
34
+ findPgFormatShimDefinitionInText,
33
35
  findUnsafeBunSqlDynamicSqlListInText,
34
36
  findUnsafeBunSqlInListMissingEmptyGuardInText,
35
37
  isBunSqlScanSourceFile,
@@ -67,13 +69,21 @@ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
67
69
  * @param {string[]} sourcePaths абсолютні шляхи джерел
68
70
  * @param {string} repoRoot абсолютний шлях до кореня
69
71
  * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
70
- * @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number }>}
72
+ * @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number, pgFormatShim: number, queryWrapper: number }>}
71
73
  * `hasBunSqlImport` — чи знайдено хоч один `import { sql|SQL } from 'bun'` у джерелах;
72
74
  * решта — кількість порушень кожного типу.
73
75
  */
74
76
  async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
75
77
  const { fail } = reporter
76
- const counts = { perRequest: 0, unsafeCall: 0, dynamicList: 0, inListGuard: 0, pgLeftover: 0 }
78
+ const counts = {
79
+ perRequest: 0,
80
+ unsafeCall: 0,
81
+ dynamicList: 0,
82
+ inListGuard: 0,
83
+ pgLeftover: 0,
84
+ pgFormatShim: 0,
85
+ queryWrapper: 0
86
+ }
77
87
  let hasBunSqlImport = false
78
88
 
79
89
  for (const absPath of sourcePaths) {
@@ -93,7 +103,7 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
93
103
  * @param {string} content вміст файлу
94
104
  * @param {string} rel posix-шлях відносно `repoRoot`
95
105
  * @param {(msg: string) => void} fail callback при помилці
96
- * @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number }} counts акумулятори
106
+ * @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number, pgFormatShim: number, queryWrapper: number }} counts акумулятори
97
107
  * @returns {void}
98
108
  */
99
109
  function scanFileForBunSqlPatterns(content, rel, fail, counts) {
@@ -134,6 +144,30 @@ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
134
144
  counts.inListGuard++
135
145
  fail(messageForBunSqlInListGuard(rel, v))
136
146
  }
147
+ for (const v of findPgFormatShimDefinitionInText(content, rel)) {
148
+ counts.pgFormatShim++
149
+ if (v.kind === 'format_function') {
150
+ fail(
151
+ `js-bun-db: ${rel}:${v.line} — функція ${JSON.stringify(v.name)} виглядає як pg-format-сумісний шим ` +
152
+ `(тіло містить %L / %I / %s). Видали шим і переведи всі call-site на tagged template ` +
153
+ `sql\`...\${value}...\` (js-bun-db.mdc): ${v.snippet}`
154
+ )
155
+ } else {
156
+ fail(
157
+ `js-bun-db: ${rel}:${v.line} — ${JSON.stringify(v.name)} — це pg-format-специфічний escape-хелпер; ` +
158
+ `з Bun SQL він не потрібен (параметризація через tagged template), видали і перепиши call-site ` +
159
+ `(js-bun-db.mdc): ${v.snippet}`
160
+ )
161
+ }
162
+ }
163
+ for (const v of findPgFormatLikeQueryWrapperInText(content, rel)) {
164
+ counts.queryWrapper++
165
+ fail(
166
+ `js-bun-db: ${rel}:${v.line} — query(text, params)-обгортка над <obj>.unsafe(...) — це прихований ` +
167
+ `pg-сумісний шим. Видали обгортку (pgRead/pgWrite/db.query) і переведи всі call-site на tagged template ` +
168
+ `sql\`...\${value}...\` (js-bun-db.mdc): ${v.snippet}`
169
+ )
170
+ }
137
171
  }
138
172
 
139
173
  /**
@@ -194,7 +228,7 @@ export async function check() {
194
228
  return reporter.getExitCode()
195
229
  }
196
230
 
197
- const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard, pgLeftover } =
231
+ const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard, pgLeftover, pgFormatShim, queryWrapper } =
198
232
  await scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter)
199
233
 
200
234
  if (!hasBunSqlImport) {
@@ -220,6 +254,12 @@ export async function check() {
220
254
  if (inListGuard === 0) {
221
255
  pass('js-bun-db: усі IN-списки винесені у змінні та мають перевірку на пустоту з throw')
222
256
  }
257
+ if (pgFormatShim === 0) {
258
+ pass('js-bun-db: немає pg-format-сумісних шимів (format/quoteLiteral/quoteIdent/...) у файлах з Bun SQL')
259
+ }
260
+ if (queryWrapper === 0) {
261
+ pass('js-bun-db: немає query(text, params)-обгорток над unsafe(...) у файлах з Bun SQL')
262
+ }
223
263
 
224
264
  return reporter.getExitCode()
225
265
  }
@@ -16,7 +16,6 @@ import { readFile } from 'node:fs/promises'
16
16
  import { dirname, join } from 'node:path'
17
17
  import { fileURLToPath } from 'node:url'
18
18
 
19
- import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
20
19
  import { createCheckReporter } from './utils/check-reporter.mjs'
21
20
 
22
21
  /** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
@@ -34,7 +33,6 @@ export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vsc
34
33
 
35
34
  const WHITESPACE_RE = /\s+/gu
36
35
  const NON_DIGITS_RE = /\D+/u
37
- const OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
38
36
 
39
37
  /**
40
38
  * Нормалізує рядок скрипта для порівняння (зайві пробіли).
@@ -247,41 +245,11 @@ async function checkEslintConfig(passFn, failFn) {
247
245
  }
248
246
  }
249
247
 
250
- /**
251
- * Перевіряє залежності lint-js у package.json (prettier, `@nitra/eslint-config`).
252
- * @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} pkg parsed package.json
253
- * @param {(msg: string) => void} passFn callback при успішній перевірці
254
- * @param {(msg: string) => void} failFn callback при помилці
255
- */
256
- function checkPackageJsonLintDeps(pkg, passFn, failFn) {
257
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
258
- if (allDeps.prettier) {
259
- failFn('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
260
- } else {
261
- passFn('package.json не містить prettier')
262
- }
263
- if (allDeps['@nitra/prettier-config']) {
264
- failFn('package.json: видали @nitra/prettier-config (js-lint.mdc)')
265
- } else {
266
- passFn('package.json не містить @nitra/prettier-config')
267
- }
268
-
269
- const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
270
- if (nitraEslint) {
271
- passFn('@nitra/eslint-config є в devDependencies')
272
- if (nitraEslintConfigMeetsMinVersion(nitraEslint)) {
273
- passFn(
274
- '@nitra/eslint-config: мінімум 3.9.2 (no-restricted-syntax для ForInStatement з 3.8.0 + вбудований ignore "**/adr/**" з 3.9.2 + @e18e/eslint-plugin транзитивно, js-lint.mdc)'
275
- )
276
- } else {
277
- failFn(
278
- '@nitra/eslint-config: онови до мінімум "^3.9.2" — з 3.9.2 у getConfig вбудовано ignore для "**/adr/**" (ADR-документи не валідуються), плюс транзитивний @e18e/eslint-plugin для oxlint і заборона for...in з 3.8.0 (js-lint.mdc)'
279
- )
280
- }
281
- } else {
282
- failFn('@nitra/eslint-config відсутній в devDependencies — додай: bun add -d @nitra/eslint-config')
283
- }
284
- }
248
+ // Перевірки `prettier` / `@nitra/prettier-config` у залежностях (text.mdc) і
249
+ // `@nitra/eslint-config 3.9.2` тепер у Rego: відповідно
250
+ // `npm/policy/text/package_json/` і `npm/policy/js_lint/package_json/`. Тут
251
+ // лишилася лише workspace-ітерація для `type: "module"` і engines, бо js_lint
252
+ // Rego запускається лише на кореневому `package.json`.
285
253
 
286
254
  /**
287
255
  * Перевіряє, що package.json має `"type": "module"`.
@@ -359,36 +327,18 @@ function checkEnginesBun(label, pkg, passFn, failFn) {
359
327
  }
360
328
 
361
329
  /**
362
- * Перевіряє package.json на lint-js, prettier, eslint-config, engines.node.
330
+ * Workspace-ітерація: для кожного workspace `package.json` перевіряємо
331
+ * `type: "module"` і `engines.{node,bun}`. Кореневий `package.json` ці поля
332
+ * валідує `npm/policy/js_lint/package_json/`; lint-js скрипт і `@nitra/eslint-config`
333
+ * — теж у Rego.
363
334
  * @param {(msg: string) => void} passFn callback при успішній перевірці
364
335
  * @param {(msg: string) => void} failFn callback при помилці
365
336
  */
366
337
  async function checkPackageJsonJsLint(passFn, failFn) {
367
338
  if (!existsSync('package.json')) return
368
339
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
369
-
370
- checkPackageJsonTypeModule('package.json', pkg, passFn, failFn)
371
-
372
340
  const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : []
373
341
  await checkWorkspacePackages(workspaces, passFn, failFn)
374
-
375
- const lintJs = pkg.scripts?.['lint-js']
376
- if (lintJs) {
377
- passFn('package.json містить скрипт lint-js')
378
- if (isCanonicalLintJs(String(lintJs))) {
379
- passFn(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
380
- } else {
381
- failFn(
382
- `lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(String(lintJs)))}`
383
- )
384
- }
385
- } else {
386
- failFn(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
387
- }
388
-
389
- checkPackageJsonLintDeps(pkg, passFn, failFn)
390
- checkEnginesNode('package.json', pkg, passFn, failFn)
391
- checkEnginesBun('package.json', pkg, passFn, failFn)
392
342
  }
393
343
 
394
344
  /**
@@ -457,67 +407,16 @@ async function checkVscodeExtensions(passFn, failFn) {
457
407
  }
458
408
 
459
409
  /**
460
- * Перевіряє lint-js.yml workflow (fallback текстовий пошук).
461
- * @param {string} content вміст workflow файлу
462
- * @param {(msg: string) => void} passFn callback при успішній перевірці
463
- * @param {(msg: string) => void} failFn callback при помилці
464
- */
465
- function checkLintJsWorkflowFallback(content, passFn, failFn) {
466
- const checks = [
467
- ['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
468
- ['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
469
- ['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
470
- ['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
471
- ['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
472
- ['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
473
- ]
474
- for (const [needle, errMsg] of checks) {
475
- if (content.includes(needle)) {
476
- passFn(`lint-js.yml містить: ${needle}`)
477
- } else {
478
- failFn(errMsg)
479
- }
480
- }
481
- if (content.includes('bunx oxlint') && OXLINT_FIX_RE.test(content)) {
482
- failFn('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
483
- }
484
- if (content.includes('eslint --fix')) {
485
- failFn('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
486
- }
487
- }
488
-
489
- /**
490
- * Перевіряє вміст lint-js.yml через YAML або fallback.
491
- * @param {string} content вміст файлу
492
- * @param {(msg: string) => void} passFn callback при успішній перевірці
493
- * @param {(msg: string) => void} failFn callback при помилці
494
- */
495
- function checkLintJsYmlContent(content, passFn, failFn) {
496
- const root = parseWorkflowYaml(content)
497
- if (root) {
498
- const v = verifyLintJsWorkflowStructure(root)
499
- if (v.ok) {
500
- passFn('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
501
- } else {
502
- for (const msg of v.failures) {
503
- failFn(`lint-js.yml: ${msg}`)
504
- }
505
- }
506
- } else {
507
- checkLintJsWorkflowFallback(content, passFn, failFn)
508
- }
509
- }
510
-
511
- /**
512
- * Перевіряє lint-js.yml і lint.yml workflow.
410
+ * FS-existence для `lint-js.yml` + cross-file перевірка, що `lint.yml` (якщо існує)
411
+ * не дублює лінт JS-кроки. Структуру `lint-js.yml` (`actions/checkout@v6`,
412
+ * `persist-credentials: false`, `setup-bun-deps`, `bunx oxlint/eslint/jscpd .`,
413
+ * заборона `--fix` у CI) валідує `npm/policy/js_lint/lint_js_yml/`.
513
414
  * @param {(msg: string) => void} passFn callback при успішній перевірці
514
415
  * @param {(msg: string) => void} failFn callback при помилці
515
416
  */
516
417
  async function checkLintJsWorkflows(passFn, failFn) {
517
418
  if (existsSync('.github/workflows/lint-js.yml')) {
518
- const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
519
- passFn('lint-js.yml існує')
520
- checkLintJsYmlContent(content, passFn, failFn)
419
+ passFn('.github/workflows/lint-js.yml є (структуру перевіряє bun run lint-conftest → js_lint.lint_js_yml)')
521
420
  } else {
522
421
  failFn('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
523
422
  }
@@ -22,20 +22,11 @@ import { join } from 'node:path'
22
22
  import { promisify } from 'node:util'
23
23
 
24
24
  import { createCheckReporter } from './utils/check-reporter.mjs'
25
- import {
26
- hasIdTokenWritePermission,
27
- hasNpmPublishStepWithPackage,
28
- parseWorkflowYaml,
29
- pushHasMainBranch,
30
- pushPathsIncludeNpmGlob
31
- } from './utils/gha-workflow.mjs'
32
25
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
33
26
  import { walkDir } from './utils/walkDir.mjs'
34
27
 
35
28
  const execFileAsync = promisify(execFile)
36
29
 
37
- const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
38
-
39
30
  /** Перший заголовок релізу у Keep a Changelog (`## [1.2.3]`). */
40
31
  const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
41
32
 
@@ -115,39 +106,6 @@ function missingHkEmitTypesConfigFragments(hkText) {
115
106
  return need.filter(s => !hkText.includes(s))
116
107
  }
117
108
 
118
- /**
119
- * Перевіряє `npm/tsconfig.emit-types.json` на мінімальний набір опцій для `emitDeclarationOnly` у `types/`.
120
- * @param {unknown} parsed результат `JSON.parse` конфігурації
121
- * @returns {string[]} повідомлення про помилки (порожній — OK)
122
- */
123
- function emitTypesConfigIssues(parsed) {
124
- const issues = []
125
- if (!parsed || typeof parsed !== 'object') {
126
- return ['некоректний JSON']
127
- }
128
- const co = /** @type {{ [k: string]: unknown }} */ (parsed).compilerOptions
129
- if (!co || typeof co !== 'object') {
130
- return ['відсутній compilerOptions']
131
- }
132
- const get = k => /** @type {{ [k: string]: unknown }} */ (co)[k]
133
- if (get('allowJs') !== true) {
134
- issues.push('compilerOptions.allowJs має бути true')
135
- }
136
- if (get('declaration') !== true) {
137
- issues.push('compilerOptions.declaration має бути true')
138
- }
139
- if (get('emitDeclarationOnly') !== true) {
140
- issues.push('compilerOptions.emitDeclarationOnly має бути true')
141
- }
142
- if (get('outDir') !== 'types') {
143
- issues.push('compilerOptions.outDir має бути "types"')
144
- }
145
- if (get('skipLibCheck') !== true) {
146
- issues.push('compilerOptions.skipLibCheck має бути true')
147
- }
148
- return issues
149
- }
150
-
151
109
  /**
152
110
  * Шлях на дискі до файлу з поля `types` у `npm/package.json` (значення на кшталт `./types/bin/x.d.ts`).
153
111
  * @param {string} typesField значення поля `types` з `package.json`
@@ -162,30 +120,9 @@ function npmTypesFileFromPackageField(typesField) {
162
120
  }
163
121
 
164
122
  /**
165
- * Перевіряє поле types у npm/package.json.
166
- * @param {unknown} typesField значення поля types
167
- * @param {boolean} useSrcJsLayout чи використовується layout з npm/src
168
- * @param {(msg: string) => void} passFn callback при успішній перевірці
169
- * @param {(msg: string) => void} failFn callback при помилці
170
- */
171
- function checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn) {
172
- if (useSrcJsLayout) {
173
- if (typesField === TYPES_INDEX) {
174
- passFn(`npm/package.json: "types": "${TYPES_INDEX}" (layout npm/src + .js)`)
175
- } else {
176
- failFn(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
177
- }
178
- } else if (typeof typesField === 'string' && TYPES_FILE_RE.test(typesField)) {
179
- passFn(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
180
- } else {
181
- failFn(
182
- 'npm/package.json: без .js під npm/src поле types має бути рядком виду ./types/….d.ts або .d.mts (див. npm-module.mdc)'
183
- )
184
- }
185
- }
186
-
187
- /**
188
- * Перевіряє npm/package.json на типи та files.
123
+ * Перевіряє наявність на диску файлу зі значення `types` у `npm/package.json`
124
+ * (cross-file: JSON-поле + FS). Структуру самого поля валідує
125
+ * `npm/policy/npm_module/npm_package_json/`; тут лише чи файл реально існує.
189
126
  * @param {boolean} useSrcJsLayout чи використовується layout з npm/src
190
127
  * @param {(msg: string) => void} passFn callback при успішній перевірці
191
128
  * @param {(msg: string) => void} failFn callback при помилці
@@ -195,14 +132,6 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
195
132
  const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
196
133
  const typesField = npmPkg.types
197
134
 
198
- checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn)
199
-
200
- if (Array.isArray(npmPkg.files) && npmPkg.files.includes('types')) {
201
- passFn('npm/package.json: files містить "types"')
202
- } else {
203
- failFn('npm/package.json: масив files має містити "types"')
204
- }
205
-
206
135
  const typesPath = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
207
136
  const missingTypesMsg = useSrcJsLayout
208
137
  ? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
@@ -215,31 +144,19 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
215
144
  }
216
145
 
217
146
  /**
218
- * Перевіряє npm/tsconfig.emit-types.json.
147
+ * FS-existence для `npm/tsconfig.emit-types.json` (структуру `compilerOptions`
148
+ * валідує `npm/policy/npm_module/emit_types_config/`).
219
149
  * @param {(msg: string) => void} passFn callback при успішній перевірці
220
150
  * @param {(msg: string) => void} failFn callback при помилці
221
151
  */
222
- async function checkEmitTypesConfig(passFn, failFn) {
152
+ function checkEmitTypesConfig(passFn, failFn) {
223
153
  if (!existsSync(EMIT_TYPES_CONFIG)) {
224
154
  failFn(
225
155
  `Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
226
156
  )
227
157
  return
228
158
  }
229
- passFn(`${EMIT_TYPES_CONFIG} існує`)
230
- let raw
231
- try {
232
- raw = JSON.parse(await readFile(EMIT_TYPES_CONFIG, 'utf8'))
233
- } catch {
234
- failFn(`${EMIT_TYPES_CONFIG}: некоректний JSON`)
235
- return
236
- }
237
- const issues = emitTypesConfigIssues(raw)
238
- if (issues.length === 0) {
239
- passFn(`${EMIT_TYPES_CONFIG}: compilerOptions придатні для emitDeclarationOnly → types/`)
240
- } else {
241
- failFn(`${EMIT_TYPES_CONFIG}: ${issues.join('; ')}`)
242
- }
159
+ passFn(`${EMIT_TYPES_CONFIG} є (структуру перевіряє bun run lint-conftest → npm_module.emit_types_config)`)
243
160
  }
244
161
 
245
162
  /**
@@ -364,71 +281,25 @@ async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
364
281
  }
365
282
 
366
283
  /**
367
- * Перевіряє npm-publish.yml workflow на наявність потрібних полів і кроків.
284
+ * FS-existence для `npm-publish.yml` workflow. Поля workflow (`on.push.paths`,
285
+ * `branches`, `id-token: write`, JS-DevTools/npm-publish step) валідує
286
+ * `npm/policy/npm_module/npm_publish_yml/`.
368
287
  * @param {(msg: string) => void} passFn callback при успішній перевірці
369
288
  * @param {(msg: string) => void} failFn callback при виявленому порушенні
370
- * @returns {Promise<void>}
371
289
  */
372
- async function checkPublishWorkflow(passFn, failFn) {
290
+ function checkPublishWorkflow(passFn, failFn) {
373
291
  const publishWf = '.github/workflows/npm-publish.yml'
374
- if (!existsSync(publishWf)) {
292
+ if (existsSync(publishWf)) {
293
+ passFn(`${publishWf} є (структуру перевіряє bun run lint-conftest → npm_module.npm_publish_yml)`)
294
+ } else {
375
295
  failFn(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
376
- return
377
- }
378
- passFn(`${publishWf} існує`)
379
- const pub = await readFile(publishWf, 'utf8')
380
- const root = parseWorkflowYaml(pub)
381
- if (root) {
382
- const checks = [
383
- {
384
- ok: pushPathsIncludeNpmGlob(root),
385
- pass: `${publishWf}: on.push.paths містить npm/**`,
386
- fail: `${publishWf}: у on.push.paths має бути npm/**`
387
- },
388
- {
389
- ok: pushHasMainBranch(root),
390
- pass: `${publishWf}: очікується branch main`,
391
- fail: `${publishWf}: очікується branch main`
392
- },
393
- {
394
- ok: hasIdTokenWritePermission(root),
395
- pass: `${publishWf}: permissions містить id-token: write (OIDC)`,
396
- fail: `${publishWf}: permissions має містити id-token: write (OIDC)`
397
- },
398
- {
399
- ok: hasNpmPublishStepWithPackage(root),
400
- pass: `${publishWf}: uses JS-DevTools/npm-publish та with.package npm/package.json`,
401
- fail: `${publishWf}: очікується uses: JS-DevTools/npm-publish та with.package: npm/package.json`
402
- }
403
- ]
404
- for (const c of checks) {
405
- if (c.ok) {
406
- passFn(c.pass)
407
- } else {
408
- failFn(c.fail)
409
- }
410
- }
411
- return
412
- }
413
- const need = [
414
- { sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
415
- { sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
416
- { sub: 'main', msg: `${publishWf}: очікується branch main` },
417
- { sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
418
- { sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
419
- { sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
420
- ]
421
- for (const { sub, msg } of need) {
422
- if (pub.includes(sub)) {
423
- passFn(`${publishWf} містить «${sub}»`)
424
- } else {
425
- failFn(msg)
426
- }
427
296
  }
428
297
  }
429
298
 
430
299
  /**
431
- * Перевіряє базову структуру монорепо (package.json, npm/, workspaces, npm/package.json).
300
+ * Перевіряє базову структуру монорепо: наявність каталогу `npm/` і
301
+ * `npm/package.json`. Поле `workspaces ∋ "npm"` у кореневому `package.json`
302
+ * валідує `npm/policy/npm_module/root_package_json/`.
432
303
  * @param {(msg: string) => void} pass callback при успішній перевірці
433
304
  * @param {(msg: string) => void} fail callback при помилці
434
305
  */
@@ -450,15 +321,6 @@ async function checkNpmModuleBasicStructure(pass, fail) {
450
321
  fail('npm/ директорія не існує')
451
322
  }
452
323
 
453
- if (existsSync('package.json')) {
454
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
455
- if (Array.isArray(pkg.workspaces) && pkg.workspaces.includes('npm')) {
456
- pass('package.json workspaces містить "npm"')
457
- } else {
458
- fail('package.json workspaces має містити "npm"')
459
- }
460
- }
461
-
462
324
  if (existsSync('npm/package.json')) {
463
325
  pass('npm/package.json існує')
464
326
  } else {