@nitra/cursor 3.14.2 → 3.15.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.15.0] - 2026-06-02
4
+
5
+ ### Added
6
+
7
+ - parity-гард дзеркала правил: тест перевіряє, що .cursor/rules/n-<id>.mdc == канонічний npm/rules/<id>/<id>.mdc з inlined-шаблонами (хелпер mirror-parity.mjs), ловлячи дрейф рано. Разово регенеровано наявний дрейф (changelog/flow/ga/npm-module/test).
8
+
3
9
  ## [3.14.2] - 2026-06-02
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.14.2",
3
+ "version": "3.15.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Parity дзеркала правил: `.cursor/rules/n-<id>.mdc` має дорівнювати канонічному
3
+ * `npm/rules/<id>/<id>.mdc` з inlined-шаблонами — тим самим трансформом, що його
4
+ * застосовує синк (`readBundledRuleContent` → `inlineTemplateLinks`). Дрейф виникає,
5
+ * коли канонічний `.mdc` змінюють, не регенерувавши дзеркало (беклог адаптації flow #10).
6
+ *
7
+ * Використовується і тестом-гардом (drift === []), і разовою регенерацією.
8
+ */
9
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
10
+ import { dirname, join } from 'node:path'
11
+
12
+ import { inlineTemplateLinks } from './inline-template-links.mjs'
13
+
14
+ const MIRROR_PREFIX = 'n-'
15
+ const MDC_EXT = '.mdc'
16
+
17
+ /**
18
+ * Керовані дзеркала `.cursor/rules/n-<id>.mdc`, що мають канонічне джерело
19
+ * `npm/rules/<id>/<id>.mdc`. Дзеркала без канону (зовнішні) пропускаються.
20
+ * @param {string} repoRoot корінь репо
21
+ * @returns {{ id: string, mirrorPath: string, canonicalPath: string }[]} список
22
+ */
23
+ export function listManagedMirrors(repoRoot) {
24
+ const rulesDir = join(repoRoot, '.cursor/rules')
25
+ if (!existsSync(rulesDir)) return []
26
+ return readdirSync(rulesDir)
27
+ .filter(f => f.startsWith(MIRROR_PREFIX) && f.endsWith(MDC_EXT))
28
+ .map(f => {
29
+ const id = f.slice(MIRROR_PREFIX.length, -MDC_EXT.length)
30
+ return {
31
+ id,
32
+ mirrorPath: join(rulesDir, f),
33
+ canonicalPath: join(repoRoot, 'npm/rules', id, `${id}${MDC_EXT}`)
34
+ }
35
+ })
36
+ .filter(m => existsSync(m.canonicalPath))
37
+ }
38
+
39
+ /**
40
+ * Очікуваний вміст дзеркала = канон з inlined-шаблонами (трансформ синку).
41
+ * @param {string} canonicalPath абсолютний шлях `npm/rules/<id>/<id>.mdc`
42
+ * @returns {Promise<string>} очікуваний текст дзеркала
43
+ */
44
+ export function expectedMirrorContent(canonicalPath) {
45
+ return inlineTemplateLinks(readFileSync(canonicalPath, 'utf8'), dirname(canonicalPath))
46
+ }
47
+
48
+ /**
49
+ * Id дзеркал, що розійшлися з каноном (actual ≠ expected).
50
+ * @param {string} repoRoot корінь репо
51
+ * @returns {Promise<string[]>} відсортовані id дрейфу
52
+ */
53
+ export async function findMirrorDrift(repoRoot) {
54
+ const drift = []
55
+ for (const m of listManagedMirrors(repoRoot)) {
56
+ const expected = await expectedMirrorContent(m.canonicalPath)
57
+ if (readFileSync(m.mirrorPath, 'utf8') !== expected) drift.push(m.id)
58
+ }
59
+ return drift.sort()
60
+ }