@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
|
@@ -19,9 +19,13 @@ const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]
|
|
|
19
19
|
const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
|
|
20
20
|
const CONFIG_REF_RE = /\b(\.[a-z][\w.-]*\.json)\b/gi
|
|
21
21
|
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
22
|
-
const CODE_BLOCK_RE = /```[a-z]
|
|
22
|
+
const CODE_BLOCK_RE = /```[a-z]{0,12}\n([\s\S]*?)\n[ \t]{0,8}\*?[ \t]{0,8}```/g
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Dedup масив, зберігаючи порядок появи.
|
|
26
|
+
* @param {Array<string>} arr вхідний масив
|
|
27
|
+
* @returns {Array<string>} масив без дублікатів у вихідному порядку
|
|
28
|
+
*/
|
|
25
29
|
function uniq(arr) {
|
|
26
30
|
const seen = new Set()
|
|
27
31
|
const out = []
|
|
@@ -36,17 +40,17 @@ function uniq(arr) {
|
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Витягує анкори з вихідного коду файла.
|
|
39
|
-
* @param {string} src
|
|
43
|
+
* @param {string} src вміст файлу
|
|
40
44
|
* @returns {{
|
|
41
45
|
* urls: string[],
|
|
42
46
|
* magicStrings: Array<{name:string, value:string}>,
|
|
43
47
|
* errorMarkers: string[],
|
|
44
48
|
* configRefs: string[],
|
|
45
49
|
* examples: string[]
|
|
46
|
-
* }}
|
|
50
|
+
* }} категоризовані анкори файлу
|
|
47
51
|
*/
|
|
48
52
|
export function extractAnchors(src) {
|
|
49
|
-
const urls = uniq(
|
|
53
|
+
const urls = uniq(Array.from(src.matchAll(URL_RE), m => m[0]))
|
|
50
54
|
|
|
51
55
|
const magicStrings = []
|
|
52
56
|
const seenNames = new Set()
|
|
@@ -59,12 +63,12 @@ export function extractAnchors(src) {
|
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
const errorMarkers = uniq(
|
|
63
|
-
const configRefs = uniq(
|
|
66
|
+
const errorMarkers = uniq(Array.from(src.matchAll(ERROR_MARKER_RE), m => m[1]))
|
|
67
|
+
const configRefs = uniq(Array.from(src.matchAll(CONFIG_REF_RE), m => m[1]))
|
|
64
68
|
|
|
65
69
|
// Витягуємо code-block приклади тільки з file-header — там автор зазвичай показує контракт.
|
|
66
70
|
const headerMatch = src.match(FILE_HEADER_RE)
|
|
67
|
-
const examples = headerMatch ? uniq(
|
|
71
|
+
const examples = headerMatch ? uniq(Array.from(headerMatch[1].matchAll(CODE_BLOCK_RE), m => m[1].trim())) : []
|
|
68
72
|
|
|
69
73
|
return { urls, magicStrings, errorMarkers, configRefs, examples }
|
|
70
74
|
}
|
|
@@ -73,20 +77,25 @@ export function extractAnchors(src) {
|
|
|
73
77
|
* Форматує анкори у компактний текст для system-промпта.
|
|
74
78
|
* Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про
|
|
75
79
|
* анкори не додається, щоб не вводити LLM в оману «обовʼязковими» полями).
|
|
76
|
-
* @param {ReturnType<typeof extractAnchors>} a
|
|
77
|
-
* @returns {string}
|
|
80
|
+
* @param {ReturnType<typeof extractAnchors>} a анкори файлу
|
|
81
|
+
* @returns {string} компактний текстовий блок або порожній рядок
|
|
78
82
|
*/
|
|
79
83
|
export function anchorsToPrompt(a) {
|
|
80
84
|
const blocks = []
|
|
81
85
|
if (a.urls.length) blocks.push(`URLs (згадай у тексті): ${a.urls.join(', ')}`)
|
|
82
86
|
if (a.magicStrings.length) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const consts = a.magicStrings.map(s => s.name + '=' + JSON.stringify(s.value)).join('; ')
|
|
88
|
+
blocks.push(`Експортовані константи-рядки (наведи назву і призначення): ${consts}`)
|
|
89
|
+
}
|
|
90
|
+
if (a.errorMarkers.length) {
|
|
91
|
+
const markers = a.errorMarkers.map(m => '(' + m + ')').join(', ')
|
|
92
|
+
blocks.push(`Маркери повідомлень (згадай у Поведінці): ${markers}`)
|
|
86
93
|
}
|
|
87
|
-
if (a.errorMarkers.length) blocks.push(`Маркери повідомлень (згадай у Поведінці): ${a.errorMarkers.map(m => `(${m})`).join(', ')}`)
|
|
88
94
|
if (a.configRefs.length) blocks.push(`Конфіги, на які спирається код: ${a.configRefs.join(', ')}`)
|
|
89
|
-
if (a.examples.length)
|
|
95
|
+
if (a.examples.length) {
|
|
96
|
+
const fenced = a.examples.map(e => '```\n' + e + '\n```').join('\n')
|
|
97
|
+
blocks.push(`Приклади з документації автора (наведи дослівно у Поведінці):\n${fenced}`)
|
|
98
|
+
}
|
|
90
99
|
if (!blocks.length) return ''
|
|
91
100
|
return `АНКОРИ ДО ОБОВ'ЯЗКОВОГО ВКЛЮЧЕННЯ:\n${blocks.join('\n')}`
|
|
92
101
|
}
|
|
@@ -25,15 +25,17 @@ const BUILTIN_MODULES = new Set([
|
|
|
25
25
|
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
26
26
|
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
27
27
|
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
28
|
-
const PARAM_LINE_RE = /^@param\
|
|
29
|
-
const RETURNS_LINE_RE = /^@returns
|
|
28
|
+
const PARAM_LINE_RE = /^@param[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?\[?([\w.]{1,80})\]?[ \t]{0,8}(.{0,400})$/
|
|
29
|
+
const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,400})$/
|
|
30
30
|
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
31
31
|
const PRECEDING_JSDOC_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
32
|
-
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(
|
|
33
|
-
const IMPORT_FROM_RE = /^import\
|
|
32
|
+
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
|
|
33
|
+
const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm
|
|
34
34
|
const NODE_PREFIX_RE = /^node:/
|
|
35
|
-
const INTERNAL_IMPORT_RE = /import\
|
|
36
|
-
const
|
|
35
|
+
const INTERNAL_IMPORT_RE = /import[ \t]{1,8}([^'"]{0,300}?)from[ \t]{1,8}['"]\.[^'"]{1,300}['"]/g
|
|
36
|
+
const NAMED_BRACES_RE = /\{([^}]{1,400})\}/
|
|
37
|
+
const IDENT_RE = /^[\w$]{1,80}$/
|
|
38
|
+
const IMPORT_AS_RE = /[ \t]{1,8}as[ \t]{1,8}.{0,200}/
|
|
37
39
|
const WRITE_FS_RE = /\b(writeFile|mkdir|rmdir|unlink|appendFile|createWriteStream|rm\()/
|
|
38
40
|
const CATCH_RE = /catch\s*\(/
|
|
39
41
|
const TRY_RE = /\btry\s*\{/
|
|
@@ -152,12 +154,16 @@ function extractImports(src) {
|
|
|
152
154
|
function extractInternalSymbols(src) {
|
|
153
155
|
const out = new Set()
|
|
154
156
|
for (const m of src.matchAll(INTERNAL_IMPORT_RE)) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const clause = m[1]
|
|
158
|
+
const named = clause.match(NAMED_BRACES_RE)
|
|
159
|
+
if (named) {
|
|
160
|
+
for (const n of named[1].split(',')) {
|
|
158
161
|
const name = n.replace(IMPORT_AS_RE, '').trim()
|
|
159
162
|
if (name) out.add(name)
|
|
160
163
|
}
|
|
164
|
+
}
|
|
165
|
+
const defName = clause.replace(NAMED_BRACES_RE, '').replaceAll(',', ' ').trim().split(' ')[0]
|
|
166
|
+
if (defName && IDENT_RE.test(defName)) out.add(defName)
|
|
161
167
|
}
|
|
162
168
|
return [...out]
|
|
163
169
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JS-оркестрація генерації файлових док (local-only, ADR 260610-2228).
|
|
3
|
+
*
|
|
4
|
+
* Уся черга/батчинг/CRC-штамп живуть тут, а не в контексті моделі — тому
|
|
5
|
+
* масовий перший прогін на сотні файлів не «заморює» агента. Конвеєр суто
|
|
6
|
+
* локальний: жодних cloud-ескалацій; якщо det-score нижче порогу — дока все
|
|
7
|
+
* одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а
|
|
8
|
+
* `gen --retry-degraded` адресно переганяє лише такі доки пізніше.
|
|
9
|
+
*
|
|
10
|
+
* Перед масовим прогоном — health-check omlx: memory-guard зайнятої 8GB машини
|
|
11
|
+
* означає «відклади прогін», а не сотні хибних «✗» у звіті.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
17
|
+
import { omlxHealthCheck, pickBackend } from '../../../lib/llm.mjs'
|
|
18
|
+
import { generateDoc, DEFAULT_LOCAL_MODEL } from './docgen-gen.mjs'
|
|
19
|
+
import { crc32, stampDoc, readDocQuality, QUALITY_THRESHOLD } from './docgen-crc.mjs'
|
|
20
|
+
import { resolveRoot, scanForDocFiles } from './docgen-scan.mjs'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Парсить `--limit N` / `--from N` / прапори режимів для дозапуску великого прогону.
|
|
24
|
+
* @param {string[]} argv аргументи
|
|
25
|
+
* @returns {{ from: number, limit: number, overwrite: boolean, retryDegraded: boolean }} зріз і режими
|
|
26
|
+
*/
|
|
27
|
+
function parseGenArgs(argv) {
|
|
28
|
+
const num = (flag, dflt) => {
|
|
29
|
+
const i = argv.indexOf(flag)
|
|
30
|
+
return i !== -1 && argv[i + 1] ? Number(argv[i + 1]) || dflt : dflt
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
from: num('--from', 0),
|
|
34
|
+
limit: num('--limit', Infinity),
|
|
35
|
+
overwrite: argv.includes('--overwrite'),
|
|
36
|
+
retryDegraded: argv.includes('--retry-degraded')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Цілі генерації за режимом:
|
|
42
|
+
* - default → застарілі (stale);
|
|
43
|
+
* - `--overwrite` → усі;
|
|
44
|
+
* - `--retry-degraded` → свіжі за CRC, але зі `score < QUALITY_THRESHOLD`.
|
|
45
|
+
* @param {string} root абсолютний корінь
|
|
46
|
+
* @param {Array<object>} all результат scanForDocFiles
|
|
47
|
+
* @param {{ overwrite: boolean, retryDegraded: boolean }} mode режими
|
|
48
|
+
* @returns {Array<object>} відфільтровані цілі
|
|
49
|
+
*/
|
|
50
|
+
function selectTargets(root, all, { overwrite, retryDegraded }) {
|
|
51
|
+
if (retryDegraded) {
|
|
52
|
+
return all.filter(f => {
|
|
53
|
+
if (f.stale) return false
|
|
54
|
+
const { score } = readDocQuality(join(root, f.docPath))
|
|
55
|
+
return score !== null && score < QUALITY_THRESHOLD
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
if (overwrite) return all
|
|
59
|
+
return all.filter(f => f.stale)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Preflight локального бекенда: для omlx-моделі — мінімальний chat-виклик.
|
|
64
|
+
* @returns {string|null} текст фатальної проблеми або null якщо можна генерувати
|
|
65
|
+
*/
|
|
66
|
+
function preflightProblem() {
|
|
67
|
+
if (pickBackend(DEFAULT_LOCAL_MODEL) !== 'omlx') return null
|
|
68
|
+
const hc = omlxHealthCheck({ model: DEFAULT_LOCAL_MODEL })
|
|
69
|
+
if (hc.ok) return null
|
|
70
|
+
if (hc.reason === 'memory-guard') {
|
|
71
|
+
return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}`
|
|
72
|
+
}
|
|
73
|
+
if (hc.reason === 'down') {
|
|
74
|
+
return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}`
|
|
75
|
+
}
|
|
76
|
+
if (hc.reason === 'auth') {
|
|
77
|
+
return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}`
|
|
78
|
+
}
|
|
79
|
+
return `omlx помилка: ${hc.detail}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `doc-files gen` — згенерувати документацію для застарілих/відсутніх док.
|
|
84
|
+
* @param {string[]} argv аргументи після назви субкоманди
|
|
85
|
+
* @returns {Promise<number>} exit-код: 0 — без помилок, 1 — хоча б одна помилка або фейл preflight
|
|
86
|
+
*/
|
|
87
|
+
export async function runDocFilesGenCli(argv) {
|
|
88
|
+
const root = resolveRoot(argv)
|
|
89
|
+
const { from, limit, overwrite, retryDegraded } = parseGenArgs(argv)
|
|
90
|
+
|
|
91
|
+
const all = scanForDocFiles(root)
|
|
92
|
+
const targets = selectTargets(root, all, { overwrite, retryDegraded }).slice(from, from + limit)
|
|
93
|
+
|
|
94
|
+
if (targets.length === 0) {
|
|
95
|
+
console.log(
|
|
96
|
+
retryDegraded
|
|
97
|
+
? '✓ doc-files: degraded-док немає. Нічого переганяти.'
|
|
98
|
+
: '✓ doc-files: усі файлові доки свіжі. Нічого генерувати.'
|
|
99
|
+
)
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const problem = preflightProblem()
|
|
104
|
+
if (problem) {
|
|
105
|
+
console.error(`✗ doc-files gen: ${problem}`)
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let modeTxt = ''
|
|
110
|
+
if (overwrite) modeTxt = ' (--overwrite)'
|
|
111
|
+
else if (retryDegraded) modeTxt = ' (--retry-degraded)'
|
|
112
|
+
console.log(`📋 doc-files: до генерації ${targets.length} файл(ів)${modeTxt}`)
|
|
113
|
+
const stats = { ok: 0, degraded: 0, err: 0, errors: [] }
|
|
114
|
+
|
|
115
|
+
let done = 0
|
|
116
|
+
for (const file of targets) {
|
|
117
|
+
done++
|
|
118
|
+
const sourceAbs = join(root, file.sourcePath)
|
|
119
|
+
process.stdout.write(` [${done}/${targets.length}] ${file.sourcePath} … `)
|
|
120
|
+
try {
|
|
121
|
+
const result = await generateDoc(sourceAbs)
|
|
122
|
+
const crc = crc32(readFileSync(sourceAbs))
|
|
123
|
+
const docAbs = join(root, file.docPath)
|
|
124
|
+
mkdirSync(dirname(docAbs), { recursive: true })
|
|
125
|
+
const quality =
|
|
126
|
+
result.score === null ? null : { score: result.score, issues: result.degraded ? result.issues : [] }
|
|
127
|
+
writeFileSync(docAbs, stampDoc(result.md, file.sourcePath, crc, quality))
|
|
128
|
+
stats.ok++
|
|
129
|
+
if (result.degraded) {
|
|
130
|
+
stats.degraded++
|
|
131
|
+
process.stdout.write(`⚠ degraded score=${result.score} crc=${crc}\n`)
|
|
132
|
+
} else {
|
|
133
|
+
process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc}\n`)
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
stats.err++
|
|
137
|
+
stats.errors.push(file.sourcePath)
|
|
138
|
+
process.stdout.write(`✗ ${error.message}\n`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`\n${'─'.repeat(50)}\n✓ OK: ${stats.ok} ⚠ degraded: ${stats.degraded} ✗ Err: ${stats.err}`)
|
|
143
|
+
if (stats.errors.length > 0) {
|
|
144
|
+
console.log('Помилки:')
|
|
145
|
+
for (const e of stats.errors) console.log(` - ${e}`)
|
|
146
|
+
}
|
|
147
|
+
if (stats.degraded > 0) {
|
|
148
|
+
console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor doc-files gen --retry-degraded`)
|
|
149
|
+
}
|
|
150
|
+
return stats.err > 0 ? 1 : 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `doc-files stamp` — детерміновано (пере)штампувати frontmatter `source`+`crc`
|
|
155
|
+
* у НАЯВНИХ доках без виклику LLM. Для міграції док, які ще не мають CRC.
|
|
156
|
+
* Поля якості (`score`/`issues`) при цьому зберігаються з наявного frontmatter.
|
|
157
|
+
* @param {string[]} argv аргументи після назви субкоманди
|
|
158
|
+
* @returns {number} exit-код: 0 — успіх
|
|
159
|
+
*/
|
|
160
|
+
export function runDocFilesStampCli(argv) {
|
|
161
|
+
const root = resolveRoot(argv)
|
|
162
|
+
let stamped = 0
|
|
163
|
+
for (const file of scanForDocFiles(root)) {
|
|
164
|
+
const docAbs = join(root, file.docPath)
|
|
165
|
+
if (!existsSync(docAbs)) continue
|
|
166
|
+
const sourceAbs = join(root, file.sourcePath)
|
|
167
|
+
const crc = crc32(readFileSync(sourceAbs))
|
|
168
|
+
const md = readFileSync(docAbs, 'utf8')
|
|
169
|
+
const { score, issues } = readDocQuality(docAbs)
|
|
170
|
+
writeFileSync(docAbs, stampDoc(md, file.sourcePath, crc, score === null ? null : { score, issues }))
|
|
171
|
+
stamped++
|
|
172
|
+
}
|
|
173
|
+
console.log(`✓ doc-files stamp: оновлено frontmatter у ${stamped} доці(ах).`)
|
|
174
|
+
return 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (isRunAsCli(import.meta.url)) {
|
|
178
|
+
const [sub, ...rest] = process.argv.slice(2)
|
|
179
|
+
const argv = sub === 'gen' || sub === 'stamp' ? rest : process.argv.slice(2)
|
|
180
|
+
process.exitCode = sub === 'stamp' ? runDocFilesStampCli(argv) : await runDocFilesGenCli(argv)
|
|
181
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/** @see ./docs/docgen-gen.md */
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { basename } from 'node:path'
|
|
4
|
+
import { env } from 'node:process'
|
|
5
|
+
import { resolveModel } from '../../../lib/models.mjs'
|
|
6
|
+
import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs'
|
|
7
|
+
import { callLlm } from '../../../lib/llm.mjs'
|
|
8
|
+
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
9
|
+
import { extractFacts } from './docgen-extract.mjs'
|
|
10
|
+
import { extractAnchors } from './docgen-extract-anchors.mjs'
|
|
11
|
+
import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
|
|
12
|
+
import {
|
|
13
|
+
oneShotMessages,
|
|
14
|
+
sectionMessages,
|
|
15
|
+
criticMessages,
|
|
16
|
+
refineMessages,
|
|
17
|
+
guaranteesFromMarkers
|
|
18
|
+
} from './docgen-prompts.mjs'
|
|
19
|
+
|
|
20
|
+
const FENCE_OPEN_RE = /^```[a-z]*\n?/
|
|
21
|
+
const FENCE_CLOSE_RE = /\n?```\s*$/
|
|
22
|
+
const LEADING_HEADING_RE = /^#{1,6}[ \t]{1,8}[^\n]{0,400}\n{1,8}/
|
|
23
|
+
const SECTION_HEADING_RE = /^##\s+(.+)/
|
|
24
|
+
const SECTION_KEY_CLEAN_RE = /[^а-яіїєґa-z0-9]/gi
|
|
25
|
+
const CACHE_MENTION_RE = /кеш/i
|
|
26
|
+
const CACHE_NEGATION_RE = /(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i
|
|
27
|
+
const CRITIC_NONE_RE = /^\s*NONE\s*$/i
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Прибирає код-фенс-обгортку (потрійні бектіки) й випадковий провідний
|
|
31
|
+
* `##`-заголовок із секції.
|
|
32
|
+
* @param {string} text сирий вихід моделі
|
|
33
|
+
* @returns {string} очищений текст секції
|
|
34
|
+
*/
|
|
35
|
+
function stripSection(text) {
|
|
36
|
+
let t = text.trim()
|
|
37
|
+
if (t.startsWith('```')) {
|
|
38
|
+
t = t.replace(FENCE_OPEN_RE, '').replace(FENCE_CLOSE_RE, '').trim()
|
|
39
|
+
}
|
|
40
|
+
t = t.replace(LEADING_HEADING_RE, '') // зрізати випадковий заголовок
|
|
41
|
+
return t.trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stage 2 (детермінований лінт, 0 токенів): зрізає сигнатури `name(args)` → `name`.
|
|
46
|
+
* Два проходи — щоб зняти вкладені виклики на кшталт `check(cwd = process.cwd())`.
|
|
47
|
+
* Не чіпає дужки без ідентифікатора перед ними (напр. `(abie.mdc)`, «(наприклад)»).
|
|
48
|
+
* @param {string} text текст секції
|
|
49
|
+
* @returns {string} текст без сигнатур у дужках
|
|
50
|
+
*/
|
|
51
|
+
function stripSignatures(text) {
|
|
52
|
+
let t = text
|
|
53
|
+
for (let i = 0; i < 2; i++) t = t.replaceAll(/([`\w$.]{1,80})\([^()]{0,300}\)/g, '$1')
|
|
54
|
+
return t
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Розбиває md на секції за ## заголовками.
|
|
59
|
+
* @param {string} md зібраний документ
|
|
60
|
+
* @returns {Record<string, string>} нормалізований ключ секції → її тіло
|
|
61
|
+
*/
|
|
62
|
+
function parseSections(md) {
|
|
63
|
+
const result = {}
|
|
64
|
+
let cur = null
|
|
65
|
+
for (const line of md.split('\n')) {
|
|
66
|
+
const m = line.match(SECTION_HEADING_RE)
|
|
67
|
+
if (m) {
|
|
68
|
+
cur = m[1].toLowerCase().replaceAll(SECTION_KEY_CLEAN_RE, '')
|
|
69
|
+
result[cur] = ''
|
|
70
|
+
} else if (cur) result[cur] += line + '\n'
|
|
71
|
+
}
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Чи містить текст бектік-обгорнуте імʼя символу (`sym`) — уникає substring false positives.
|
|
77
|
+
* @param {string} text текст секції
|
|
78
|
+
* @param {string} sym імʼя символу без бектіків
|
|
79
|
+
* @returns {boolean} true — імʼя згадано
|
|
80
|
+
*/
|
|
81
|
+
function hasName(text, sym) {
|
|
82
|
+
return text.includes('`' + sym + '`')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
|
|
87
|
+
* @param {string} md зібраний документ
|
|
88
|
+
* @param {object} facts факт-лист про файл
|
|
89
|
+
* @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
|
|
90
|
+
*/
|
|
91
|
+
function scoreDoc(md, facts) {
|
|
92
|
+
const s = parseSections(md)
|
|
93
|
+
let score = 100
|
|
94
|
+
const issues = []
|
|
95
|
+
|
|
96
|
+
if (!s['огляд']) {
|
|
97
|
+
score -= 25
|
|
98
|
+
issues.push('no-overview')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const behavior = s['поведінка'] ?? ''
|
|
102
|
+
if (behavior.length < 60) {
|
|
103
|
+
score -= 20
|
|
104
|
+
issues.push('short-behavior')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const guarantees = s['гарантіїповедінки'] ?? ''
|
|
108
|
+
// Будь-яка згадка "кеш" у Гарантіях коли файл не кешує — галюцинація
|
|
109
|
+
// Негація: "не кешує", "не має кешування", "без кешування", "немає кешу"
|
|
110
|
+
const cacheHit = CACHE_MENTION_RE.test(guarantees) && !CACHE_NEGATION_RE.test(guarantees)
|
|
111
|
+
if (!facts.markers?.caches && cacheHit) {
|
|
112
|
+
score -= 20
|
|
113
|
+
issues.push('cache-hallucination')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const sym of facts.internalSymbols ?? []) {
|
|
117
|
+
const inDoc = hasName(guarantees, sym) || hasName(s['огляд'] ?? '', sym) || hasName(s['поведінка'] ?? '', sym)
|
|
118
|
+
if (inDoc) {
|
|
119
|
+
score -= 10
|
|
120
|
+
issues.push(`internal-name:${sym}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { score: Math.max(0, score), issues }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* E2 — один цикл critique→refine на секцію.
|
|
129
|
+
* Повертає або уточнену чорнетку, або оригінал якщо критик повідомив NONE.
|
|
130
|
+
* @param {'overview'|'behavior'|'api'} sectionKey ключ секції
|
|
131
|
+
* @param {string} draft чорнетка секції
|
|
132
|
+
* @param {object} facts факт-лист
|
|
133
|
+
* @param {object|null} anchors анкори файлу
|
|
134
|
+
* @param {string} model model-id
|
|
135
|
+
* @param {number} timeoutMs ліміт на один виклик
|
|
136
|
+
* @returns {string} фінальний текст секції
|
|
137
|
+
*/
|
|
138
|
+
function critiqueRefineSection(sectionKey, draft, facts, anchors, model, timeoutMs) {
|
|
139
|
+
const critique = callLlm(criticMessages(sectionKey, draft, facts, anchors), model, { timeoutMs }).trim()
|
|
140
|
+
if (!critique || CRITIC_NONE_RE.test(critique) || critique.length < 12) return draft
|
|
141
|
+
const refined = callLlm(refineMessages(sectionKey, draft, critique, facts, anchors), model, { timeoutMs }).trim()
|
|
142
|
+
return stripSignatures(stripSection(refined)) || draft
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Чи треба refine для секції API: тільки якщо є >1 експорту і всі desc-и порожні
|
|
147
|
+
* (саме там модель схильна писати «застосовує логіку до файлу»).
|
|
148
|
+
* @param {object} facts факт-лист
|
|
149
|
+
* @returns {boolean} true — секцію API варто прогнати через критика
|
|
150
|
+
*/
|
|
151
|
+
function apiNeedsRefine(facts) {
|
|
152
|
+
const exps = facts.exports ?? []
|
|
153
|
+
if (exps.length <= 1) return false
|
|
154
|
+
return exps.every(e => !e.desc)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One-shot: один виклик LLM на весь документ (для unsupported-структур).
|
|
159
|
+
* @param {object} facts факт-лист
|
|
160
|
+
* @param {string} src вміст файлу
|
|
161
|
+
* @param {string} model model-id
|
|
162
|
+
* @param {number} [timeoutMs] ліміт на виклик
|
|
163
|
+
* @returns {{ md: string }} зібраний документ
|
|
164
|
+
*/
|
|
165
|
+
function oneShotDoc(facts, src, model, timeoutMs = LOCAL_TIMEOUT_MS) {
|
|
166
|
+
const text = callLlm(oneShotMessages(facts, src), model, { timeoutMs })
|
|
167
|
+
let md = stripSignatures(stripSection(text))
|
|
168
|
+
if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
|
|
169
|
+
return { md: md + '\n' }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stage 3: фіксовані заголовки у фіксованому порядку.
|
|
174
|
+
* @param {string} stem назва файлу для H1
|
|
175
|
+
* @param {Record<string, string>} sections тексти секцій за ключами
|
|
176
|
+
* @returns {string} зібраний md-документ
|
|
177
|
+
*/
|
|
178
|
+
function assemble(stem, sections) {
|
|
179
|
+
const order = [
|
|
180
|
+
['overview', '## Огляд'],
|
|
181
|
+
['behavior', '## Поведінка'],
|
|
182
|
+
['api', '## Публічний API'],
|
|
183
|
+
['guarantees', '## Гарантії поведінки']
|
|
184
|
+
]
|
|
185
|
+
const parts = [`# ${stem}`]
|
|
186
|
+
for (const [key, title] of order) {
|
|
187
|
+
const body = sections[key]
|
|
188
|
+
if (body && body.trim()) parts.push(`${title}\n\n${body.trim()}`)
|
|
189
|
+
}
|
|
190
|
+
return parts.join('\n\n') + '\n'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Orchestrated: N окремих LLM-викликів, по одному на секцію.
|
|
195
|
+
* Код потрапляє лише в `behavior`; решта секцій — на мінімальному факт-листі.
|
|
196
|
+
* @param {object} facts факт-лист
|
|
197
|
+
* @param {string} src вміст файлу
|
|
198
|
+
* @param {string} model model-id
|
|
199
|
+
* @param {number} timeoutMs ліміт на один виклик
|
|
200
|
+
* @param {{ anchors?: object|null, temperature?: number }} [opts] анкори й температура семплінгу
|
|
201
|
+
* @returns {{ md: string }} зібраний документ
|
|
202
|
+
*/
|
|
203
|
+
function orchestratedDoc(facts, src, model, timeoutMs, { anchors = null, temperature = 0.2 } = {}) {
|
|
204
|
+
const sections = {}
|
|
205
|
+
const anc = anchors ?? extractAnchors(src)
|
|
206
|
+
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
|
|
207
|
+
sections.guarantees = guaranteesFromMarkers(facts)
|
|
208
|
+
for (const s of sectionMessages(facts, src, anc)) {
|
|
209
|
+
if (s.key === 'guarantees') continue // вже згенеровано детерміновано
|
|
210
|
+
let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
|
|
211
|
+
// E2 + E3: critique→refine лише для секцій, де мала модель зриває на generic
|
|
212
|
+
if (s.key === 'overview' || (s.key === 'api' && apiNeedsRefine(facts))) {
|
|
213
|
+
draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
|
|
214
|
+
}
|
|
215
|
+
sections[s.key] = draft
|
|
216
|
+
}
|
|
217
|
+
return { md: assemble(basename(facts.relPath), sections) }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Максимальний час генерації одного LLM-виклику. */
|
|
221
|
+
const LOCAL_TIMEOUT_MS = 5 * 60 * 1000
|
|
222
|
+
/**
|
|
223
|
+
* Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') → omlx напряму.
|
|
224
|
+
* Останній fallback гарантує local-only шлях без жодних env (через pi CLI той
|
|
225
|
+
* самий локальний виклик виміряно повільніший на ~46%).
|
|
226
|
+
*/
|
|
227
|
+
export const DEFAULT_LOCAL_MODEL = env.N_CURSOR_DOCGEN_MODEL ?? (resolveModel('min') || `omlx/${DEFAULT_OMLX_MODEL}`)
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Головний API: файл → md-дока з det-оцінкою.
|
|
231
|
+
*
|
|
232
|
+
* Local-only (ADR 260610-2228): жодних cloud-ескалацій і pre-route — будь-який
|
|
233
|
+
* файл генерується локальною моделлю. Якщо det-score нижче порогу, один retry
|
|
234
|
+
* з вищою температурою (best-of-2); якщо й він не допоміг — результат
|
|
235
|
+
* позначається `degraded`, рішення про перегенерацію приймає batch/користувач.
|
|
236
|
+
* @param {string} file абсолютний шлях джерела
|
|
237
|
+
* @param {{ model?: string, threshold?: number }} [opts] model-id і поріг degraded
|
|
238
|
+
* @returns {{ md: string, ms: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації
|
|
239
|
+
*/
|
|
240
|
+
export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUALITY_THRESHOLD } = {}) {
|
|
241
|
+
const src = readFileSync(file, 'utf8')
|
|
242
|
+
const facts = extractFacts(src, file)
|
|
243
|
+
const t0 = Date.now()
|
|
244
|
+
|
|
245
|
+
const anchors = facts.unsupported ? null : extractAnchors(src)
|
|
246
|
+
let r = facts.unsupported
|
|
247
|
+
? oneShotDoc(facts, src, model)
|
|
248
|
+
: orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors })
|
|
249
|
+
|
|
250
|
+
// unsupported (vue/py до юніт-шару): скорер не застосовний — score=null, не degraded
|
|
251
|
+
if (facts.unsupported) {
|
|
252
|
+
return { ...r, ms: Date.now() - t0, score: null, issues: [], degraded: false, model }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Stage 2.5: детермінований скоринг (0 токенів)
|
|
256
|
+
let { score, issues } = scoreDoc(r.md, facts)
|
|
257
|
+
|
|
258
|
+
// E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
|
|
259
|
+
if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
|
|
260
|
+
try {
|
|
261
|
+
const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
|
|
262
|
+
const s2 = scoreDoc(r2.md, facts)
|
|
263
|
+
if (s2.score > score) {
|
|
264
|
+
r = r2
|
|
265
|
+
score = s2.score
|
|
266
|
+
issues = [...s2.issues, 'best-of-2:retry-won']
|
|
267
|
+
} else {
|
|
268
|
+
issues = [...issues, 'best-of-2:retry-lost']
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
issues = [...issues, `best-of-2:retry-error: ${error.message}`]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { ...r, ms: Date.now() - t0, score, issues, degraded: score < threshold, model }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// CLI: node docgen-gen.mjs <file> [--model <m>]
|
|
279
|
+
if (isRunAsCli(import.meta.url)) {
|
|
280
|
+
const args = process.argv.slice(2)
|
|
281
|
+
const file = args.find(a => !a.startsWith('--'))
|
|
282
|
+
const mi = args.indexOf('--model')
|
|
283
|
+
const model = mi === -1 ? DEFAULT_LOCAL_MODEL : args[mi + 1]
|
|
284
|
+
if (!file) {
|
|
285
|
+
throw new Error('Usage: node docgen-gen.mjs <file> [--model <m>]')
|
|
286
|
+
}
|
|
287
|
+
const r = generateDoc(file, { model })
|
|
288
|
+
const issuesTxt = r.issues?.length ? ` issues=${r.issues.join(',')}` : ''
|
|
289
|
+
process.stderr.write(`[local ${r.model}] ${r.ms}ms / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n`)
|
|
290
|
+
process.stdout.write(r.md)
|
|
291
|
+
}
|