@nitra/cursor 3.10.0 → 3.12.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/CHANGELOG.md +18 -0
- package/bin/n-cursor.js +3 -2
- package/package.json +1 -1
- package/rules/changelog/changelog.mdc +7 -1
- package/rules/flow/flow.mdc +6 -3
- package/rules/js-lint/coverage/coverage.mjs +89 -22
- package/rules/npm-module/js/package_structure.mjs +5 -147
- package/rules/npm-module/npm-module.mdc +4 -14
- package/rules/rust/coverage/coverage.mjs +13 -2
- package/rules/test/coverage/coverage.mjs +50 -6
- package/rules/test/test.mdc +2 -0
- package/scripts/dispatcher/index.mjs +36 -3
- package/scripts/dispatcher/lib/commands.mjs +100 -8
- package/scripts/dispatcher/lib/flow-resolve.mjs +154 -0
- package/scripts/dispatcher/lib/gate.mjs +11 -3
- package/scripts/dispatcher/lib/plan.mjs +11 -3
- package/scripts/dispatcher/lib/review.mjs +11 -3
- package/scripts/dispatcher/lib/reviewer.mjs +9 -5
- package/scripts/dispatcher/lib/spec.mjs +11 -3
- package/scripts/dispatcher/trace.mjs +50 -11
- package/scripts/lib/changed-files.mjs +28 -0
|
@@ -7,12 +7,20 @@
|
|
|
7
7
|
* FS-доступ (`readdir`/`readFile`/`exists`) ін'єктується — тестується без диска.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
10
|
-
import { join } from 'node:path'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
11
|
import { cwd as processCwd } from 'node:process'
|
|
12
12
|
|
|
13
|
-
/** Поля-лінки у front-matter
|
|
13
|
+
/** Поля-лінки у front-matter (порядок відображення). */
|
|
14
14
|
const LINK_FIELDS = ['adr', 'spec', 'plan', 'flow', 'change', 'task']
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Інформаційні лінк-поля: показуються, але їх відсутність НЕ є розривом ланцюга.
|
|
18
|
+
* `flow` вказує на runtime-стан `.worktrees/<branch>.flow.json` — gitignored, поза
|
|
19
|
+
* `docs/`, існує лише під час задачі; у чистому checkout/CI його нема ніколи, тож
|
|
20
|
+
* рахувати його розривом — хибний сигнал. Решта полів — ланки ланцюга (breaking).
|
|
21
|
+
*/
|
|
22
|
+
const INFO_LINK_FIELDS = new Set(['flow'])
|
|
23
|
+
|
|
16
24
|
/** Каталоги з traceable-артефактами. */
|
|
17
25
|
const DIRS = ['docs/tasks', 'docs/specs', 'docs/plans', 'docs/adr']
|
|
18
26
|
|
|
@@ -52,20 +60,40 @@ function isSimpleKey(key) {
|
|
|
52
60
|
|
|
53
61
|
/**
|
|
54
62
|
* Будує аналіз: для кожного артефакту — його лінки зі статусом ok/розрив.
|
|
63
|
+
* `breaking` — чи відсутність цього лінка рве ланцюг (chain-поля) чи лише
|
|
64
|
+
* інформаційна (`flow` → runtime-стан).
|
|
55
65
|
* @param {{ file: string, fm: Record<string, string | null> }[]} artifacts артефакти з front-matter
|
|
56
|
-
* @param {(target: string) => boolean}
|
|
57
|
-
* @returns {{ file: string, kind: string | null, id: string | null, status: string | null, links: { field: string, target: string, ok: boolean }[] }[]} аналіз
|
|
66
|
+
* @param {(target: string, artifactFile: string) => boolean} resolve чи резолвиться лінк (відносно артефакту/кореня)
|
|
67
|
+
* @returns {{ file: string, kind: string | null, id: string | null, status: string | null, links: { field: string, target: string, ok: boolean, breaking: boolean }[] }[]} аналіз
|
|
58
68
|
*/
|
|
59
|
-
export function analyze(artifacts,
|
|
69
|
+
export function analyze(artifacts, resolve) {
|
|
60
70
|
return artifacts.map(({ file, fm }) => ({
|
|
61
71
|
file,
|
|
62
72
|
kind: fm.kind ?? null,
|
|
63
73
|
id: fm.id ?? null,
|
|
64
74
|
status: fm.status ?? null,
|
|
65
|
-
links: LINK_FIELDS.filter(f => fm[f]).map(f => ({
|
|
75
|
+
links: LINK_FIELDS.filter(f => fm[f]).map(f => ({
|
|
76
|
+
field: f,
|
|
77
|
+
target: fm[f],
|
|
78
|
+
ok: resolve(fm[f], file),
|
|
79
|
+
breaking: !INFO_LINK_FIELDS.has(f)
|
|
80
|
+
}))
|
|
66
81
|
}))
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Чи резолвиться лінк: спершу відносно теки артефакту (конвенція доків `../specs/…`),
|
|
86
|
+
* далі fallback на root-relative (`docs/specs/…`). Обидві форми вважаються валідними.
|
|
87
|
+
* @param {string} root корінь репо
|
|
88
|
+
* @param {string} artifactFile rel-шлях артефакту (напр. `docs/plans/x.md`)
|
|
89
|
+
* @param {string} target значення лінка
|
|
90
|
+
* @param {(absPath: string) => boolean} exists перевірка існування
|
|
91
|
+
* @returns {boolean} чи знайдено цільовий файл
|
|
92
|
+
*/
|
|
93
|
+
function resolveLink(root, artifactFile, target, exists) {
|
|
94
|
+
return exists(join(root, dirname(artifactFile), target)) || exists(join(root, target))
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
/**
|
|
70
98
|
* Текстовий рендер аналізу.
|
|
71
99
|
* @param {object[]} analysis результат `analyze`
|
|
@@ -77,14 +105,24 @@ export function render(analysis) {
|
|
|
77
105
|
for (const a of analysis) {
|
|
78
106
|
lines.push(`${a.kind ?? '?'} · ${a.id ?? a.file} [${a.status ?? '—'}]`)
|
|
79
107
|
for (const l of a.links) {
|
|
80
|
-
|
|
81
|
-
const note = l.ok ? '' : ' (РОЗРИВ — файл відсутній)'
|
|
82
|
-
lines.push(` ${mark} ${l.field}: ${l.target}${note}`)
|
|
108
|
+
lines.push(` ${renderLink(l)}`)
|
|
83
109
|
}
|
|
84
110
|
}
|
|
85
111
|
return lines.join('\n')
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Рядок одного лінка: `→` ok; `✗ … (РОЗРИВ)` — нерезолвлене chain-поле;
|
|
116
|
+
* `~ … (не рве ланцюг)` — нерезолвлене інформаційне поле (`flow`, runtime-стан).
|
|
117
|
+
* @param {{ field: string, target: string, ok: boolean, breaking: boolean }} l лінк
|
|
118
|
+
* @returns {string} рядок
|
|
119
|
+
*/
|
|
120
|
+
function renderLink(l) {
|
|
121
|
+
if (l.ok) return `→ ${l.field}: ${l.target}`
|
|
122
|
+
if (l.breaking) return `✗ ${l.field}: ${l.target} (РОЗРИВ — файл відсутній)`
|
|
123
|
+
return `~ ${l.field}: ${l.target} (runtime-стан — не рве ланцюг)`
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
/**
|
|
89
127
|
* CLI `n-cursor trace [--json]`. Повертає 1, якщо є розриви ланцюга.
|
|
90
128
|
* @param {string[]} args аргументи
|
|
@@ -108,7 +146,8 @@ export function runTraceCli(args, deps = {}) {
|
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
148
|
|
|
111
|
-
const analysis = analyze(artifacts, target =>
|
|
149
|
+
const analysis = analyze(artifacts, (target, file) => resolveLink(root, file, target, exists))
|
|
112
150
|
log(args.includes('--json') ? JSON.stringify(analysis, null, 2) : render(analysis))
|
|
113
|
-
|
|
151
|
+
// Розрив ланцюга — лише нерезолвлене chain-поле; нерезолвлений `flow` (runtime-стан) не рахуємо.
|
|
152
|
+
return analysis.some(a => a.links.some(l => l.breaking && !l.ok)) ? 1 : 0
|
|
114
153
|
}
|
|
@@ -28,3 +28,31 @@ export function collectChangedFiles(cwd = process.cwd()) {
|
|
|
28
28
|
const untracked = gitLines(['ls-files', '--others', '--exclude-standard'], cwd)
|
|
29
29
|
return [...new Set([...modified, ...untracked])]
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Список змінених + untracked файлів **відносно базового комміту**.
|
|
34
|
+
*
|
|
35
|
+
* `git diff <base>` (без `..`/`...`, без `HEAD`) порівнює base-комміт із поточним
|
|
36
|
+
* **робочим деревом** — тобто однаково ловить і закомічене від base, і staged, і
|
|
37
|
+
* незакомічені модифікації. Це гарантує однакову поведінку незалежно від того, чи
|
|
38
|
+
* зміни вже закомічені у worktree (потрібно для flow-турнікета, де executor комітить
|
|
39
|
+
* кожен крок). Без `base` — fallback на `collectChangedFiles` (робоче дерево vs HEAD).
|
|
40
|
+
* @param {string|null} [base] базовий комміт (`metadata.base_commit` зі стану flow)
|
|
41
|
+
* @param {string} [cwd] корінь репо
|
|
42
|
+
* @returns {string[]} унікальні шляхи (без видалених)
|
|
43
|
+
*/
|
|
44
|
+
export function collectChangedFilesSince(base, cwd = process.cwd()) {
|
|
45
|
+
if (!base) return collectChangedFiles(cwd)
|
|
46
|
+
// Fail-closed: недосяжний base (rebase/force-update/shallow prune) інакше дав би `git diff`
|
|
47
|
+
// exit 128 → порожній список → gate мовчки пройшов би без перевірки. Краще явна помилка.
|
|
48
|
+
const verify = spawnSync('git', ['rev-parse', '--verify', '--quiet', `${base}^{commit}`], { cwd, encoding: 'utf8' })
|
|
49
|
+
if (verify.status !== 0 || verify.error) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`collectChangedFilesSince: base-комміт «${base}» недосяжний у ${cwd} ` +
|
|
52
|
+
'(rebase/force-update?) — coverage --changed не може визначити scope'
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
const changed = gitLines(['diff', base, '--name-only', '--diff-filter=ACMR'], cwd)
|
|
56
|
+
const untracked = gitLines(['ls-files', '--others', '--exclude-standard'], cwd)
|
|
57
|
+
return [...new Set([...changed, ...untracked])]
|
|
58
|
+
}
|