@nitra/cursor 5.3.4 → 5.4.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.
Files changed (151) hide show
  1. package/.claude-template/settings.template.json +2 -2
  2. package/.pi-template/extensions/n-cursor-adr/docs/index.md +13 -24
  3. package/CHANGELOG.md +11 -0
  4. package/bin/n-cursor.js +43 -22
  5. package/lib/docs/models.md +29 -18
  6. package/lib/docs/omlx-trace.md +51 -0
  7. package/lib/docs/omlx.md +31 -15
  8. package/lib/omlx.mjs +2 -5
  9. package/package.json +1 -1
  10. package/rules/abie/docs/fix.md +17 -11
  11. package/rules/adr/docs/fix.md +25 -140
  12. package/rules/bun/docs/fix.md +18 -151
  13. package/rules/capacitor/docs/fix.md +16 -13
  14. package/rules/capacitor/js/docs/platforms.md +31 -43
  15. package/rules/changelog/docs/fix.md +25 -169
  16. package/rules/ci4/docs/fix.md +11 -14
  17. package/rules/doc-files/doc-files.mdc +60 -0
  18. package/rules/doc-files/docs/fix.md +31 -0
  19. package/rules/doc-files/fix.mjs +19 -0
  20. package/{skills → rules}/doc-files/js/docgen-extract.mjs +42 -19
  21. package/{skills → rules}/doc-files/js/docgen-ignore.mjs +2 -1
  22. package/{skills → rules}/doc-files/js/docgen-scan.mjs +9 -1
  23. package/{skills → rules}/doc-files/js/docs/docgen-crc.md +1 -1
  24. package/{skills → rules}/doc-files/js/docs/docgen-extract-anchors.md +1 -1
  25. package/{skills → rules}/doc-files/js/docs/docgen-extract.md +2 -2
  26. package/{skills → rules}/doc-files/js/docs/docgen-files-batch.md +1 -1
  27. package/{skills → rules}/doc-files/js/docs/docgen-gen.md +1 -1
  28. package/{skills → rules}/doc-files/js/docs/docgen-ignore.md +4 -4
  29. package/rules/doc-files/js/docs/docgen-prompts.md +39 -0
  30. package/rules/doc-files/js/docs/docgen-scan.md +54 -0
  31. package/rules/doc-files/js/docs/lint.md +36 -0
  32. package/rules/doc-files/js/docs/units-js.md +31 -0
  33. package/rules/doc-files/js/docs/units-rs.md +35 -0
  34. package/rules/doc-files/js/docs/units.md +30 -0
  35. package/rules/doc-files/js/lint.mjs +96 -0
  36. package/{skills → rules}/doc-files/js/units-rs.mjs +37 -17
  37. package/rules/doc-files/lint/docs/lint.md +37 -0
  38. package/rules/doc-files/lint/lint.mjs +105 -0
  39. package/rules/doc-files/meta.json +1 -0
  40. package/rules/docker/docs/fix.md +21 -161
  41. package/rules/efes/docs/fix.md +23 -194
  42. package/rules/feedback/docs/fix.md +10 -8
  43. package/rules/ga/docs/fix.md +10 -5
  44. package/rules/graphql/docs/fix.md +23 -119
  45. package/rules/hasura/docs/fix.md +19 -5
  46. package/rules/hasura/js/docs/internal_urls.md +34 -307
  47. package/rules/image-avif/docs/fix.md +16 -127
  48. package/rules/image-compress/docs/fix.md +20 -141
  49. package/rules/image-compress/js/docs/package_setup.md +22 -182
  50. package/rules/js-bun-db/docs/fix.md +23 -139
  51. package/rules/js-bun-db/js/docs/safety.md +33 -221
  52. package/rules/js-bun-redis/docs/fix.md +25 -114
  53. package/rules/js-bun-redis/js/docs/imports.md +18 -166
  54. package/rules/js-lint/docs/fix.md +30 -108
  55. package/rules/js-lint/js/docs/lint-findings.md +37 -17
  56. package/rules/js-lint/js/docs/lint.md +22 -238
  57. package/rules/js-lint/js/docs/tooling.md +34 -331
  58. package/rules/js-lint-ci/docs/fix.md +16 -149
  59. package/rules/js-lint-ci/js/docs/lint.md +16 -136
  60. package/rules/js-mssql/docs/fix.md +18 -123
  61. package/rules/js-mssql/js/docs/deps.md +28 -251
  62. package/rules/js-run/docs/fix.md +23 -138
  63. package/rules/js-run/js/docs/runtime.md +24 -378
  64. package/rules/k8s/docs/fix.md +18 -123
  65. package/rules/nginx-default-tpl/docs/fix.md +22 -118
  66. package/rules/nginx-default-tpl/js/docs/template.md +38 -360
  67. package/rules/npm-module/docs/fix.md +27 -89
  68. package/rules/npm-module/js/docs/header_doc_pointer.md +15 -15
  69. package/rules/npm-module/js/docs/package_structure.md +36 -258
  70. package/rules/npm-module/js/docs/rule_meta.md +25 -127
  71. package/rules/npm-module/js/docs/skill_meta.md +18 -180
  72. package/rules/php/docs/fix.md +21 -98
  73. package/rules/php/js/docs/tooling.md +20 -143
  74. package/rules/python/docs/fix.md +25 -157
  75. package/rules/python/js/docs/applies.md +20 -98
  76. package/rules/python/js/docs/tooling.md +27 -144
  77. package/rules/rego/docs/fix.md +24 -112
  78. package/rules/rego/js/docs/applies.md +20 -164
  79. package/rules/rego/js/docs/lint.md +15 -110
  80. package/rules/release/docs/fix.md +16 -114
  81. package/rules/rust/docs/fix.md +24 -119
  82. package/rules/rust/js/docs/applies.md +20 -129
  83. package/rules/security/docs/fix.md +21 -78
  84. package/rules/security/js/docs/sample_secret.md +23 -182
  85. package/rules/security/js/docs/trufflehog.md +19 -128
  86. package/rules/style-lint/docs/fix.md +16 -150
  87. package/rules/style-lint/js/docs/lint.md +21 -172
  88. package/rules/style-lint/js/docs/tooling.md +19 -184
  89. package/rules/tauri/docs/fix.md +26 -152
  90. package/rules/tauri/js/docs/cargo_mutants_config.md +21 -159
  91. package/rules/tauri/js/docs/tooling.md +20 -217
  92. package/rules/test/docs/fix.md +19 -127
  93. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +15 -127
  94. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +17 -153
  95. package/rules/test/js/docs/cargo_mutants_config.md +24 -164
  96. package/rules/test/js/docs/location.md +24 -126
  97. package/rules/test/js/docs/no-process-chdir.md +20 -151
  98. package/rules/test/js/docs/no-relative-fs-path.md +24 -261
  99. package/rules/test/js/docs/stryker_config.md +48 -148
  100. package/rules/test/js/docs/vitest-config-pool-forks.md +21 -164
  101. package/rules/text/docs/fix.md +25 -113
  102. package/rules/text/js/docs/forbidden-prettier.md +21 -132
  103. package/rules/text/js/docs/formatting.md +60 -251
  104. package/rules/text/js/docs/lint.md +17 -114
  105. package/rules/vue/docs/fix.md +25 -118
  106. package/rules/vue/js/docs/packages.md +25 -323
  107. package/rules/worktree/docs/fix.md +31 -150
  108. package/scripts/coverage-classify/docs/index.md +23 -209
  109. package/scripts/coverage-classify/docs/verdict-schema.md +14 -159
  110. package/scripts/dispatcher/docs/trace.md +35 -0
  111. package/scripts/docs/auto-rules.md +37 -361
  112. package/scripts/docs/lint-cli.md +12 -13
  113. package/scripts/docs/post-tool-use-fix.md +16 -15
  114. package/scripts/docs/skills-cli.md +26 -23
  115. package/scripts/docs/sync-claude-config.md +94 -34
  116. package/scripts/docs/worktree-cli.md +11 -34
  117. package/scripts/lib/docs/assert-project-root.md +14 -16
  118. package/scripts/lib/docs/changed-files.md +24 -139
  119. package/scripts/lib/docs/discover-check-rules-from-cursor.md +14 -146
  120. package/scripts/lib/docs/rule-predicates.md +20 -17
  121. package/scripts/lib/docs/run-rule-cli.md +14 -18
  122. package/scripts/lib/docs/run-rule.md +13 -20
  123. package/scripts/lib/docs/run-standard-rule.md +12 -15
  124. package/scripts/lib/docs/sync-gitignore-worktree.md +15 -18
  125. package/scripts/lib/rule-predicates.mjs +1 -1
  126. package/scripts/sync-claude-config.mjs +4 -1
  127. package/scripts/utils/docs/with-lock.md +19 -12
  128. package/scripts/utils/with-lock.mjs +4 -2
  129. package/skills/doc-aggregate/SKILL.md +2 -2
  130. package/skills/doc-aggregate/js/docgen-ignore.mjs +6 -6
  131. package/skills/doc-aggregate/js/docs/docgen-ignore.md +1 -1
  132. package/skills/doc-aggregate/js/docs/docgen-scan.md +78 -0
  133. package/skills/doc-files/.changes/260612-0031.md +5 -0
  134. package/skills/doc-files/.changes/260612-0036.md +5 -0
  135. package/skills/doc-files/.changes/260612-0114.md +5 -0
  136. package/skills/doc-files/SKILL.md +6 -6
  137. package/skills/fix/js/docs/llm-worker.md +17 -15
  138. package/skills/fix/js/docs/orchestrator.md +30 -23
  139. package/skills/fix/js/docs/t0.md +26 -16
  140. package/skills/start-check/js/docs/check.md +26 -22
  141. package/skills/taze/js/docs/diff.md +44 -20
  142. package/skills/doc-files/js/docs/docgen-prompts.md +0 -32
  143. package/skills/doc-files/js/docs/docgen-scan.md +0 -25
  144. package/skills/doc-files/js/docs/units-rs.md +0 -35
  145. /package/{skills → rules}/doc-files/js/docgen-crc.mjs +0 -0
  146. /package/{skills → rules}/doc-files/js/docgen-extract-anchors.mjs +0 -0
  147. /package/{skills → rules}/doc-files/js/docgen-files-batch.mjs +0 -0
  148. /package/{skills → rules}/doc-files/js/docgen-gen.mjs +0 -0
  149. /package/{skills → rules}/doc-files/js/docgen-prompts.mjs +0 -0
  150. /package/{skills → rules}/doc-files/js/units-js.mjs +0 -0
  151. /package/{skills → rules}/doc-files/js/units.mjs +0 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/js/docgen-scan.mjs
4
+ crc: 46f11827
5
+ score: 100
6
+ ---
7
+
8
+ # docgen-scan.mjs
9
+
10
+ ## Огляд
11
+
12
+ isSourceFile перевіряє, чи є файл кодовим джерелом.
13
+ docPathForSource обчислює шлях md-документа для кодового файлу.
14
+ isDocCandidate перевіряє, чи підлягає файл документуванню.
15
+ describeFile описує кодовий файл, включаючи шлях доки та стан застарілості.
16
+ scanForDocFiles рекурсивно обходить дерево, повертаючи кандидати з інформацією про старілість.
17
+ resolveRoot парсить аргументи, щоб визначити абсолютний корінь.
18
+ runDocFilesScanCli сканує дерево і друкує JSON-масив усіх кодових файлів зі станом застарілості.
19
+ runDocFilesCheckCli детектує застарілість для хук'ів, гейтів або інших режимів, повертаючи код виходу.
20
+
21
+ ## Поведінка
22
+
23
+ isSourceFile Обчислює, чи є файл кодовим джерелом.
24
+ docPathForSource Обчислює шлях md-документа для кодового файлу.
25
+ isDocCandidate Перевіряє, чи підлягає файл документуванню.
26
+ describeFile Описує кодовий файл, включаючи шлях доки та стан застарілості.
27
+ scanForDocFiles Рекурсивно обходить дерево, повертаючи кандидати з інформацією про старілість.
28
+ resolveRoot Парсить аргументи, щоб визначити абсолютний корінь.
29
+ runDocFilesScanCli Сканує дерево і друкує JSON-масив усіх кодових файлів зі станом застарілості.
30
+ runDocFilesCheckCli Детектує застарілість для хук'ів, гейтів або інших режимів, повертаючи код виходу.
31
+
32
+ ## Публічний API
33
+
34
+ isSourceFile — перевіряє, чи є файл коду для документування.
35
+ docPathForSource — обчислює шлях до документа для кодового файлу, розміщуючи його в теці `docs/` поруч із джерелом. Якщо шлях відносний, документ також відносний; якщо абсолютний, документ залишається абсолютним.
36
+ isDocCandidate — визначає, чи підлягає кодовий файл документуванню: має правильне розширення, не є тестом, не знаходиться в ігнорованому списку, і не є кореневим системним документуванням.
37
+ describeFile — надає опис кодового файлу: шлях до джерела, шлях до документа та стан застарілості за CRC.
38
+ scanForDocFiles — рекурсивно переглядає дерево від заданого кореня і повертає кодові файли разом зі станом застарілості.
39
+ resolveRoot — парсить аргумент `--root <dir>` з командного рядка; за замовчуванням використовує поточну робочу директорію.
40
+ runDocFilesScanCli — сканує дерево і виводить JSON-масив усіх кодових файлів із зазначенням їхнього стану застарілості.
41
+ runDocFilesCheckCli — виконує перевірку застарілості для хуків та командного рядка через інструмент `doc-files check`.
42
+ Режими — це способи виконання:
43
+ --hook — бере шлях до файлу з вводу JSON і перевіряє один файл.
44
+ --git — перевіряє різницю в Git (`git diff --name-only HEAD`) з урахуванням порогу `--max` (за замовчуванням 50); якщо застарілості більше, не блокує (виходить з кодом 0 з попередженням).
45
+ --degraded — генерує інформаційний звіт про документи, які мають оцінку нижче встановленого порогу (виходить з кодом 0).
46
+ <paths…> — використовується для визначення явних шляхів до джерел.
47
+ Exit 2 — повертається, якщо знайдено застарілі дані; повертається 0, якщо дані свіжі або пройдено перевищення порогу.
48
+
49
+ ## Гарантії поведінки
50
+
51
+ - Read-only: файл не виконує операцій запису у файлову систему.
52
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
53
+ - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
54
+ - Не звертається до мережі.
@@ -0,0 +1,36 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/js/lint.mjs
4
+ crc: f25a3bbe
5
+ score: 100
6
+ ---
7
+
8
+ # lint.mjs
9
+
10
+ ## Огляд
11
+
12
+ Адаптер правила doc-files до агрегатора `n-cursor lint`. Дає агрегатору відповідь на одне
13
+ питання: чи має кожен дотичний кодовий файл актуальну файлову документацію поряд із собою.
14
+
15
+ ## Поведінка
16
+
17
+ У quick-фазі отримує список змінених файлів і зводить його до набору джерел для перевірки в
18
+ **обидва** боки: змінене джерело перевіряється проти своєї доки, а змінена або видалена дока
19
+ (`<dir>/docs/<stem>.md`) повертається до відповідного джерела з тим самим іменем у каталозі над
20
+ `docs/`. У ci-фазі (списку немає) обходить усе дерево.
21
+
22
+ Порушенням вважається відсутня дока (`missing`) або розбіжність контрольної суми джерела з тією,
23
+ що записана у frontmatter доки (`crc-mismatch`). Документ із низькою якістю, але свіжою сумою —
24
+ не порушення.
25
+
26
+ ## Публічний API
27
+
28
+ `lint` — перевіряє документацію для переданого набору змінених файлів (або всього репозиторію,
29
+ якщо набір не задано); повертає код виходу `1`, якщо є застарілі чи відсутні доки, інакше `0`.
30
+ Список проблемних файлів друкується у stderr із підказкою регенерувати.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Детермінованість: жодного виклику мовної моделі, рішення лише за контрольною сумою.
35
+ - Реверс-мапінг доки до джерела перебирає лише кодові розширення і повертає наявний файл-кандидат.
36
+ - Порожній набір змінених файлів дає код `0` без обходу дерева.
@@ -0,0 +1,31 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/js/units-js.mjs
4
+ crc: 58b898cc
5
+ score: 100
6
+ ---
7
+
8
+ # units-js.mjs
9
+
10
+ ## Огляд
11
+
12
+ Файл парсить програму, ітеруючи по її елементах для збору юнітів. Для кожної декларації визначається опис. Для функцій та класів збираються виклики інших юнітів через ребра викликів.
13
+
14
+ ## Поведінка
15
+
16
+ 1. Парсинг програми.
17
+ 2. Ітерація по тілу програми.
18
+ 3. Збір юнітів.
19
+ 4. Для кожної декларації визначається опис.
20
+ 5. Для функцій та класів збираються виклики інших юнітів.
21
+ 6. Вибираються виклики інших юнітів з ребрами викликів.
22
+
23
+ ## Публічний API
24
+
25
+ extractUnitsJs — Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc, прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
26
+
27
+ ## Гарантії поведінки
28
+
29
+ - Read-only: файл не виконує операцій запису у файлову систему.
30
+ - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
31
+ - Не звертається до мережі.
@@ -0,0 +1,35 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/js/units-rs.mjs
4
+ crc: 114e9a0b
5
+ score: 100
6
+ ---
7
+
8
+ # units-rs.mjs
9
+
10
+ ## Огляд
11
+
12
+ Файл аналізує структуру коду для вилучення інформації про юніти. Процес включає сканування рядків, ітерацію для визначення глибини блоків та позиції закриваючих лапок. Обробляються рядкові літерали для визначення межі блоків та підраховуються глибини блоків для визначення видимості. Обробляються декларації для визначення публічності. Витягується тіло функцій, структур, перерахувань або трейтів. Визначається експонування функцій через атрибути. Збирається документація перед декларацією. Формується об'єкт юніту з назвою, типом, публічністю, ім'я impl та діапазон. Визначається область видимості тіла за допомогою пошуку закриваючої фігу. Збираються виклики інших юнітів у тілі функції.
13
+
14
+ ## Поведінка
15
+
16
+ 1. Скан файлу по рядках.
17
+ 2. Ітерація по рядках для визначення глибини блоків.
18
+ 3. Обробка рядкових літералів для визначення позиції закриваючого лапки.
19
+ 4. Підрахунок глибини блоків для визначення видимості.
20
+ 5. Обробка декларацій для визначення публічності.
21
+ 6. Витягнення тіла функції, структури, перерахування або трейта.
22
+ 7. Визначення експонування функції через атрибути.
23
+ 8. Збір документації перед декларацією.
24
+ 9. Формування об'єкта юніту з назвою, типом, публічністю, ім'ям impl та діапазоном.
25
+ 10. Визначення області видимості тіла за допомогою пошуку закриваючої фігу.
26
+ 11. Збір викликів інших юнітів у тілі функції.
27
+
28
+ ## Публічний API
29
+
30
+ extractUnitsRs — Визначає top-level і impl-методи через підрахунок дужок по рядках. Обмеження: рядкові літерали з `{`/`}` всередині `{}` можуть дати хибну глибину.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Read-only: файл не виконує операцій запису у файлову систему.
35
+ - Не звертається до мережі.
@@ -0,0 +1,30 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/js/units.mjs
4
+ crc: af91bd0d
5
+ score: 100
6
+ ---
7
+
8
+ # units.mjs
9
+
10
+ ## Огляд
11
+
12
+ Файл витягує одиниці з вмісту файлу залежно від розширення шляху. Якщо розширення належить до набору JS_EXT, використовується extractUnitsJs. Якщо розширення дорівнює 'rs', використовується extractUnitsRs. В інших випадках функція повертає null.
13
+
14
+ ## Поведінка
15
+
16
+ 1. extractUnits приймає вміст файлу та шлях файлу.
17
+ 2. Перевіряє розширення шляху.
18
+ 3. Якщо розширення знаходиться у наборі JS_EXT, викликає extractUnitsJs.
19
+ 4. Якщо розширення дорівнює 'rs', викликає extractUnitsRs.
20
+ 5. У інших випадках повертає null.
21
+
22
+ ## Публічний API
23
+
24
+ extractUnits — Визначає тип парсингу залежно від розширення файлу: js/mjs/ts використовує oxc AST, rs використовує regex+brace-counting, vue/py повертає null (повний шлях).
25
+
26
+ ## Гарантії поведінки
27
+
28
+ - Read-only: файл не виконує операцій запису у файлову систему.
29
+ - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
30
+ - Не звертається до мережі.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Адаптер агрегатора `n-cursor lint` для правила doc-files.
3
+ *
4
+ * Quick-фаза отримує список змінених файлів і мапить їх у пари в **обидва** боки:
5
+ * - змінене **джерело** (`.js/.mjs/.ts/.vue/.py/.rs`) → перевірка його доки `<dir>/docs/<stem>.md`;
6
+ * - змінена/видалена **дока** (`<dir>/docs/<stem>.md`) → перевірка відповідного джерела
7
+ * (той самий stem у каталозі над текою `docs`).
8
+ * Ci-фаза (files === undefined) проганяє повний скан дерева.
9
+ *
10
+ * Порушення — `missing` ∪ `crc-mismatch` (детермінований CRC-детект, 0 LLM-токенів);
11
+ * degraded не блокує. Exit 1 — є stale; 0 — все свіже (конвенція агрегатора).
12
+ */
13
+ import { join, dirname, basename, extname } from 'node:path'
14
+ import { existsSync, readdirSync } from 'node:fs'
15
+
16
+ import { describeFile, isDocCandidate, isSourceFile, scanForDocFiles } from './docgen-scan.mjs'
17
+
18
+ /** Дока живе у `<dir>/docs/<stem>.md`; повертає `<dir>/<stem>` для реверс-мапінгу. */
19
+ const DOC_MD_RE = /(?:^|\/)docs\/[^/]+\.md$/u
20
+
21
+ /**
22
+ * Реверс-мапінг доки → джерело: для `<dir>/docs/<stem>.md` шукає у `<dir>` файл
23
+ * `<stem>.<ext>` із кодовим розширенням, що існує і є кандидатом на доку.
24
+ * @param {string} cwd корінь репо
25
+ * @param {string} docRel posix-шлях доки від кореня
26
+ * @returns {string|null} posix-шлях джерела або null
27
+ */
28
+ function sourceForDoc(cwd, docRel) {
29
+ const docsDir = dirname(docRel) // `<dir>/docs`
30
+ const srcDir = dirname(docsDir) // `<dir>`
31
+ const stem = basename(docRel, '.md')
32
+ let entries
33
+ try {
34
+ entries = readdirSync(join(cwd, srcDir), { withFileTypes: true })
35
+ } catch {
36
+ return null
37
+ }
38
+ for (const e of entries) {
39
+ if (!e.isFile() || !isSourceFile(e.name)) continue
40
+ if (basename(e.name, extname(e.name)) !== stem) continue
41
+ const rel = srcDir === '.' ? e.name : `${srcDir}/${e.name}`
42
+ if (isDocCandidate(cwd, rel)) return rel
43
+ }
44
+ return null
45
+ }
46
+
47
+ /**
48
+ * Зводить список змінених файлів у множину джерел-кандидатів для перевірки доки.
49
+ * @param {string[]} files змінені шляхи (posix або нативні)
50
+ * @param {string} cwd корінь репо
51
+ * @returns {string[]} унікальні posix-шляхи джерел
52
+ */
53
+ function sourcesFromChanged(files, cwd) {
54
+ const out = new Set()
55
+ for (const raw of files) {
56
+ const rel = raw.split('\\').join('/')
57
+ if (DOC_MD_RE.test(rel)) {
58
+ const src = sourceForDoc(cwd, rel)
59
+ if (src) out.add(src)
60
+ } else if (isDocCandidate(cwd, rel) && existsSync(join(cwd, rel))) {
61
+ out.add(rel)
62
+ }
63
+ }
64
+ return [...out]
65
+ }
66
+
67
+ /**
68
+ * Друкує список stale і повертає exit-код.
69
+ * @param {Array<{sourcePath:string, reason:string|null}>} stale застарілі описи
70
+ * @returns {number} 1 — є stale; 0 — немає
71
+ */
72
+ function reportStale(stale) {
73
+ if (stale.length === 0) return 0
74
+ const list = stale.map(f => ` - ${f.sourcePath} (${f.reason})`).join('\n')
75
+ process.stderr.write(
76
+ `✗ doc-files: документація застаріла/відсутня для ${stale.length} файл(ів):\n${list}\n→ перегенеруй: npx @nitra/cursor fix-doc-files\n`
77
+ )
78
+ return 1
79
+ }
80
+
81
+ /**
82
+ * Крок агрегатора lint для doc-files.
83
+ * @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
84
+ * @param {string} [cwd] корінь репо
85
+ * @returns {Promise<number>} 0 — OK, 1 — є застарілі доки
86
+ */
87
+ export function lint(files, cwd = process.cwd()) {
88
+ if (files === undefined) {
89
+ const stale = scanForDocFiles(cwd).filter(f => f.stale)
90
+ return Promise.resolve(reportStale(stale))
91
+ }
92
+ const sources = sourcesFromChanged(files, cwd)
93
+ if (sources.length === 0) return Promise.resolve(0)
94
+ const stale = sources.map(src => describeFile(cwd, src)).filter(f => f.stale)
95
+ return Promise.resolve(reportStale(stale))
96
+ }
@@ -9,7 +9,10 @@
9
9
  function skipString(src, i) {
10
10
  i++ // відкриваючий "
11
11
  while (i < src.length) {
12
- if (src[i] === '\\') { i += 2; continue }
12
+ if (src[i] === '\\') {
13
+ i += 2
14
+ continue
15
+ }
13
16
  if (src[i] === '"') return i + 1
14
17
  i++
15
18
  }
@@ -38,8 +41,15 @@ function findClosingBrace(src, start) {
38
41
  i = end === -1 ? src.length : end + 2
39
42
  continue
40
43
  }
41
- if (ch === '"') { i = skipString(src, i); continue }
42
- if (ch === '{') { depth++; i++; continue }
44
+ if (ch === '"') {
45
+ i = skipString(src, i)
46
+ continue
47
+ }
48
+ if (ch === '{') {
49
+ depth++
50
+ i++
51
+ continue
52
+ }
43
53
  if (ch === '}') {
44
54
  depth--
45
55
  if (depth === 0) return i
@@ -73,12 +83,17 @@ function docBefore(lines, lineIdx) {
73
83
  return doc.join(' ').trim()
74
84
  }
75
85
 
76
- // Pub-items: pub fn / pub struct / pub enum / pub trait / pub type
86
+ // Pub-items матчаться у два кроки по trim-нутому рядку (прості регекспи без
87
+ // бектрекінгу): спершу опційний pub(...)-префікс, потім сама декларація.
77
88
  // Також ловить fn без pub (для localSymbols і impl-методів)
78
- const ITEM_RE = /^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/
89
+ const PUB_PREFIX_RE = /^pub(?:\([^)]*\))?\s+/
90
+ const ITEM_DECL_RE = /^(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/
79
91
 
80
- // impl Type { або impl<T> Trait for Type {
81
- const IMPL_RE = /^[ \t]*(?:pub\s+)?impl(?:<[^>]*>)?\s+(?:(?:\w[\w:<>, ]*\s+for\s+))?(\w+)/
92
+ // impl Type { або impl<T> Trait for Type { — теж двокроково: голова `impl<...>`,
93
+ // далі тип після `for` (trait-impl) або перше слово (inherent impl)
94
+ const IMPL_HEAD_RE = /^impl(?:<[^>]*>)?\s+/
95
+ const IMPL_FOR_TYPE_RE = /\bfor\s+(\w+)/
96
+ const TYPE_NAME_RE = /^(\w+)/
82
97
 
83
98
  // Підозрілі exposure-атрибути, що роблять непуб-fn фактично публічними
84
99
  const EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/
@@ -93,7 +108,7 @@ const CALL_RE = /\b([a-z_]\w*)\s*\(/g
93
108
  * хибну глибину (рідкісно в реальному Rust-коді з rustfmt).
94
109
  * @param {string} src вміст файлу
95
110
  * @param {string} [_relPath] резервний (не використовується)
96
- * @returns {Array<{name:string, kind:string, exported:boolean, implName:string|null, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null}
111
+ * @returns {Array<{name:string, kind:string, exported:boolean, implName:string|null, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти файлу (fn та impl-методи) або `null`, якщо юнітів не знайдено
97
112
  */
98
113
  export function extractUnitsRs(src, _relPath) {
99
114
  const lines = src.split('\n')
@@ -129,7 +144,7 @@ export function extractUnitsRs(src, _relPath) {
129
144
  }
130
145
 
131
146
  // Закриті impl-блоки прибираємо зі стека
132
- while (implStack.length > 0 && implStack[implStack.length - 1].openDepth > depth) {
147
+ while (implStack.length > 0 && implStack.at(-1).openDepth > depth) {
133
148
  implStack.pop()
134
149
  }
135
150
 
@@ -140,22 +155,27 @@ export function extractUnitsRs(src, _relPath) {
140
155
  nextFnExposed = true
141
156
  }
142
157
 
158
+ const trimmed = line.trimStart()
159
+
143
160
  // Impl-декларація (зазвичай глибина 0, але може бути в mod)
144
161
  if (depthAtStart <= 1) {
145
- const implM = line.match(IMPL_RE)
146
- if (implM && line.includes('{')) {
147
- implStack.push({ typeName: implM[1], openDepth: depth })
162
+ const headM = trimmed.match(IMPL_HEAD_RE)
163
+ if (headM && line.includes('{')) {
164
+ const rest = trimmed.slice(headM[0].length)
165
+ const typeM = rest.match(IMPL_FOR_TYPE_RE) ?? rest.match(TYPE_NAME_RE)
166
+ if (typeM) implStack.push({ typeName: typeM[1], openDepth: depth })
148
167
  }
149
168
  }
150
169
 
151
170
  // Елементи на глибині 0 (top-level) і 1 (всередині impl)
152
171
  if (depthAtStart <= 1) {
153
- const m = line.match(ITEM_RE)
172
+ const pubM = trimmed.match(PUB_PREFIX_RE)
173
+ const m = (pubM ? trimmed.slice(pubM[0].length) : trimmed).match(ITEM_DECL_RE)
154
174
  if (m) {
155
- const isPub = Boolean(m[1]) || (m[2] === 'fn' && nextFnExposed)
156
- if (m[2] === 'fn') nextFnExposed = false
157
- const kind = m[2]
158
- const name = m[3]
175
+ const isPub = Boolean(pubM) || (m[1] === 'fn' && nextFnExposed)
176
+ if (m[1] === 'fn') nextFnExposed = false
177
+ const kind = m[1]
178
+ const name = m[2]
159
179
  const doc = docBefore(lines, li)
160
180
 
161
181
  // Витягуємо тіло через findClosingBrace для fn/struct/enum/trait
@@ -0,0 +1,37 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/lint/lint.mjs
4
+ crc: a30dd81f
5
+ score: 100
6
+ ---
7
+
8
+ # lint.mjs
9
+
10
+ ## Огляд
11
+
12
+ CLI-команда `lint-doc-files` — детермінований детектор застарілості файлових док. Працює без
13
+ мовної моделі скрізь: локально, в hook'ах і в CI.
14
+
15
+ ## Поведінка
16
+
17
+ Без прапорців або з переліком шляхів робить повний чи точковий детект і повертає код `1`, якщо є
18
+ застарілі/відсутні доки. `--missing-only` звужує до самих відсутніх. `--json` друкує машинний
19
+ лістинг усіх кандидатів зі станом і завершується кодом `0`. Режими `--hook`, `--git` і
20
+ `--degraded` делегуються детектору hook-протоколу (PostToolUse за одним файлом зі stdin, Stop-гейт
21
+ за зміненими у задачі джерелами з порогом, інформаційний звіт про доки нижчої якості).
22
+
23
+ Повний прогін серіалізується спільним локом під ключем, виведеним зі шляху каталогу, тож паралельні
24
+ запуски не накладаються; hook-форми навмисно виконуються без локу заради завжди-свіжого вердикту.
25
+
26
+ ## Публічний API
27
+
28
+ `runLintDocFilesCli` — точка входу команди: маршрутизує між делегатом hook-протоколу, JSON-лістингом
29
+ і повним/точковим детектом; повертає код виходу.
30
+ `runLintDocFilesSteps` — сама робота повного чи точкового детекту, придатна для прямого виклику з
31
+ тестів без локу; повертає `1` за наявності застарілих док, інакше `0`.
32
+
33
+ ## Гарантії поведінки
34
+
35
+ - Неіснуючий корінь дає зрозумілу помилку і код `1`.
36
+ - Повний прогін — код `1` (конвенція `lint-*`); hook/git — код `2` (blocking feedback для Claude Code).
37
+ - Жодних викликів мовної моделі.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * CLI-обгортка канонічного `lint-doc-files` (doc-files.mdc): детермінований детектор
3
+ * застарілості файлових док (`<dir>/docs/<stem>.md`) — 0 викликів LLM, працює будь-де.
4
+ *
5
+ * Режими (мапа команд у doc-files.mdc / спеці 2026-06-12):
6
+ * - (без прапорців) / `[paths…]` — повний або точковий детект; **exit 1**, якщо є stale.
7
+ * - `--missing-only` — звужує до `missing` (без `crc-mismatch`); exit 1.
8
+ * - `--json` — JSON-лістинг усіх кандидатів зі станом (= старий `scan`); exit 0.
9
+ * - `--hook` / `--git` / `--degraded` — делегат у `runDocFilesCheckCli` (hook-протокол: exit 2/0).
10
+ *
11
+ * Серіалізація: повний прогін — через `runStandardLint` (ключ `lint-doc-files`,
12
+ * виводиться зі шляху каталогу). Hook/git/degraded форми — **без локу** (швидкі точкові
13
+ * перевірки в hook-протоколі потребують завжди-свіжого вердикту) — канон scripts.mdc.
14
+ */
15
+ import { existsSync, statSync } from 'node:fs'
16
+ import { join, relative, resolve, sep, isAbsolute } from 'node:path'
17
+
18
+ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
19
+ import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
20
+ import {
21
+ describeFile,
22
+ isDocCandidate,
23
+ resolveRoot,
24
+ runDocFilesCheckCli,
25
+ runDocFilesScanCli,
26
+ scanForDocFiles
27
+ } from '../js/docgen-scan.mjs'
28
+
29
+ /**
30
+ * Нормалізує шлях-кандидат до posix-шляху від кореня (null поза деревом).
31
+ * @param {string} root абсолютний корінь
32
+ * @param {string} candidate шлях-кандидат
33
+ * @returns {string|null} posix-шлях від кореня або null
34
+ */
35
+ function toRelSource(root, candidate) {
36
+ const rel = relative(root, resolve(root, candidate))
37
+ if (rel.startsWith('..') || isAbsolute(rel)) return null
38
+ return rel.split(sep).join('/')
39
+ }
40
+
41
+ /**
42
+ * Витягує позиційні шляхи з argv (не прапорці й не значення `--root`).
43
+ * @param {string[]} argv аргументи
44
+ * @returns {string[]} позиційні шляхи
45
+ */
46
+ function positionalPaths(argv) {
47
+ const rootIdx = argv.indexOf('--root')
48
+ const skip = rootIdx !== -1 ? rootIdx + 1 : -1
49
+ return argv.filter((a, i) => !a.startsWith('--') && i !== skip)
50
+ }
51
+
52
+ /**
53
+ * Реальна робота повного / точкового детекту. Exit 1 — є stale, 0 — все свіже.
54
+ * @param {string[]} argv аргументи після назви команди
55
+ * @returns {number} exit-код
56
+ */
57
+ export function runLintDocFilesSteps(argv) {
58
+ const root = resolveRoot(argv)
59
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
60
+ console.error(`lint-doc-files: корінь не існує або не є директорією: ${root}`)
61
+ return 1
62
+ }
63
+ const missingOnly = argv.includes('--missing-only')
64
+ const paths = positionalPaths(argv)
65
+
66
+ const described = paths.length
67
+ ? paths
68
+ .map(p => toRelSource(root, p))
69
+ .filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
70
+ .map(rel => describeFile(root, /** @type {string} */ (rel)))
71
+ : scanForDocFiles(root)
72
+
73
+ let stale = described.filter(f => f.stale)
74
+ if (missingOnly) stale = stale.filter(f => f.reason === 'missing')
75
+
76
+ if (stale.length === 0) {
77
+ console.log('✓ doc-files: усі файлові доки актуальні.')
78
+ return 0
79
+ }
80
+ const list = stale.map(f => ` - ${f.sourcePath} (${f.reason})`).join('\n')
81
+ console.error(
82
+ `✗ doc-files: документація застаріла/відсутня для ${stale.length} файл(ів):\n${list}\n→ перегенеруй: npx @nitra/cursor fix-doc-files`
83
+ )
84
+ return 1
85
+ }
86
+
87
+ /**
88
+ * Публічна CLI-форма `lint-doc-files`. Hook/git/degraded — делегат без локу; `--json` —
89
+ * scan; решта — повний/точковий детект під `runStandardLint` (ключ `lint-doc-files`).
90
+ * @param {string[]} [argv] аргументи після назви команди
91
+ * @returns {Promise<number>} exit-код
92
+ */
93
+ export function runLintDocFilesCli(argv = process.argv.slice(3)) {
94
+ if (argv.includes('--hook') || argv.includes('--git') || argv.includes('--degraded')) {
95
+ return runDocFilesCheckCli(argv)
96
+ }
97
+ if (argv.includes('--json')) {
98
+ return Promise.resolve(runDocFilesScanCli(argv))
99
+ }
100
+ return runStandardLint(import.meta.dirname, () => runLintDocFilesSteps(argv))
101
+ }
102
+
103
+ if (isRunAsCli(import.meta.url)) {
104
+ process.exitCode = await runLintDocFilesCli(process.argv.slice(2))
105
+ }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "lint": "quick" }