@nitra/cursor 4.1.0 → 4.1.2

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 (135) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-cursor.js +25 -13
  3. package/docs/flow.MD +1364 -0
  4. package/docs/stryker.config.md +37 -0
  5. package/docs/vitest.config.md +23 -0
  6. package/lib/models.mjs +1 -2
  7. package/package.json +2 -1
  8. package/rules/abie/fix.mjs +1 -1
  9. package/rules/bun/docs/fix.md +3 -0
  10. package/rules/bun/fix.mjs +1 -1
  11. package/rules/capacitor/fix.mjs +1 -1
  12. package/rules/changelog/docs/fix.md +3 -0
  13. package/rules/changelog/fix.mjs +1 -1
  14. package/rules/ci4/fix.mjs +1 -1
  15. package/rules/ci4/js/docs/marksman_config.md +1 -0
  16. package/rules/docker/docs/fix.md +1 -1
  17. package/rules/docker/fix.mjs +1 -1
  18. package/rules/docker/lint/docs/lint.md +1 -0
  19. package/rules/efes/docs/fix.md +2 -1
  20. package/rules/efes/fix.mjs +1 -1
  21. package/rules/feedback/fix.mjs +1 -1
  22. package/rules/ga/fix.mjs +1 -1
  23. package/rules/ga/js/lint.mjs +1 -1
  24. package/rules/graphql/docs/fix.md +4 -1
  25. package/rules/graphql/fix.mjs +1 -1
  26. package/rules/graphql/lib/docs/graphql-gql-scan.md +3 -0
  27. package/rules/hasura/fix.mjs +1 -1
  28. package/rules/image-avif/docs/fix.md +4 -1
  29. package/rules/image-avif/fix.mjs +1 -1
  30. package/rules/image-avif/js/docs/avif_generation.md +1 -0
  31. package/rules/image-compress/fix.mjs +1 -1
  32. package/rules/js-bun-db/fix.mjs +1 -1
  33. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +6 -0
  34. package/rules/js-bun-redis/fix.mjs +1 -1
  35. package/rules/js-lint/fix.mjs +1 -1
  36. package/rules/js-lint/js/docs/utils_imports.md +1 -0
  37. package/rules/js-lint-ci/docs/fix.md +4 -1
  38. package/rules/js-lint-ci/fix.mjs +1 -1
  39. package/rules/js-mssql/docs/fix.md +3 -0
  40. package/rules/js-mssql/fix.mjs +1 -1
  41. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +9 -0
  42. package/rules/js-run/docs/fix.md +3 -0
  43. package/rules/js-run/fix.mjs +1 -1
  44. package/rules/js-run/lib/docs/check-env-scan.md +2 -1
  45. package/rules/js-run/lib/docs/promise-settimeout-scan.md +4 -0
  46. package/rules/k8s/docs/fix.md +3 -0
  47. package/rules/k8s/fix.mjs +1 -1
  48. package/rules/nginx-default-tpl/docs/fix.md +3 -0
  49. package/rules/nginx-default-tpl/fix.mjs +1 -1
  50. package/rules/npm-module/fix.mjs +1 -1
  51. package/rules/npm-module/js/header_doc_pointer.mjs +14 -3
  52. package/rules/php/docs/fix.md +2 -1
  53. package/rules/php/fix.mjs +1 -1
  54. package/rules/python/docs/fix.md +4 -1
  55. package/rules/python/fix.mjs +1 -1
  56. package/rules/rego/fix.mjs +1 -1
  57. package/rules/rego/js/lint.mjs +1 -1
  58. package/rules/release/docs/fix.md +4 -1
  59. package/rules/release/fix.mjs +1 -1
  60. package/rules/rust/fix.mjs +1 -1
  61. package/rules/security/docs/fix.md +1 -1
  62. package/rules/security/fix.mjs +1 -1
  63. package/rules/style-lint/docs/fix.md +3 -0
  64. package/rules/style-lint/fix.mjs +1 -1
  65. package/rules/tauri/docs/fix.md +4 -1
  66. package/rules/tauri/fix.mjs +1 -1
  67. package/rules/test/docs/fix.md +3 -0
  68. package/rules/test/fix.mjs +1 -1
  69. package/rules/test/js/no-relative-fs-path.mjs +2 -1
  70. package/rules/text/docs/fix.md +3 -0
  71. package/rules/text/fix.mjs +1 -1
  72. package/rules/text/js/lint.mjs +1 -1
  73. package/rules/vue/fix.mjs +1 -1
  74. package/rules/worktree/fix.mjs +1 -1
  75. package/scripts/auto-rules.mjs +1 -1
  76. package/scripts/coverage-classify/index.mjs +10 -10
  77. package/scripts/coverage-fix.mjs +2 -2
  78. package/scripts/dispatcher/graph/lib/cmd-init.mjs +112 -0
  79. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +96 -0
  80. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +141 -0
  81. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +142 -0
  82. package/scripts/dispatcher/graph/lib/cmd-run.mjs +328 -0
  83. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +115 -0
  84. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +111 -0
  85. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +328 -0
  86. package/scripts/dispatcher/graph/lib/cmd-status.mjs +131 -0
  87. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +100 -0
  88. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +128 -0
  89. package/scripts/dispatcher/graph/lib/config.mjs +103 -0
  90. package/scripts/dispatcher/graph/lib/frontmatter.mjs +224 -0
  91. package/scripts/dispatcher/graph/lib/nnn.mjs +127 -0
  92. package/scripts/dispatcher/graph/lib/node-state.mjs +157 -0
  93. package/scripts/dispatcher/graph/lib/scanner.mjs +235 -0
  94. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +193 -0
  95. package/scripts/dispatcher/graph-tasks.mjs +92 -0
  96. package/scripts/dispatcher/index.mjs +3 -3
  97. package/scripts/dispatcher/lib/docs/events.md +1 -0
  98. package/scripts/dispatcher/lib/executor.mjs +1 -1
  99. package/scripts/dispatcher/lib/subagent-runner.mjs +9 -9
  100. package/scripts/dispatcher/trace.mjs +6 -2
  101. package/scripts/docs/build-agents-commands.md +1 -0
  102. package/scripts/docs/cli-entry.md +6 -0
  103. package/scripts/graph/index.mjs +115 -0
  104. package/scripts/graph/lib/config.mjs +62 -0
  105. package/scripts/graph/lib/dag.mjs +161 -0
  106. package/scripts/graph/lib/frontmatter.mjs +70 -0
  107. package/scripts/graph/lib/nnn.mjs +77 -0
  108. package/scripts/graph/lib/state.mjs +110 -0
  109. package/scripts/graph/scan.mjs +64 -0
  110. package/scripts/graph/status.mjs +86 -0
  111. package/scripts/lib/docs/load-cursor-config.md +3 -0
  112. package/scripts/lib/root-notice.mjs +4 -2
  113. package/scripts/lib/rule-predicates.mjs +1 -1
  114. package/scripts/lib/worktree-notice.mjs +14 -7
  115. package/scripts/lib/worktree.mjs +3 -2
  116. package/scripts/utils/resolve-js-root.mjs +2 -1
  117. package/scripts/utils/with-lock.mjs +1 -1
  118. package/skills/docgen/js/docgen-batch.mjs +7 -7
  119. package/skills/docgen/js/docgen-extract.mjs +80 -37
  120. package/skills/docgen/js/docgen-ignore.mjs +1 -1
  121. package/skills/docgen/js/docgen-prompts.mjs +21 -5
  122. package/skills/fix/js/llm-worker.mjs +19 -22
  123. package/skills/fix/js/orchestrator.mjs +6 -7
  124. package/skills/fix/js/t0.mjs +14 -13
  125. package/types/bin/n-cursor.d.ts +1 -1
  126. package/rules/flow/docs/fix.md +0 -152
  127. package/rules/flow/fix.mjs +0 -18
  128. package/rules/flow/flow.mdc +0 -127
  129. package/rules/flow/meta.json +0 -1
  130. package/scripts/dispatcher/lib/docs/flow-lock.md +0 -161
  131. package/scripts/dispatcher/lib/docs/flow-resolve.md +0 -267
  132. package/scripts/dispatcher/lib/flow-plan.mjs +0 -153
  133. package/scripts/dispatcher/lib/flow-resolve.mjs +0 -156
  134. package/scripts/dispatcher/lib/flow-signals.mjs +0 -235
  135. package/scripts/dispatcher/lib/flow-verify.mjs +0 -127
@@ -1,156 +0,0 @@
1
- /**
2
- * cwd-незалежний резолвер активного flow (беклог адаптації #1).
3
- *
4
- * Команди `spec/plan/verify/review/gate/release` мають знаходити `.flow.json`
5
- * поточної задачі навіть коли їх запущено НЕ з кореня worktree (напр. з головного
6
- * дерева чи з підтеки worktree) — інакше `flowStatePath(cwd)` обчислює хибний шлях
7
- * і видає «стану нема», хоча flow активний.
8
- *
9
- * Порядок (spec 2026-06-01-flow-cwd-state-resolution):
10
- * 1. явний `branch` → `.worktrees/<sanitizeBranch>.flow.json`;
11
- * 2. toplevel-резолвинг: `git rev-parse --show-toplevel` від `cwd`; якщо toplevel
12
- * лежить безпосередньо під `<repoRoot>/.worktrees/` і для нього є стан — беремо;
13
- * 3. скан `<repoRoot>/.worktrees/*.flow.json` зі `status: in_progress`: рівно один →
14
- * авторезолв; кілька → помилка зі списком; нуль → «стану нема».
15
- *
16
- * Резолвер не пише на диск. `git`/FS ін'єктуються — тестується без репозиторію.
17
- */
18
- import { existsSync, readdirSync } from 'node:fs'
19
- import { spawnSync } from 'node:child_process'
20
- import { basename, dirname, join } from 'node:path'
21
- import { cwd as processCwd } from 'node:process'
22
-
23
- import { sanitizeBranch, worktreePaths } from '../../lib/worktree.mjs'
24
- import { flowStatePath, readState as defaultReadState } from './state-store.mjs'
25
-
26
- const FLOW_STATE_SUFFIX = '.flow.json'
27
-
28
- /**
29
- * Реальний sync git-runner у заданому `cwd`.
30
- * @param {string[]} args аргументи git
31
- * @param {string} cwd робочий каталог
32
- * @returns {{ status: number, stdout: string }} результат
33
- */
34
- function realGit(args, cwd) {
35
- const r = spawnSync('git', args, { encoding: 'utf8', cwd })
36
- return { status: r.status ?? 1, stdout: r.stdout ?? '' }
37
- }
38
-
39
- /**
40
- * Корінь головного worktree через `git worktree list --porcelain` (перший запис).
41
- * @param {(args: string[]) => { status: number, stdout: string }} git git-runner
42
- * @returns {string | null} абсолютний шлях кореня репо або `null`
43
- */
44
- function mainRepoRoot(git) {
45
- const r = git(['worktree', 'list', '--porcelain'])
46
- if ((r.status ?? 1) !== 0) return null
47
- const line = r.stdout.split('\n').find(l => l.startsWith('worktree '))
48
- const root = line ? line.slice('worktree '.length).trim() : ''
49
- return root.length > 0 ? root : null
50
- }
51
-
52
- /**
53
- * Корінь поточного worktree (`git rev-parse --show-toplevel`).
54
- * @param {(args: string[]) => { status: number, stdout: string }} git git-runner
55
- * @returns {string | null} абсолютний шлях або `null`
56
- */
57
- function currentToplevel(git) {
58
- const r = git(['rev-parse', '--show-toplevel'])
59
- return (r.status ?? 1) === 0 && r.stdout.trim().length > 0 ? r.stdout.trim() : null
60
- }
61
-
62
- /**
63
- * @typedef {object} ResolvedFlow
64
- * @property {string | null} statePath абсолютний шлях `.flow.json` або `null`
65
- * @property {string | null} worktreeDir тека worktree (ефективний cwd для гейтів) або `null`
66
- * @property {string | null} label мітка flow (sanitized branch) або `null`
67
- * @property {boolean} autoResolved `true`, якщо знайдено скануванням (cwd поза worktree)
68
- * @property {string | null} error повідомлення для логу, якщо `statePath === null`
69
- */
70
-
71
- /**
72
- * Резолвить активний flow незалежно від `cwd`.
73
- * @param {{ cwd?: string, branch?: string }} [params] параметри
74
- * @param {{ git?: (args: string[]) => { status: number, stdout: string }, exists?: (p: string) => boolean, readState?: (p: string) => object | null, readdir?: (d: string) => string[], repoRoot?: string }} [deps] ін'єкції
75
- * @returns {ResolvedFlow} результат
76
- */
77
- export function resolveActiveFlowState({ cwd = processCwd(), branch } = {}, deps = {}) {
78
- const git = deps.git ?? (args => realGit(args, cwd))
79
- const exists = deps.exists ?? existsSync
80
- const readState = deps.readState ?? defaultReadState
81
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
82
-
83
- const resolveRoot = () => deps.repoRoot ?? mainRepoRoot(git)
84
-
85
- // 1. Явний --branch завжди перемагає. Валідуємо існування теки worktree, щоб
86
- // команда не пішла виконувати гейти в неіснуючому каталозі (ENOENT).
87
- if (branch) {
88
- const repoRoot = resolveRoot()
89
- if (!repoRoot) return notFound('стану нема — спершу `flow init`')
90
- const label = sanitizeBranch(branch)
91
- const worktreeDir = worktreePaths(repoRoot, branch).checkout
92
- if (!exists(worktreeDir)) {
93
- return notFound(
94
- `worktree для гілки «${branch}» не знайдено (${worktreeDir}) — перевір назву або зроби \`flow init\``
95
- )
96
- }
97
- return { statePath: flowStatePath(worktreeDir), worktreeDir, label, autoResolved: false, error: null }
98
- }
99
-
100
- // 2. Швидкий шлях без git: `cwd` уже є текою worktree зі станом-sibling
101
- // (звичайний запуск із кореня worktree).
102
- const direct = flowStatePath(cwd)
103
- if (exists(direct)) {
104
- return { statePath: direct, worktreeDir: cwd, label: basename(cwd), autoResolved: false, error: null }
105
- }
106
-
107
- // Далі потрібен корінь репо (git). Якщо недоступний — трактуємо як «стану нема».
108
- const repoRoot = resolveRoot()
109
- if (!repoRoot) return notFound('стану нема — спершу `flow init`')
110
- const worktreesDir = join(repoRoot, '.worktrees')
111
-
112
- // 3. Якщо ми ВСЕРЕДИНІ worktree (toplevel під .worktrees/, у т.ч. з підтеки) —
113
- // беремо стан саме цього worktree. Якщо його нема — це проблема цього worktree
114
- // (`flow init` не зроблено); чужий активний flow НЕ підтягуємо.
115
- const top = currentToplevel(git)
116
- if (top && dirname(top) === worktreesDir) {
117
- const statePath = flowStatePath(top)
118
- if (exists(statePath)) {
119
- return { statePath, worktreeDir: top, label: basename(top), autoResolved: false, error: null }
120
- }
121
- return notFound('стану нема — спершу `flow init`')
122
- }
123
-
124
- // 4. Поза worktree (головне дерево) — скан активних flow.
125
- const active = []
126
- for (const name of readdir(worktreesDir)) {
127
- if (!name.endsWith(FLOW_STATE_SUFFIX)) continue
128
- const statePath = join(worktreesDir, name)
129
- let state
130
- try {
131
- state = readState(statePath)
132
- } catch {
133
- continue // пошкоджений стан — пропускаємо при скануванні
134
- }
135
- if (state?.status === 'in_progress') {
136
- const label = name.slice(0, -FLOW_STATE_SUFFIX.length)
137
- active.push({ statePath, worktreeDir: join(worktreesDir, label), label })
138
- }
139
- }
140
- if (active.length === 1) {
141
- return { ...active[0], autoResolved: true, error: null }
142
- }
143
- if (active.length > 1) {
144
- const list = active.map(a => ` - ${a.label}`).join('\n')
145
- return notFound(`кілька активних flow — уточни \`--branch <гілка>\` або \`cd\` у потрібний worktree:\n${list}`)
146
- }
147
- return notFound('стану нема — спершу `flow init`')
148
- }
149
-
150
- /**
151
- * @param {string} error повідомлення
152
- * @returns {ResolvedFlow} результат без statePath
153
- */
154
- function notFound(error) {
155
- return { statePath: null, worktreeDir: null, label: null, autoResolved: false, error }
156
- }
@@ -1,235 +0,0 @@
1
- /**
2
- * Handlers сигнальних команд `flow done/audit/failed/spawn` (думка.MD).
3
- *
4
- * Агент ніколи не знає свій абсолютний path — команди обчислюють path вузла з
5
- * env var `NCURSOR_NODE_PATH` (встановлюється wrapper-скриптом) або з файлу
6
- * `.n-cursor/current-node` у корені worktree. Якщо нічого — error.
7
- *
8
- * done → делегує `n-cursor graph done <path>`
9
- * audit → створює `pending-audit_NNN.md` → делегує `n-cursor graph audit <path>`
10
- * failed → делегує `n-cursor graph failed <path>`
11
- * spawn → делегує `n-cursor graph spawn <path>`
12
- *
13
- * Всі IO ін'єктуються для тестування без реальних процесів і диска.
14
- */
15
- import { spawnSync } from 'node:child_process'
16
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
17
- import { join } from 'node:path'
18
- import { cwd as processCwd } from 'node:process'
19
-
20
- /**
21
- * Резолвить шлях вузла з env або fallback-файлу.
22
- * @param {{
23
- * env?: Record<string, string | undefined>,
24
- * cwd?: string,
25
- * readFile?: (p: string, enc: string) => string,
26
- * exists?: (p: string) => boolean
27
- * }} deps ін'єкції
28
- * @returns {{ nodePath: string | null, error: string | null }} результат
29
- */
30
- export function resolveNodePath(deps = {}) {
31
- const env = deps.env ?? process.env
32
- const cwd = deps.cwd ?? processCwd()
33
- const exists = deps.exists ?? existsSync
34
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
35
-
36
- // 1. Env var
37
- const fromEnv = env['NCURSOR_NODE_PATH']
38
- if (fromEnv && fromEnv.trim().length > 0) {
39
- return { nodePath: fromEnv.trim(), error: null }
40
- }
41
-
42
- // 2. Fallback-файл .n-cursor/current-node у CWD (корінь worktree)
43
- const fallbackPath = join(cwd, '.n-cursor', 'current-node')
44
- if (exists(fallbackPath)) {
45
- try {
46
- const content = readFile(fallbackPath, 'utf8').trim()
47
- if (content.length > 0) {
48
- return { nodePath: content, error: null }
49
- }
50
- } catch {
51
- // якщо не читається — fallthrough до error
52
- }
53
- }
54
-
55
- return { nodePath: null, error: 'NCURSOR_NODE_PATH not set and .n-cursor/current-node not found' }
56
- }
57
-
58
- /**
59
- * Знаходить поточний найбільший номер `outputs_NNN.md`.
60
- * @param {string} dir директорія вузла
61
- * @param {(dir: string) => string[]} readdir ін'єктована readdir
62
- * @returns {string | null} рядок типу `001` або null якщо не знайдено
63
- */
64
- function findCurrentOutputsNum(dir, readdir) {
65
- const files = readdir(dir)
66
- let max = -1
67
- for (const f of files) {
68
- const m = f.match(/^outputs_(\d+)\.md$/)
69
- if (m) {
70
- const n = parseInt(m[1], 10)
71
- if (n > max) max = n
72
- }
73
- }
74
- return max >= 0 ? String(max).padStart(3, '0') : null
75
- }
76
-
77
- /**
78
- * Виконує n-cursor graph <sub> <nodePath>.
79
- * @param {string} sub підкоманда graph
80
- * @param {string} nodePath шлях вузла
81
- * @param {{
82
- * run: (cmd: string, args: string[]) => { status: number, stdout: string, stderr: string }
83
- * }} deps
84
- * @returns {number} exit code
85
- */
86
- function delegateToGraph(sub, nodePath, deps) {
87
- const result = deps.run('npx', ['@nitra/cursor', 'graph', sub, nodePath])
88
- return result.status ?? 1
89
- }
90
-
91
- /**
92
- * Реальний sync-runner процесу.
93
- * @param {string} cmd
94
- * @param {string[]} args
95
- * @returns {{ status: number, stdout: string, stderr: string }}
96
- */
97
- function realRun(cmd, args) {
98
- const r = spawnSync(cmd, args, { encoding: 'utf8' })
99
- return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' }
100
- }
101
-
102
- /**
103
- * Базовий handler для сигнальних команд без аудиту.
104
- * @param {string} sub підкоманда graph
105
- * @param {{
106
- * cwd?: string,
107
- * env?: Record<string, string | undefined>,
108
- * log?: (m: string) => void,
109
- * run?: (cmd: string, args: string[]) => { status: number, stdout: string, stderr: string },
110
- * readFile?: (p: string, enc: string) => string,
111
- * exists?: (p: string) => boolean
112
- * }} deps ін'єкції
113
- * @returns {Promise<number>} exit code
114
- */
115
- async function signalHandler(sub, deps = {}) {
116
- const cwd = deps.cwd ?? processCwd()
117
- const log = deps.log ?? console.error
118
- const run = deps.run ?? realRun
119
-
120
- const { nodePath, error } = resolveNodePath({ env: deps.env, cwd, readFile: deps.readFile, exists: deps.exists })
121
- if (!nodePath) {
122
- log(`flow ${sub}: ${error}`)
123
- return 1
124
- }
125
-
126
- log(`flow ${sub}: node path = ${nodePath}`)
127
- const code = delegateToGraph(sub, nodePath, { run })
128
- if (code !== 0) {
129
- log(`flow ${sub}: graph ${sub} завершився з кодом ${code}`)
130
- }
131
- return code
132
- }
133
-
134
- /**
135
- * `flow done` — сигналізує успіх → `graph done <path>`.
136
- * @param {string[]} _rest аргументи (не використовуються)
137
- * @param {object} [deps] ін'єкції
138
- * @returns {Promise<number>} exit code
139
- */
140
- export async function done(_rest, deps = {}) {
141
- return signalHandler('done', deps)
142
- }
143
-
144
- /**
145
- * `flow failed` — сигналізує провал → `graph failed <path>`.
146
- * @param {string[]} _rest аргументи (не використовуються)
147
- * @param {object} [deps] ін'єкції
148
- * @returns {Promise<number>} exit code
149
- */
150
- export async function failed(_rest, deps = {}) {
151
- return signalHandler('failed', deps)
152
- }
153
-
154
- /**
155
- * `flow spawn` — сигналізує розклад → `graph spawn <path>`.
156
- * @param {string[]} _rest аргументи (не використовуються)
157
- * @param {object} [deps] ін'єкції
158
- * @returns {Promise<number>} exit code
159
- */
160
- export async function spawn(_rest, deps = {}) {
161
- return signalHandler('spawn', deps)
162
- }
163
-
164
- /**
165
- * `flow audit` — створює `pending-audit_NNN.md` → `graph audit <path>`.
166
- *
167
- * NNN у `pending-audit_NNN.md` = NNN відповідного `outputs_NNN.md`.
168
- * Якщо outputs відсутні — error.
169
- *
170
- * @param {string[]} _rest аргументи (не використовуються)
171
- * @param {{
172
- * cwd?: string,
173
- * env?: Record<string, string | undefined>,
174
- * log?: (m: string) => void,
175
- * run?: (cmd: string, args: string[]) => { status: number, stdout: string, stderr: string },
176
- * readFile?: (p: string, enc: string) => string,
177
- * writeFile?: (p: string, content: string, enc: string) => void,
178
- * readdir?: (dir: string) => string[],
179
- * exists?: (p: string) => boolean,
180
- * now?: () => string
181
- * }} [deps] ін'єкції
182
- * @returns {Promise<number>} exit code
183
- */
184
- export async function audit(_rest, deps = {}) {
185
- const cwd = deps.cwd ?? processCwd()
186
- const log = deps.log ?? console.error
187
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
188
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
189
- const exists = deps.exists ?? existsSync
190
- const nowFn = deps.now ?? (() => new Date().toISOString())
191
- const run = deps.run ?? realRun
192
-
193
- const { nodePath, error } = resolveNodePath({ env: deps.env, cwd, readFile: deps.readFile, exists })
194
- if (!nodePath) {
195
- log(`flow audit: ${error}`)
196
- return 1
197
- }
198
-
199
- // Знаходимо поточний outputs NNN
200
- const outputsNum = findCurrentOutputsNum(cwd, readdir)
201
- if (!outputsNum) {
202
- log('flow audit: outputs_NNN.md не знайдено — спершу напиши outputs')
203
- return 1
204
- }
205
-
206
- const pendingPath = join(cwd, `pending-audit_${outputsNum}.md`)
207
- if (exists(pendingPath)) {
208
- log(`flow audit: ${pendingPath} вже існує — audit вже запитано для outputs_${outputsNum}.md`)
209
- return 1
210
- }
211
-
212
- const content = [
213
- '---',
214
- `created_at: ${nowFn()}`,
215
- `outputs_ref: outputs_${outputsNum}.md`,
216
- `actor: agent`,
217
- '---',
218
- ''
219
- ].join('\n')
220
-
221
- try {
222
- writeFile(pendingPath, content, 'utf8')
223
- } catch (err) {
224
- log(`flow audit: не вдалося записати ${pendingPath} — ${err instanceof Error ? err.message : String(err)}`)
225
- return 1
226
- }
227
-
228
- log(`flow audit: ${pendingPath} створено`)
229
- log(`flow audit: node path = ${nodePath}`)
230
- const code = delegateToGraph('audit', nodePath, { run })
231
- if (code !== 0) {
232
- log(`flow audit: graph audit завершився з кодом ${code}`)
233
- }
234
- return code
235
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * Handler `flow verify` — Stage 2 structural check (думка.MD § "flow verify").
3
- *
4
- * Перевіряє що `outputs_NNN.md` існує і непорожній у директорії поточного вузла
5
- * (CWD). Якщо так — виводить `## Done when` секцію з `task.md` та вміст
6
- * `outputs_NNN.md` на stdout для агентської self-evaluation.
7
- *
8
- * exit 0 = структурно OK
9
- * exit 1 = структурна помилка (outputs відсутній або порожній)
10
- *
11
- * НІЯКОГО артефакту не пишеться. FS ін'єктується для тестування без диска.
12
- */
13
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
14
- import { join } from 'node:path'
15
- import { cwd as processCwd } from 'node:process'
16
-
17
- const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
18
- const SECTION_RE = /^## (.+)$/m
19
-
20
- /**
21
- * Читає секцію за заголовком із markdown-файлу.
22
- * Повертає вміст від заголовка до наступного `## ` або кінця файлу.
23
- * @param {string} text вміст файлу
24
- * @param {string} heading заголовок без `## `
25
- * @returns {string | null} вміст секції (включно з рядком заголовка) або null
26
- */
27
- function extractSection(text, heading) {
28
- const lines = text.split(/\r?\n/)
29
- const start = lines.findIndex(l => l === `## ${heading}`)
30
- if (start === -1) return null
31
- const end = lines.findIndex((l, i) => i > start && SECTION_RE.test(l))
32
- const section = end === -1 ? lines.slice(start) : lines.slice(start, end)
33
- return section.join('\n').trimEnd()
34
- }
35
-
36
- /**
37
- * Знаходить outputs-файл з найбільшим NNN у директорії вузла.
38
- * @param {string[]} files список файлів директорії
39
- * @returns {string | null} ім'я файлу (напр. `outputs_001.md`) або null
40
- */
41
- export function findLatestOutputs(files) {
42
- let max = -1
43
- let best = null
44
- for (const f of files) {
45
- const m = f.match(/^outputs_(\d+)\.md$/)
46
- if (m) {
47
- const n = parseInt(m[1], 10)
48
- if (n > max) {
49
- max = n
50
- best = f
51
- }
52
- }
53
- }
54
- return best
55
- }
56
-
57
- /**
58
- * `flow verify` handler.
59
- *
60
- * @param {string[]} _rest аргументи після `verify` (не використовуються)
61
- * @param {{
62
- * cwd?: string,
63
- * log?: (m: string) => void,
64
- * readFile?: (path: string, enc: string) => string,
65
- * readdir?: (dir: string) => string[],
66
- * exists?: (path: string) => boolean
67
- * }} [deps] ін'єкції
68
- * @returns {Promise<number>} exit code (0=OK, 1=структурна помилка)
69
- */
70
- export async function verify(_rest, deps = {}) {
71
- const cwd = deps.cwd ?? processCwd()
72
- const log = deps.log ?? console.error
73
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
74
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
75
- const exists = deps.exists ?? existsSync
76
-
77
- // Перевіряємо outputs_NNN.md
78
- const files = readdir(cwd)
79
- const outputsName = findLatestOutputs(files)
80
- if (!outputsName) {
81
- log('flow verify: outputs_NNN.md не знайдено — структурна помилка')
82
- return 1
83
- }
84
-
85
- const outputsPath = join(cwd, outputsName)
86
- if (!exists(outputsPath)) {
87
- log(`flow verify: ${outputsName} не існує — структурна помилка`)
88
- return 1
89
- }
90
-
91
- let outputsContent
92
- try {
93
- outputsContent = readFile(outputsPath, 'utf8')
94
- } catch (err) {
95
- log(`flow verify: не вдалося прочитати ${outputsName} — ${err instanceof Error ? err.message : String(err)}`)
96
- return 1
97
- }
98
-
99
- // Перевіряємо що файл не порожній (без front-matter — суто тіло)
100
- const withoutFm = outputsContent.replace(FRONT_MATTER_RE, '').trim()
101
- if (withoutFm.length === 0) {
102
- log(`flow verify: ${outputsName} порожній — структурна помилка`)
103
- return 1
104
- }
105
-
106
- // Виводимо Done when + outputs на stdout для агентської self-evaluation
107
- const outLines = [`## verify context`, ``]
108
-
109
- const taskPath = join(cwd, 'task.md')
110
- if (exists(taskPath)) {
111
- try {
112
- const taskContent = readFile(taskPath, 'utf8')
113
- const doneWhen = extractSection(taskContent, 'Done when')
114
- if (doneWhen) {
115
- outLines.push(doneWhen, '')
116
- }
117
- } catch {
118
- // якщо task.md недоступний — не блокуємо verify
119
- }
120
- }
121
-
122
- outLines.push(`### ${outputsName}`, ``, outputsContent.trimEnd())
123
-
124
- console.log(outLines.join('\n'))
125
-
126
- return 0
127
- }