@nitra/cursor 5.0.3 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-template/settings.template.json +22 -0
- package/.pi-template/extensions/n-cursor-adr/docs/index.md +15 -9
- package/CHANGELOG.md +18 -1
- package/bin/n-cursor.js +73 -16
- package/docs/stryker.config.md +6 -0
- package/docs/vitest.config.md +6 -0
- package/lib/docs/llm.md +29 -0
- package/lib/docs/omlx.md +32 -0
- package/lib/llm.mjs +137 -0
- package/lib/models.mjs +9 -1
- package/lib/omlx.mjs +147 -0
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +6 -0
- package/rules/abie/js/docs/applies.md +6 -0
- package/rules/abie/js/docs/env_dns.md +25 -22
- package/rules/abie/js/docs/firebase_hosting.md +6 -0
- package/rules/abie/js/docs/hc_pairing.md +21 -25
- package/rules/abie/js/docs/ua_http_route.md +27 -19
- package/rules/abie/js/docs/ua_node_selector.md +24 -19
- package/rules/abie/lib/docs/enabled.md +13 -7
- package/rules/abie/lib/docs/env-dns.md +9 -3
- package/rules/abie/lib/docs/hc-yaml.md +6 -0
- package/rules/abie/lib/docs/http-route.md +6 -0
- package/rules/abie/lib/docs/k8s-tree.md +6 -0
- package/rules/abie/lib/docs/kustomization-patches.md +6 -0
- package/rules/abie/lib/docs/overlay-paths.md +6 -0
- package/rules/abie/lib/docs/yaml.md +6 -0
- package/rules/adr/docs/fix.md +6 -0
- package/rules/adr/js/docs/hooks.md +29 -244
- package/rules/bun/docs/fix.md +6 -0
- package/rules/bun/js/docs/layout.md +37 -375
- package/rules/capacitor/docs/fix.md +22 -108
- package/rules/capacitor/js/docs/platforms.md +62 -268
- package/rules/changelog/docs/fix.md +6 -0
- package/rules/changelog/lib/docs/package-manifest.md +6 -0
- package/rules/ci4/docs/fix.md +23 -165
- package/rules/ci4/js/docs/marksman_config.md +9 -1
- package/rules/docker/docs/fix.md +6 -0
- package/rules/docker/js/docs/lint.md +55 -239
- package/rules/docker/lib/docs/docker-hadolint.md +6 -0
- package/rules/docker/lib/docs/docker-mirror.md +6 -0
- package/rules/docker/lib/docs/docker-native-addon.md +6 -0
- package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
- package/rules/docker/lint/docs/lint.md +9 -1
- package/rules/efes/docs/fix.md +6 -0
- package/rules/ga/lint/docs/lint.md +6 -0
- package/rules/graphql/docs/fix.md +6 -0
- package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
- package/rules/image-avif/docs/fix.md +6 -0
- package/rules/image-avif/js/docs/avif_generation.md +6 -0
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
- package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
- package/rules/js-lint/js/docs/utils_imports.md +6 -0
- package/rules/js-lint-ci/docs/fix.md +7 -1
- package/rules/js-mssql/docs/fix.md +6 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
- package/rules/js-run/docs/fix.md +6 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
- package/rules/js-run/lib/docs/check-env-scan.md +6 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
- package/rules/js-run/lib/docs/temporal-scan.md +6 -0
- package/rules/k8s/docs/fix.md +6 -0
- package/rules/k8s/lint/docs/lint.md +6 -0
- package/rules/nginx-default-tpl/docs/fix.md +6 -0
- package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
- package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
- package/rules/php/docs/fix.md +6 -0
- package/rules/php/lint/docs/lint.md +6 -0
- package/rules/python/docs/fix.md +6 -0
- package/rules/python/lint/docs/lint.md +6 -0
- package/rules/rego/lint/docs/lint.md +6 -0
- package/rules/release/docs/change.md +6 -0
- package/rules/release/docs/fix.md +6 -0
- package/rules/release/docs/release.md +6 -0
- package/rules/release/lib/docs/aggregate.md +6 -0
- package/rules/release/lib/docs/change-file.md +6 -0
- package/rules/release/lib/docs/fallback.md +6 -0
- package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
- package/rules/security/docs/fix.md +7 -1
- package/rules/security/js/docs/lint.md +6 -0
- package/rules/style-lint/docs/fix.md +6 -0
- package/rules/tauri/docs/fix.md +6 -0
- package/rules/test/docs/fix.md +6 -0
- package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
- package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
- package/rules/text/docs/fix.md +6 -0
- package/rules/text/lint/docs/lint.md +6 -0
- package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
- package/rules/text/lint/docs/run-shellcheck.md +6 -0
- package/rules/text/lint/docs/run-v8r.md +6 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
- package/scripts/coverage-classify/cache.mjs +1 -1
- package/scripts/coverage-classify/docs/apply.md +6 -0
- package/scripts/coverage-classify/docs/cache.md +6 -0
- package/scripts/coverage-classify/docs/prompt.md +6 -0
- package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
- package/scripts/coverage-classify/index.mjs +24 -15
- package/scripts/coverage-classify/prompt.mjs +1 -1
- package/scripts/coverage-fix-extract.mjs +1 -1
- package/scripts/coverage-fix.mjs +2 -1
- package/scripts/docs/auto-skills.md +6 -0
- package/scripts/docs/build-agents-commands.md +7 -1
- package/scripts/docs/cli-entry.md +6 -0
- package/scripts/docs/coverage-fix-extract.md +6 -0
- package/scripts/docs/coverage-fix.md +6 -0
- package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
- package/scripts/docs/lint-cli.md +6 -0
- package/scripts/docs/post-tool-use-fix.md +6 -0
- package/scripts/docs/rename-yaml-extensions.md +6 -0
- package/scripts/docs/skills-cli.md +6 -0
- package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
- package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
- package/scripts/docs/worktree-cli.md +6 -0
- package/scripts/lib/docs/assert-project-root.md +6 -0
- package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
- package/scripts/lib/docs/check-reporter.md +6 -0
- package/scripts/lib/docs/diff-added-lines.md +6 -0
- package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
- package/scripts/lib/docs/discover-checkable-rules.md +6 -0
- package/scripts/lib/docs/ensure-tool.md +6 -0
- package/scripts/lib/docs/generated-markdown.md +6 -0
- package/scripts/lib/docs/gha-workflow.md +6 -0
- package/scripts/lib/docs/inline-template-links.md +6 -0
- package/scripts/lib/docs/list-rule-ids.md +6 -0
- package/scripts/lib/docs/load-cursor-config.md +6 -0
- package/scripts/lib/docs/mirror-parity.md +6 -0
- package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
- package/scripts/lib/docs/resolve-target-files.md +6 -0
- package/scripts/lib/docs/root-notice.md +6 -0
- package/scripts/lib/docs/rule-meta-helpers.md +6 -0
- package/scripts/lib/docs/rule-meta.md +6 -0
- package/scripts/lib/docs/run-conftest-batch.md +6 -0
- package/scripts/lib/docs/run-lint-step.md +6 -0
- package/scripts/lib/docs/run-rule-cli.md +6 -0
- package/scripts/lib/docs/run-rule.md +6 -0
- package/scripts/lib/docs/run-standard-lint.md +6 -0
- package/scripts/lib/docs/run-standard-rule.md +6 -0
- package/scripts/lib/docs/skill-meta.md +6 -0
- package/scripts/lib/docs/template.md +6 -0
- package/scripts/lib/docs/timing-summary.md +6 -0
- package/scripts/lib/docs/workspaces.md +6 -0
- package/scripts/lib/docs/worktree-notice.md +6 -0
- package/scripts/lib/docs/worktree.md +6 -0
- package/scripts/lib/mirror-parity.mjs +1 -1
- package/scripts/lib/root-notice.mjs +1 -1
- package/scripts/lib/worktree-notice.mjs +5 -5
- package/scripts/lib/worktree.mjs +1 -1
- package/scripts/sync-claude-config.mjs +3 -0
- package/scripts/utils/docs/ast-scan-utils.md +6 -0
- package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
- package/scripts/utils/docs/find-package-json-paths.md +6 -0
- package/scripts/utils/docs/lock-cache-dir.md +6 -0
- package/scripts/utils/docs/pass.md +6 -0
- package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
- package/scripts/utils/docs/resolve-cmd.md +6 -0
- package/scripts/utils/docs/resolve-js-root.md +6 -0
- package/scripts/utils/docs/test-helpers.md +6 -0
- package/scripts/utils/docs/walk-cache.md +6 -0
- package/scripts/utils/docs/walkDir.md +6 -0
- package/scripts/utils/docs/worktree-fingerprint.md +6 -0
- package/scripts/utils/resolve-js-root.mjs +1 -1
- package/skills/doc-aggregate/SKILL.md +129 -0
- package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
- package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
- package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
- package/skills/doc-files/SKILL.md +100 -0
- package/skills/doc-files/js/docgen-crc.mjs +164 -0
- package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +24 -15
- package/skills/{docgen → doc-files}/js/docgen-extract.mjs +15 -9
- package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
- package/skills/doc-files/js/docgen-gen.mjs +291 -0
- package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +43 -40
- package/skills/doc-files/js/docgen-scan.mjs +298 -0
- package/skills/doc-files/js/docs/docgen-crc.md +32 -0
- package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
- package/skills/doc-files/js/docs/docgen-extract.md +29 -0
- package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
- package/skills/doc-files/js/docs/docgen-gen.md +30 -0
- package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
- package/skills/doc-files/js/docs/docgen-scan.md +25 -0
- package/skills/doc-files/meta.json +1 -0
- package/skills/fix/js/docs/llm-worker.md +6 -0
- package/skills/fix/js/docs/orchestrator.md +6 -0
- package/skills/fix/js/llm-worker.mjs +23 -14
- package/skills/fix/js/orchestrator.mjs +1 -1
- package/skills/start-check/js/check.mjs +5 -3
- package/skills/start-check/js/docs/check.md +6 -0
- package/skills/docgen/SKILL.md +0 -224
- package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
- package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
- package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
- package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
- package/skills/docgen/js/docgen-batch.mjs +0 -95
- package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
- package/skills/docgen/js/docgen-gen.mjs +0 -339
- package/skills/docgen/js/docs/docgen-extract.md +0 -28
- package/skills/docgen/js/docs/docgen-gen.md +0 -41
- package/skills/docgen/js/docs/docgen-ignore.md +0 -24
- package/skills/docgen/js/docs/docgen-prompts.md +0 -24
- package/skills/docgen/js/docs/docgen-scan.md +0 -48
- /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
- /package/skills/{docgen → doc-files}/js/docgen-ignore.mjs +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-aggregate
|
|
3
|
+
description: >-
|
|
4
|
+
Агрегуюча документація за запитом: module-summary на кожен логічний модуль (docs/ARCHITECTURE.md) і доменні доки бізнес-процесів у кореневій docs/ — синтез поверх готових файлових док (doc-files), батч-диспатч субагентів у worktree
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# doc-aggregate — агрегуюча документація (за запитом)
|
|
8
|
+
|
|
9
|
+
## Мета
|
|
10
|
+
|
|
11
|
+
Синтезувати документацію вищого рівня поверх **готових файлових док** (їх підтримує
|
|
12
|
+
обовʼязковий скіл `doc-files`). Два рівні, строго послідовно:
|
|
13
|
+
|
|
14
|
+
1. **Tier 2 — module-summary**: `<module_root>/docs/ARCHITECTURE.md`, субагент на модуль.
|
|
15
|
+
2. **Tier 3 — доменні доки**: `docs/<домен>.md` у кореневій `docs/`, субагент-синтезатор
|
|
16
|
+
виділяє бізнес-домени й пише файл на кожен домен.
|
|
17
|
+
|
|
18
|
+
Агрегат ніколи не випереджає джерело: Tier 3 — лише після завершення всього Tier 2.
|
|
19
|
+
Цей скіл викликається **за запитом** (не обовʼязковий крок задачі).
|
|
20
|
+
|
|
21
|
+
## ⚠️ Паралелізм
|
|
22
|
+
|
|
23
|
+
Tier 2 — батчами **по 5** субагентів одночасно (кожен пише свій файл, гонок немає).
|
|
24
|
+
Tier 3 — **один** субагент-синтезатор після завершення всього Tier 2.
|
|
25
|
+
|
|
26
|
+
## Передумова
|
|
27
|
+
|
|
28
|
+
- Доступний `npx @nitra/cursor`.
|
|
29
|
+
- Файлові доки мають бути свіжі. Перевір і за потреби онови перед агрегацією:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @nitra/cursor doc-files check --git
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Якщо багато застарілих — спершу прожени `npx @nitra/cursor doc-files gen`.
|
|
36
|
+
|
|
37
|
+
## Крок 1: Tier 2 — module-summary
|
|
38
|
+
|
|
39
|
+
Зібрати список модулів:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @nitra/cursor doc-aggregate modules
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Команда друкує JSON-масив (усі шляхи абсолютні):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
"moduleRoot": "/abs/npm/rules/adr",
|
|
51
|
+
"relRoot": "npm/rules/adr",
|
|
52
|
+
"slug": "npm-rules-adr",
|
|
53
|
+
"docPath": "/abs/npm/rules/adr/docs/ARCHITECTURE.md",
|
|
54
|
+
"members": ["npm/rules/adr/index.mjs"],
|
|
55
|
+
"exists": false
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
module-summary **завжди регенерується** (це агрегат — поле `exists` ігноруй). Розбий модулі
|
|
61
|
+
на батчі по 5 і диспатч субагентів. Промпт кожного (підстав `relRoot`, `docPath`, `members`):
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Напиши module-summary для одного логічного модуля.
|
|
65
|
+
|
|
66
|
+
МОДУЛЬ: <relRoot>
|
|
67
|
+
ЗАПИСАТИ В: <docPath>
|
|
68
|
+
ФАЙЛИ МОДУЛЯ (members): <members>
|
|
69
|
+
|
|
70
|
+
Кроки:
|
|
71
|
+
1. Прочитай файлові доки членів модуля. <member> — sourcePath відносно кореня проєкту
|
|
72
|
+
(= поточний CWD); його файлова дока — <CWD>/<dir>/docs/<stem>.md. За потреби зазирни
|
|
73
|
+
в самі файли.
|
|
74
|
+
2. Створи теку для <docPath>, якщо її немає.
|
|
75
|
+
3. Запиши markdown у <docPath> за тими ж правилами стилю, що й файлова дока
|
|
76
|
+
(українська, чистий Markdown, контекстна незалежність, без HTML).
|
|
77
|
+
|
|
78
|
+
Секції module-summary:
|
|
79
|
+
## Огляд модуля — призначення модуля <relRoot>, його роль у проєкті.
|
|
80
|
+
## Ключові файли — список із кліковими посиланнями (відносними до розташування цього
|
|
81
|
+
ARCHITECTURE.md) на члени модуля та їхні файлові доки.
|
|
82
|
+
## Публічний API — що модуль експортує назовні.
|
|
83
|
+
## Внутрішній потік — як компоненти модуля взаємодіють.
|
|
84
|
+
## Підмодулі — вкладені модулі, якщо є.
|
|
85
|
+
|
|
86
|
+
Поверни лише підтвердження, що файл <docPath> записано.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Крок 2: Tier 3 — доменні доки
|
|
90
|
+
|
|
91
|
+
Після завершення **всіх** module-summary диспатч **одного** субагента-синтезатора.
|
|
92
|
+
У промпт підстав конкретний перелік шляхів module-summary (`<module_root>/docs/ARCHITECTURE.md`
|
|
93
|
+
кожного модуля з виводу `doc-aggregate modules`), а не інструкцію їх шукати. Промпт:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Синтезуй доменну документацію бізнес-процесів проєкту.
|
|
97
|
+
|
|
98
|
+
ДЖЕРЕЛА (module-summary, читай усі): <перелік шляхів ARCHITECTURE.md, підставлений вище>
|
|
99
|
+
|
|
100
|
+
Кроки:
|
|
101
|
+
1. Прочитай усі module-summary.
|
|
102
|
+
2. Виділи бізнес-домени та процеси (можуть перетинати межі модулів). Доменів може бути багато.
|
|
103
|
+
3. Для КОЖНОГО домену запиши окремий файл docs/<домен>.md у кореневій docs/:
|
|
104
|
+
- назва файлу — короткий kebab-slug домену;
|
|
105
|
+
- не перезаписуй файлові доки кореневих файлів у docs/ (напр. app.md, eslint.config.md):
|
|
106
|
+
якщо слаґ домену збігається з іменем такого файлу — додай суфікс -domain
|
|
107
|
+
(напр. app-domain.md). Інакше пиши docs/<домен>.md як є;
|
|
108
|
+
- опиши бізнес-процес домену з кліковими відносними посиланнями на module-summary, конкретні файли й директорії.
|
|
109
|
+
|
|
110
|
+
Правила стилю — ті ж (українська, чистий Markdown, контекстна незалежність, без HTML).
|
|
111
|
+
|
|
112
|
+
Поверни перелік створених файлів docs/<домен>.md.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Крок 3: Підсумок
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
✓ doc-aggregate завершено.
|
|
119
|
+
Tier 2 (модулі): <M> module-summary.
|
|
120
|
+
Tier 3 (домени): <D> доменних доків у docs/.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Перелічи файли з помилками (субагент впав або не записав `docPath`), якщо такі є.
|
|
124
|
+
Помилка одного модуля не зупиняє решту.
|
|
125
|
+
|
|
126
|
+
## Нотатки
|
|
127
|
+
|
|
128
|
+
- Не комітити автоматично — користувач вирішує, коли комітити згенеровану доку.
|
|
129
|
+
- Файлові доки (Tier 1) — окремий обовʼязковий скіл `doc-files`; цей скіл їх не пише, лише агрегує.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export спільного списку ignore-глобів зі скіла doc-files.
|
|
3
|
+
*
|
|
4
|
+
* Канонічне джерело — `npm/skills/doc-files/js/docgen-ignore.mjs`: обидва скіли
|
|
5
|
+
* (file-level доки і агрегати) мусять бачити однакове дерево кодових файлів,
|
|
6
|
+
* інакше агрегат посилатиметься на файли без док (або навпаки). Залежність
|
|
7
|
+
* спрямована doc-aggregate → doc-files за ADR про розбиття docgen.
|
|
8
|
+
*/
|
|
9
|
+
export * from '../../doc-files/js/docgen-ignore.mjs'
|
|
@@ -13,9 +13,7 @@ const SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.ts', '.vue', '.py'])
|
|
|
13
13
|
const TEST_FILE_RE = /\.(?:test|spec)\.[^.]+$/u
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Чи корінь має system-wide docs layout.
|
|
17
|
-
* Такий корінь зарезервований під репозиторні docs/adr, docs/explanation тощо,
|
|
18
|
-
* тому file-level docs у нього не пишемо.
|
|
16
|
+
* Чи корінь має system-wide docs layout (зарезервований під repo docs/adr тощо).
|
|
19
17
|
* @param {string} root абсолютний корінь обходу
|
|
20
18
|
* @returns {boolean} true — корінь system-wide docs
|
|
21
19
|
*/
|
|
@@ -26,7 +24,7 @@ function isSystemWideDocsRoot(root) {
|
|
|
26
24
|
/**
|
|
27
25
|
* Чи є файл кодовим джерелом для документування.
|
|
28
26
|
* @param {string} fileName базове ім'я файлу
|
|
29
|
-
* @returns {boolean} true —
|
|
27
|
+
* @returns {boolean} true — документуємо
|
|
30
28
|
*/
|
|
31
29
|
export function isSourceFile(fileName) {
|
|
32
30
|
if (fileName.endsWith('.d.ts')) return false
|
|
@@ -35,30 +33,14 @@ export function isSourceFile(fileName) {
|
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
|
-
*
|
|
39
|
-
* Якщо `sourcePath` відносний, `docPath` теж відносний; якщо абсолютний — абсолютний.
|
|
40
|
-
* @param {string} sourcePath шлях до джерела (відносний або абсолютний)
|
|
41
|
-
* @returns {string} шлях до `<dir>/docs/<stem>.md` у тому ж просторі шляхів
|
|
42
|
-
*/
|
|
43
|
-
export function docPathForSource(sourcePath) {
|
|
44
|
-
const dir = path.dirname(sourcePath)
|
|
45
|
-
const stem = path.basename(sourcePath, path.extname(sourcePath))
|
|
46
|
-
return path.join(dir, 'docs', `${stem}.md`)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Рекурсивно обходить дерево від `root`, повертає кодові файли для документування.
|
|
51
|
-
* Синхронний `readdirSync` — детермінований порядок і простий рекурсивний обхід без
|
|
52
|
-
* гонок; обсяг дерева проєкту це дозволяє.
|
|
36
|
+
* Рекурсивно збирає кодові файли проєкту (posix-шляхи від кореня).
|
|
53
37
|
* @param {string} root абсолютний корінь обходу
|
|
54
|
-
* @returns {
|
|
38
|
+
* @returns {string[]} sourcePath-и
|
|
55
39
|
*/
|
|
56
|
-
export function
|
|
40
|
+
export function scanSourceFiles(root) {
|
|
57
41
|
const results = []
|
|
58
42
|
|
|
59
|
-
/**
|
|
60
|
-
* @param {string} dir поточний каталог обходу
|
|
61
|
-
*/
|
|
43
|
+
/** @param {string} dir поточний каталог обходу */
|
|
62
44
|
function walk(dir) {
|
|
63
45
|
let entries
|
|
64
46
|
try {
|
|
@@ -76,12 +58,7 @@ export function scanForDocgen(root) {
|
|
|
76
58
|
if (isSystemWideDocsRoot(root) && path.dirname(relPath) === '.') continue
|
|
77
59
|
const sourcePath = relPath.split(path.sep).join('/')
|
|
78
60
|
if (isDocgenIgnored(sourcePath)) continue
|
|
79
|
-
|
|
80
|
-
results.push({
|
|
81
|
-
sourcePath,
|
|
82
|
-
docPath,
|
|
83
|
-
exists: existsSync(path.join(root, docPath))
|
|
84
|
-
})
|
|
61
|
+
results.push(sourcePath)
|
|
85
62
|
}
|
|
86
63
|
}
|
|
87
64
|
}
|
|
@@ -98,7 +75,6 @@ export function scanForDocgen(root) {
|
|
|
98
75
|
*/
|
|
99
76
|
export function slugForModule(root, moduleRoot) {
|
|
100
77
|
const rel = path.relative(root, moduleRoot)
|
|
101
|
-
// корінь репо: фіксований sentinel 'root'
|
|
102
78
|
if (rel === '') return 'root'
|
|
103
79
|
return rel
|
|
104
80
|
.split(path.sep)
|
|
@@ -108,7 +84,6 @@ export function slugForModule(root, moduleRoot) {
|
|
|
108
84
|
|
|
109
85
|
/**
|
|
110
86
|
* Знаходить корені модулів — теки з `package.json` (корінь завжди модуль).
|
|
111
|
-
* Ті ж ignore-glob правила, тож `package.json` у службових деревах не враховується.
|
|
112
87
|
* @param {string} root абсолютний корінь обходу
|
|
113
88
|
* @returns {string[]} абсолютні шляхи коренів модулів
|
|
114
89
|
*/
|
|
@@ -159,17 +134,17 @@ export function nearestModuleRoot(filePath, moduleRoots) {
|
|
|
159
134
|
* Лістить логічні модулі проєкту з членами-файлами і docPath module-summary.
|
|
160
135
|
* Модулі без кодових файлів пропускаються.
|
|
161
136
|
* @param {string} root абсолютний корінь обходу
|
|
162
|
-
* @returns {Array<{moduleRoot:string, relRoot:string, slug:string, docPath:string, members:string[], exists:boolean}>} модулі (members — sourcePath
|
|
137
|
+
* @returns {Array<{moduleRoot:string, relRoot:string, slug:string, docPath:string, members:string[], exists:boolean}>} модулі (members — sourcePath-и від root)
|
|
163
138
|
*/
|
|
164
139
|
export function scanForModules(root) {
|
|
165
|
-
const files =
|
|
140
|
+
const files = scanSourceFiles(root)
|
|
166
141
|
const moduleRoots = findModuleRoots(root)
|
|
167
142
|
const byRoot = new Map()
|
|
168
|
-
for (const
|
|
169
|
-
const moduleRoot = nearestModuleRoot(path.join(root,
|
|
143
|
+
for (const sourcePath of files) {
|
|
144
|
+
const moduleRoot = nearestModuleRoot(path.join(root, sourcePath), moduleRoots)
|
|
170
145
|
if (moduleRoot === null) continue
|
|
171
146
|
if (!byRoot.has(moduleRoot)) byRoot.set(moduleRoot, [])
|
|
172
|
-
byRoot.get(moduleRoot).push(
|
|
147
|
+
byRoot.get(moduleRoot).push(sourcePath)
|
|
173
148
|
}
|
|
174
149
|
|
|
175
150
|
const results = []
|
|
@@ -190,7 +165,7 @@ export function scanForModules(root) {
|
|
|
190
165
|
}
|
|
191
166
|
|
|
192
167
|
/**
|
|
193
|
-
* Парсить `--root <dir
|
|
168
|
+
* Парсить `--root <dir>`; default — cwd.
|
|
194
169
|
* @param {string[]} argv аргументи після підкоманди
|
|
195
170
|
* @returns {string} абсолютний корінь
|
|
196
171
|
*/
|
|
@@ -200,42 +175,22 @@ export function resolveRoot(argv) {
|
|
|
200
175
|
}
|
|
201
176
|
|
|
202
177
|
/**
|
|
203
|
-
*
|
|
204
|
-
* @param {string[]} argv аргументи після назви субкоманди
|
|
205
|
-
* @returns {
|
|
206
|
-
*/
|
|
207
|
-
export async function runDocgenScanCli(argv) {
|
|
208
|
-
const root = resolveRoot(argv)
|
|
209
|
-
|
|
210
|
-
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
211
|
-
console.error(`docgen scan: корінь не існує або не є директорією: ${root}`)
|
|
212
|
-
return 1
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const items = await scanForDocgen(root)
|
|
216
|
-
console.log(JSON.stringify(items, null, 2))
|
|
217
|
-
return 0
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Парсить `--root`, сканує модулі і друкує JSON-масив у stdout.
|
|
222
|
-
* @param {string[]} argv аргументи після назви субкоманди (наприклад ['--root', '<dir>'])
|
|
223
|
-
* @returns {Promise<number>} exit-код: 0 — успіх, 1 — корінь не існує
|
|
178
|
+
* `doc-aggregate modules` — сканує модулі і друкує JSON-масив у stdout.
|
|
179
|
+
* @param {string[]} argv аргументи після назви субкоманди
|
|
180
|
+
* @returns {number} exit-код: 0 — успіх, 1 — корінь не існує
|
|
224
181
|
*/
|
|
225
|
-
export
|
|
182
|
+
export function runDocAggregateModulesCli(argv) {
|
|
226
183
|
const root = resolveRoot(argv)
|
|
227
|
-
|
|
228
184
|
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
229
|
-
console.error(`
|
|
185
|
+
console.error(`doc-aggregate modules: корінь не існує або не є директорією: ${root}`)
|
|
230
186
|
return 1
|
|
231
187
|
}
|
|
232
|
-
|
|
233
|
-
const items = await scanForModules(root)
|
|
234
|
-
console.log(JSON.stringify(items, null, 2))
|
|
188
|
+
console.log(JSON.stringify(scanForModules(root), null, 2))
|
|
235
189
|
return 0
|
|
236
190
|
}
|
|
237
191
|
|
|
238
192
|
if (isRunAsCli(import.meta.url)) {
|
|
239
|
-
// Прямий запуск: `node skills/
|
|
240
|
-
|
|
193
|
+
// Прямий запуск: `node skills/doc-aggregate/js/docgen-scan.mjs modules --root <dir>`
|
|
194
|
+
const [sub, ...rest] = process.argv.slice(2)
|
|
195
|
+
process.exitCode = runDocAggregateModulesCli(sub === 'modules' ? rest : process.argv.slice(2))
|
|
241
196
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
docgen:
|
|
3
|
+
source: npm/skills/doc-aggregate/js/docgen-ignore.mjs
|
|
4
|
+
crc: 8821af65
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# docgen-ignore
|
|
8
|
+
|
|
9
|
+
## Огляд
|
|
10
|
+
|
|
11
|
+
Re-export спільного списку ignore-глобів зі скіла doc-files: обидва скіли документації (пофайлові доки й агрегати) мусять бачити однакове дерево кодових файлів, інакше агрегат посилатиметься на файли без док або навпаки.
|
|
12
|
+
|
|
13
|
+
## Поведінка
|
|
14
|
+
|
|
15
|
+
1. Модуль не має власної логіки: повністю делегує канонічному джерелу в doc-files і повторно експортує його API (список глобів і перевірку належності шляху до ігнорованих).
|
|
16
|
+
2. Напрям залежності — doc-aggregate → doc-files, відповідно до рішення про розбиття docgen на два скіли.
|
|
17
|
+
|
|
18
|
+
## Гарантії поведінки
|
|
19
|
+
|
|
20
|
+
- Списки ignore-глобів двох скілів не можуть розійтися — джерело одне.
|
|
21
|
+
- Read-only, без мережі й побічних ефектів.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-files
|
|
3
|
+
description: >-
|
|
4
|
+
Обовʼязковий крок задачі (як lint): для кожного зміненого/нового кодового файлу (js/mjs/ts/vue/py) JS-оркестрована генерація лаконічної поведінкової української md-документації у теку docs/ поряд із кодом, зі звіркою застарілості за CRC у frontmatter
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# doc-files — файлова документація (обовʼязковий крок)
|
|
8
|
+
|
|
9
|
+
## Мета
|
|
10
|
+
|
|
11
|
+
Для кожного кодового файлу проєкту тримати **актуальну** лаконічну поведінкову `.md`-документацію
|
|
12
|
+
у теці `docs/` **поряд із самим файлом** (`<dir>/docs/<stem>.md`). Це **обовʼязковий крок кожної
|
|
13
|
+
задачі** — як `lint`: після зміни коду його дока має бути перегенерована.
|
|
14
|
+
|
|
15
|
+
Застарілість визначається **детерміновано за CRC**: кожна дока несе у frontmatter контрольну
|
|
16
|
+
суму байтів джерела на момент генерації. Дока **застаріла**, якщо її немає або
|
|
17
|
+
`crc(поточне джерело) ≠ crc у frontmatter`.
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
---
|
|
21
|
+
docgen:
|
|
22
|
+
source: src/lib/foo.js
|
|
23
|
+
crc: a3f1c9e0
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Огляд
|
|
27
|
+
|
|
28
|
+
…
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Оркестрацію веде JS, не модель; конвеєр — local-only
|
|
32
|
+
|
|
33
|
+
Уся важка робота — черга, батчинг, виклики LLM і штамп CRC — живе в JS-команді
|
|
34
|
+
`doc-files gen`. **Ти не диспатчиш субагентів і не тримаєш сотні файлів у контексті**
|
|
35
|
+
— тому навіть масовий перший прогін усього репо не «заморює». Цей скіл **тонкий**: твоє завдання —
|
|
36
|
+
запустити генерацію і прочитати підсумок.
|
|
37
|
+
|
|
38
|
+
Конвеєр **суто локальний** (ADR `260610-2228`): будь-який файл генерується локальною
|
|
39
|
+
моделлю (`omlx/…` напряму), хмарних ескалацій немає. Якщо det-оцінка нижча за поріг —
|
|
40
|
+
дока все одно пишеться з **degraded-маркером** (`score`/`issues` у frontmatter, CRC свіжий),
|
|
41
|
+
а перегенерація таких док — окремою командою пізніше.
|
|
42
|
+
|
|
43
|
+
## Передумова
|
|
44
|
+
|
|
45
|
+
- Поточна директорія — корінь проєкту (`requireRoot`), не worktree.
|
|
46
|
+
- Доступний `npx @nitra/cursor`.
|
|
47
|
+
|
|
48
|
+
## Workflow
|
|
49
|
+
|
|
50
|
+
### Крок 1: Генерація застарілих/відсутніх док
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @nitra/cursor doc-files gen
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Команда сама: перевіряє omlx (preflight: «сервер лежить» / «модель не влазить у пам'ять
|
|
57
|
+
зайнятої машини» / «потрібен API-ключ» → одна зрозуміла зупинка замість лавини «✗») →
|
|
58
|
+
сканує проєкт → фільтрує застарілі (`stale`) → генерує локальною моделлю → пише доку зі
|
|
59
|
+
**свіжим CRC** (і degraded-маркером, якщо не дотягнула) → друкує прогрес і підсумок.
|
|
60
|
+
|
|
61
|
+
- Для дуже великого прогону можна порціями: `--from N --limit M`.
|
|
62
|
+
- Перегенерувати **всі** доки (не лише застарілі): `--overwrite`.
|
|
63
|
+
- Перегенерувати лише degraded (свіжі за CRC, score < порогу): `--retry-degraded`.
|
|
64
|
+
|
|
65
|
+
### Крок 2: Підтвердження
|
|
66
|
+
|
|
67
|
+
Дочекайся підсумку `✓ OK: <N> ⚠ degraded: <D> ✗ Err: <E>`. Якщо є помилки — перелічи
|
|
68
|
+
проблемні файли. Exit-код `1` означає, що хоча б один файл не згенерувався (або не пройшов
|
|
69
|
+
preflight). Degraded — не помилка: дока існує, борг видно у `check --degraded`.
|
|
70
|
+
|
|
71
|
+
### Крок 3 (за потреби): перевірка перед завершенням
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @nitra/cursor doc-files check --git
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Перевіряє змінені у задачі джерела (`git diff --name-only HEAD`) проти CRC їхніх док.
|
|
78
|
+
Цю ж перевірку виконує **Stop-hook** як твердий гейт: завершити задачу зі застарілими
|
|
79
|
+
доками не можна (виняток — масовий прогін понад поріг `N_CURSOR_DOC_FILES_GATE_MAX`, дефолт 50).
|
|
80
|
+
Degraded-доки гейт **не** блокує (CRC свіжий); їх список — `doc-files check --degraded`.
|
|
81
|
+
|
|
82
|
+
## Правила стилю документа (за adr/ci4)
|
|
83
|
+
|
|
84
|
+
- Мова — **УКРАЇНСЬКА** для всього тексту. Code identifiers, шляхи, імена API, команди — як у коді.
|
|
85
|
+
- **Чистий Markdown.** Жодних HTML-обгорток. Єдиний виняток — машинний `docgen:`-frontmatter із CRC.
|
|
86
|
+
- **Фокус на ПОВЕДІНЦІ, не реалізації.** ЩО і НАВІЩО, а не як саме зроблено.
|
|
87
|
+
- Не перелічуй модулі стандартної бібліотеки і внутрішні назви допоміжних функцій.
|
|
88
|
+
- Кожна секція самодостатня (без «як вище», «ця функція»).
|
|
89
|
+
- Секції (лише доречні): `## Огляд`, `## Поведінка`, `## Публічний API`, `## Де використовується`,
|
|
90
|
+
`## Гарантії поведінки`; для `.vue` — `## Інтерфейс компонента`.
|
|
91
|
+
- Не вигадуй деталей, яких немає в коді.
|
|
92
|
+
|
|
93
|
+
## Нотатки
|
|
94
|
+
|
|
95
|
+
- Не комітити автоматично — користувач вирішує, коли комітити згенеровану доку.
|
|
96
|
+
- Scanner ігнорує `node_modules`, `dist`, `.git`, `__pycache__`, `coverage`, `.cursor`, `.claude`,
|
|
97
|
+
усі теки `docs/`, а також `*.test.*` / `*.spec.*` / `*.d.ts`. Кореневий repo `docs/` —
|
|
98
|
+
system-wide only: file-level docs туди не пишуться. Список glob-ів — `docgen-ignore.mjs`.
|
|
99
|
+
- Агрегуюча документація (module-summary, доменні доки) — окремий скіл `doc-aggregate`, за запитом.
|
|
100
|
+
- Для наявних док без CRC одноразово: `npx @nitra/cursor doc-files stamp` (штампує frontmatter без LLM).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRC32 джерела + YAML-frontmatter файлової документації.
|
|
3
|
+
*
|
|
4
|
+
* Кожна файлова дока несе у frontmatter контрольну суму байтів джерела на момент
|
|
5
|
+
* генерації. Це детермінований маркер застарілості: `crc32(поточне джерело)` звіряється
|
|
6
|
+
* з `crc` у доці — розбіжність (або відсутня дока) означає, що дока відстала від коду.
|
|
7
|
+
* CRC не залежить від git-стану (rebase, незакомічене, гілки), тож придатний і для
|
|
8
|
+
* per-edit hook (бачить лише змінений файл), і для повного сканування.
|
|
9
|
+
*
|
|
10
|
+
* Degraded-маркер (ADR 260610-2228): якщо локальний конвеєр не дотягнув до порогу
|
|
11
|
+
* якості, дока все одно пишеться, а frontmatter додатково несе `score` (det-оцінка)
|
|
12
|
+
* та `issues` (коди проблем). CRC при цьому свіжий — Stop-гейт не блокує задачі через
|
|
13
|
+
* слабкість моделі; борг видимий через `check --degraded` і адресно перегенеровується
|
|
14
|
+
* через `gen --retry-degraded`.
|
|
15
|
+
*
|
|
16
|
+
* Frontmatter — єдиний дозволений виняток із правила «чистий Markdown без HTML»:
|
|
17
|
+
* це машинні метадані, не контент. Формат:
|
|
18
|
+
*
|
|
19
|
+
* ---
|
|
20
|
+
* docgen:
|
|
21
|
+
* source: src/lib/foo.js
|
|
22
|
+
* crc: a3f1c9e0
|
|
23
|
+
* score: 55
|
|
24
|
+
* issues: short-behavior,internal-name:bar
|
|
25
|
+
* ---
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
28
|
+
import { crc32 as zlibCrc32 } from 'node:zlib'
|
|
29
|
+
import { env } from 'node:process'
|
|
30
|
+
|
|
31
|
+
/** Поріг degraded: дока зі `score` нижче вважається неякісною. */
|
|
32
|
+
export const QUALITY_THRESHOLD = Number(env.N_CURSOR_DOC_FILES_THRESHOLD ?? 70) || 70
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* CRC32 вмісту у hex (8 символів, з провідними нулями). Делегує у нативний
|
|
36
|
+
* `node:zlib.crc32` — без ручної бітової арифметики.
|
|
37
|
+
* @param {string|Buffer} input текст або байти джерела
|
|
38
|
+
* @returns {string} CRC32 у hex
|
|
39
|
+
*/
|
|
40
|
+
export function crc32(input) {
|
|
41
|
+
const buf = typeof input === 'string' ? Buffer.from(input, 'utf8') : input
|
|
42
|
+
return zlibCrc32(buf).toString(16).padStart(8, '0')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Провідний YAML-frontmatter-блок `---\n…\n---`. */
|
|
46
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/u
|
|
47
|
+
const SOURCE_RE = /^[ \t]{0,8}source:[ \t]{0,8}(.+)$/mu
|
|
48
|
+
const CRC_RE = /^[ \t]{0,8}crc:[ \t]{0,8}(.+)$/mu
|
|
49
|
+
const SCORE_RE = /^[ \t]{0,8}score:[ \t]{0,8}(\d+)$/mu
|
|
50
|
+
const ISSUES_RE = /^[ \t]{0,8}issues:[ \t]{0,8}(.+)$/mu
|
|
51
|
+
const LEADING_NEWLINES_RE = /^\n+/u
|
|
52
|
+
const ISSUE_CODE_TAIL_RE = /[,:]$/u
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Парсить frontmatter файлової доки. Без блоку — `data:null` і `body` дорівнює входу.
|
|
56
|
+
* Поля `score`/`issues` опційні (back-compat зі старими доками): без них —
|
|
57
|
+
* `score:null`, `issues:[]`.
|
|
58
|
+
* @param {string} md вміст md-файлу
|
|
59
|
+
* @returns {{ data: { source: string|null, crc: string|null, score: number|null, issues: string[] }|null, body: string }} метадані + тіло без frontmatter
|
|
60
|
+
*/
|
|
61
|
+
export function parseDocFrontmatter(md) {
|
|
62
|
+
const match = md.match(FRONTMATTER_RE)
|
|
63
|
+
if (!match) return { data: null, body: md }
|
|
64
|
+
const block = match[1]
|
|
65
|
+
const scoreRaw = block.match(SCORE_RE)?.[1]
|
|
66
|
+
const issuesRaw = block.match(ISSUES_RE)?.[1]
|
|
67
|
+
return {
|
|
68
|
+
data: {
|
|
69
|
+
source: block.match(SOURCE_RE)?.[1].trim() ?? null,
|
|
70
|
+
crc: block.match(CRC_RE)?.[1].trim() ?? null,
|
|
71
|
+
score: scoreRaw === undefined ? null : Number(scoreRaw),
|
|
72
|
+
issues: issuesRaw
|
|
73
|
+
? issuesRaw
|
|
74
|
+
.split(',')
|
|
75
|
+
.map(s => s.trim())
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
: []
|
|
78
|
+
},
|
|
79
|
+
body: md.slice(match[0].length)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Максимум кодів issues у frontmatter — це маркер, а не повний лог. */
|
|
84
|
+
const MAX_ISSUE_CODES = 8
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Нормалізує issues до YAML-безпечних кодів: бере фрагмент до першого пробілу
|
|
88
|
+
* (зрізає людиночитні хвости помилок), відкидає порожні, обмежує кількість.
|
|
89
|
+
* @param {string[]} issues сирі issue-рядки від скорера
|
|
90
|
+
* @returns {string[]} коди без пробілів
|
|
91
|
+
*/
|
|
92
|
+
function issueCodes(issues) {
|
|
93
|
+
return issues
|
|
94
|
+
.map(i => String(i).split(' ')[0].replace(ISSUE_CODE_TAIL_RE, ''))
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.slice(0, MAX_ISSUE_CODES)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Будує frontmatter-блок із шляхом джерела, CRC і (опційно) якістю.
|
|
101
|
+
* @param {string} source відносний шлях джерела
|
|
102
|
+
* @param {string} crc CRC32 джерела у hex
|
|
103
|
+
* @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки; null — без полів якості
|
|
104
|
+
* @returns {string} рядок `---\ndocgen:\n source: …\n crc: …[\n score: …][\n issues: …]\n---\n`
|
|
105
|
+
*/
|
|
106
|
+
export function buildDocFrontmatter(source, crc, quality = null) {
|
|
107
|
+
const lines = [`source: ${source}`, `crc: ${crc}`]
|
|
108
|
+
if (quality && typeof quality.score === 'number') {
|
|
109
|
+
lines.push(`score: ${quality.score}`)
|
|
110
|
+
const codes = issueCodes(quality.issues ?? [])
|
|
111
|
+
if (codes.length > 0) lines.push(`issues: ${codes.join(',')}`)
|
|
112
|
+
}
|
|
113
|
+
const indented = lines.map(l => ' ' + l).join('\n')
|
|
114
|
+
return `---\ndocgen:\n${indented}\n---\n`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* (Пере)штампує frontmatter у md-доку: знімає наявний блок і додає свіжий.
|
|
119
|
+
* @param {string} md тіло доки (з frontmatter або без)
|
|
120
|
+
* @param {string} source відносний шлях джерела
|
|
121
|
+
* @param {string} crc CRC32 джерела у hex
|
|
122
|
+
* @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки
|
|
123
|
+
* @returns {string} md зі свіжим frontmatter
|
|
124
|
+
*/
|
|
125
|
+
export function stampDoc(md, source, crc, quality = null) {
|
|
126
|
+
const { body } = parseDocFrontmatter(md)
|
|
127
|
+
return `${buildDocFrontmatter(source, crc, quality)}\n${body.replace(LEADING_NEWLINES_RE, '')}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* CRC, збережений у frontmatter доки; `null` — доки немає або CRC відсутній.
|
|
132
|
+
* @param {string} docAbsPath абсолютний шлях md-доки
|
|
133
|
+
* @returns {string|null} CRC32 з frontmatter або null
|
|
134
|
+
*/
|
|
135
|
+
export function readDocCrc(docAbsPath) {
|
|
136
|
+
if (!existsSync(docAbsPath)) return null
|
|
137
|
+
return parseDocFrontmatter(readFileSync(docAbsPath, 'utf8')).data?.crc ?? null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Якість, збережена у frontmatter доки.
|
|
142
|
+
* @param {string} docAbsPath абсолютний шлях md-доки
|
|
143
|
+
* @returns {{ score: number|null, issues: string[] }} `score:null` — доки немає або поле відсутнє
|
|
144
|
+
*/
|
|
145
|
+
export function readDocQuality(docAbsPath) {
|
|
146
|
+
if (!existsSync(docAbsPath)) return { score: null, issues: [] }
|
|
147
|
+
const data = parseDocFrontmatter(readFileSync(docAbsPath, 'utf8')).data
|
|
148
|
+
return { score: data?.score ?? null, issues: data?.issues ?? [] }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Стан застарілості доки відносно її джерела.
|
|
153
|
+
* `missing` — доки немає; `crc-mismatch` — CRC джерела ≠ CRC у доці; інакше свіжа.
|
|
154
|
+
* @param {string} sourceAbsPath абсолютний шлях джерела
|
|
155
|
+
* @param {string} docAbsPath абсолютний шлях md-доки
|
|
156
|
+
* @returns {{ stale: boolean, reason: 'missing'|'crc-mismatch'|null }} стан застарілості
|
|
157
|
+
*/
|
|
158
|
+
export function staleness(sourceAbsPath, docAbsPath) {
|
|
159
|
+
const docCrc = readDocCrc(docAbsPath)
|
|
160
|
+
if (docCrc === null) return { stale: true, reason: 'missing' }
|
|
161
|
+
const srcCrc = crc32(readFileSync(sourceAbsPath))
|
|
162
|
+
if (srcCrc !== docCrc) return { stale: true, reason: 'crc-mismatch' }
|
|
163
|
+
return { stale: false, reason: null }
|
|
164
|
+
}
|