@nitra/cursor 1.8.103 → 1.8.104

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.103",
3
+ "version": "1.8.104",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -45,6 +45,11 @@
45
45
  * **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
46
46
  * на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
47
47
  *
48
+ * **Мішень patch:** у **`patches[].target`** і **`patchesJson6902[].target`** (без **labelSelector** / **annotationSelector**)
49
+ * має існувати відповідний ресурс у зібраному з **`resources`**, **`bases`**, **`components`**, **`crds`** каталозі (рекурсивно для підкаталогів з **`kustomization.yaml`**).
50
+ * Для **`patchesStrategicMerge`** і для **`patches[].path`** без **`target`** і без inline **`patch`** (зовнішній strategic-merge)
51
+ * кожен YAML-документ з кореневим **`kind`** і **`metadata.name`** також звіряється з цим каталогом.
52
+ *
48
53
  * Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
49
54
  * **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
50
55
  * компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
@@ -471,6 +476,533 @@ export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
471
476
  return managed
472
477
  }
473
478
 
479
+ /**
480
+ * Шляхи лише з полів ресурсів Kustomization (**без** patch-файлів).
481
+ * @param {unknown} obj корінь першого документа Kustomization
482
+ * @returns {string[]} відносні посилання
483
+ */
484
+ function resourcePathRefsFromKustomizationObject(obj) {
485
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
486
+ const rec = /** @type {Record<string, unknown>} */ (obj)
487
+ /** @type {string[]} */
488
+ const out = []
489
+ pushStringPaths(rec.resources, out)
490
+ pushStringPaths(rec.bases, out)
491
+ pushStringPaths(rec.components, out)
492
+ pushStringPaths(rec.crds, out)
493
+ return out
494
+ }
495
+
496
+ /**
497
+ * Дескриптор ресурсу для звірки з **`target`** Kustomize / strategic-merge фрагментом.
498
+ * @typedef {{ group: string, version: string, kind: string, name: string, namespace: string }} KustomizeResourceDescriptor
499
+ */
500
+
501
+ /**
502
+ * Розбиває **`apiVersion`** Kubernetes на **group** і **version**.
503
+ * @param {unknown} apiVersion значення з YAML
504
+ * @returns {{ group: string, version: string }} для `group/version` — два сегменти; для `v1` — core (**group** порожній).
505
+ */
506
+ export function splitK8sApiVersion(apiVersion) {
507
+ if (typeof apiVersion !== 'string') {
508
+ return { group: '', version: '' }
509
+ }
510
+ const t = apiVersion.trim()
511
+ if (t === '') {
512
+ return { group: '', version: '' }
513
+ }
514
+ const i = t.indexOf('/')
515
+ if (i === -1) {
516
+ return { group: '', version: t }
517
+ }
518
+ return { group: t.slice(0, i), version: t.slice(i + 1) }
519
+ }
520
+
521
+ /**
522
+ * Чи patch-**target** використовує **labelSelector** / **annotationSelector** (тоді статична перевірка за іменем не застосовується).
523
+ * @param {Record<string, unknown>} t об’єкт **target**
524
+ * @returns {boolean} true, якщо є непорожній селектор
525
+ */
526
+ function patchTargetUsesSelector(t) {
527
+ const ls = t.labelSelector
528
+ if (
529
+ ls !== undefined &&
530
+ ls !== null &&
531
+ ls !== '' &&
532
+ ((typeof ls === 'object' && !Array.isArray(ls) && Object.keys(ls).length > 0) ||
533
+ (typeof ls === 'string' && ls.trim() !== ''))
534
+ ) {
535
+ return true
536
+ }
537
+ const asel = t.annotationSelector
538
+ if (
539
+ asel !== undefined &&
540
+ asel !== null &&
541
+ asel !== '' &&
542
+ ((typeof asel === 'object' && !Array.isArray(asel) && Object.keys(asel).length > 0) ||
543
+ (typeof asel === 'string' && asel.trim() !== ''))
544
+ ) {
545
+ return true
546
+ }
547
+ return false
548
+ }
549
+
550
+ /**
551
+ * Чи варто перевіряти **target** на наявність ресурсу в каталозі (є **kind** і **name**, немає селекторів).
552
+ * @param {unknown} target значення **patches[].target**
553
+ * @returns {boolean} true, якщо перевірка доречна
554
+ */
555
+ export function shouldValidateKustomizePatchTarget(target) {
556
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
557
+ return false
558
+ }
559
+ const t = /** @type {Record<string, unknown>} */ (target)
560
+ const kind = t.kind
561
+ const name = t.name
562
+ if (typeof kind !== 'string' || kind.trim() === '' || typeof name !== 'string' || name.trim() === '') {
563
+ return false
564
+ }
565
+ return !patchTargetUsesSelector(t)
566
+ }
567
+
568
+ /**
569
+ * Чи **target** Kustomize відповідає дескриптору ресурсу (узгоджено з правилами відбору Kustomize: пропущені поля **target** не звужують).
570
+ * @param {unknown} target об’єкт **target**
571
+ * @param {KustomizeResourceDescriptor} res дескриптор з інвентарю
572
+ * @returns {boolean} true, якщо збігається
573
+ */
574
+ export function kustomizePatchTargetMatchesDescriptor(target, res) {
575
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
576
+ return false
577
+ }
578
+ const rec = /** @type {Record<string, unknown>} */ (target)
579
+ const tk = rec.kind
580
+ const tn = rec.name
581
+ if (typeof tk !== 'string' || typeof tn !== 'string') {
582
+ return false
583
+ }
584
+ if (tk.trim() !== res.kind || tn.trim() !== res.name) {
585
+ return false
586
+ }
587
+ const tgtGroup = rec.group
588
+ if (typeof tgtGroup === 'string' && tgtGroup.trim() !== '' && res.group !== tgtGroup.trim()) {
589
+ return false
590
+ }
591
+ const tgtVersion = rec.version
592
+ if (typeof tgtVersion === 'string' && tgtVersion.trim() !== '' && res.version !== tgtVersion.trim()) {
593
+ return false
594
+ }
595
+ const tgtNs = rec.namespace
596
+ if (typeof tgtNs === 'string' && tgtNs.trim() !== '' && res.namespace !== tgtNs.trim()) {
597
+ return false
598
+ }
599
+ return true
600
+ }
601
+
602
+ /**
603
+ * Чи є в каталозі ресурс, який задовольняє **target**.
604
+ * @param {KustomizeResourceDescriptor[]} catalog зібрані дескриптори
605
+ * @param {unknown} target об’єкт **target**
606
+ * @returns {boolean} true, якщо перевірка не потрібна або знайдено збіг
607
+ */
608
+ export function kustomizeResourceCatalogMatchesPatchTarget(catalog, target) {
609
+ if (!shouldValidateKustomizePatchTarget(target)) {
610
+ return true
611
+ }
612
+ return catalog.some(res => kustomizePatchTargetMatchesDescriptor(target, res))
613
+ }
614
+
615
+ /**
616
+ * Чи два дескриптори повністю збігаються (для strategic-merge фрагмента).
617
+ * @param {KustomizeResourceDescriptor} a перший
618
+ * @param {KustomizeResourceDescriptor} b другий
619
+ * @returns {boolean} true, якщо всі поля однакові
620
+ */
621
+ export function kustomizeResourceDescriptorsIdentityEqual(a, b) {
622
+ return (
623
+ a.group === b.group &&
624
+ a.version === b.version &&
625
+ a.kind === b.kind &&
626
+ a.name === b.name &&
627
+ a.namespace === b.namespace
628
+ )
629
+ }
630
+
631
+ /**
632
+ * Будує дескриптор з маніфесту (пропускає **Kustomization** та об’єкти без **metadata.name**).
633
+ * @param {Record<string, unknown>} obj корінь документа
634
+ * @param {string} kustomizationDefaultNs значення **`namespace:`** з kustomization, що підключив файл
635
+ * @returns {KustomizeResourceDescriptor | null} дескриптор для звірки або **null**, якщо документ не підходить.
636
+ */
637
+ export function kustomizeResourceDescriptorFromManifest(obj, kustomizationDefaultNs) {
638
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
639
+ return null
640
+ }
641
+ const kindRaw = obj.kind
642
+ if (typeof kindRaw !== 'string' || kindRaw.trim() === '') {
643
+ return null
644
+ }
645
+ const kind = kindRaw.trim()
646
+ if (kind === 'Kustomization') {
647
+ return null
648
+ }
649
+ const meta = obj.metadata
650
+ let name = ''
651
+ if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
652
+ const m = /** @type {Record<string, unknown>} */ (meta)
653
+ const n = m.name
654
+ if (typeof n === 'string' && n.trim() !== '') {
655
+ name = n.trim()
656
+ }
657
+ }
658
+ if (name === '') {
659
+ return null
660
+ }
661
+ const { group, version } = splitK8sApiVersion(obj.apiVersion)
662
+ let namespace = ''
663
+ if (!isClusterScopedKubernetesKind(kind)) {
664
+ let metaNs = ''
665
+ if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
666
+ const m = /** @type {Record<string, unknown>} */ (meta)
667
+ const ns = m.namespace
668
+ if (typeof ns === 'string' && ns.trim() !== '') {
669
+ metaNs = ns.trim()
670
+ }
671
+ }
672
+ const def =
673
+ typeof kustomizationDefaultNs === 'string' && kustomizationDefaultNs.trim() !== ''
674
+ ? kustomizationDefaultNs.trim()
675
+ : ''
676
+ namespace = metaNs || def
677
+ }
678
+ return { group, version, kind, name, namespace }
679
+ }
680
+
681
+ /**
682
+ * Читає k8s YAML і повертає корені документів-об’єктів (після modeline, якщо він є).
683
+ * @param {string} abs абсолютний шлях до файлу
684
+ * @returns {Promise<Record<string, unknown>[]>} масив коренів-об’єктів YAML-документів (без масивів на корені).
685
+ */
686
+ async function readK8sYamlDocumentRootsForInventory(abs) {
687
+ let raw
688
+ try {
689
+ raw = await readFile(abs, 'utf8')
690
+ } catch {
691
+ return []
692
+ }
693
+ const lines = toLines(raw)
694
+ const body =
695
+ lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
696
+ /** @type {unknown[]} */
697
+ const roots = parseK8sYamlDocumentObjectRoots(body)
698
+ /** @type {Record<string, unknown>[]} */
699
+ const out = []
700
+ for (const r of roots) {
701
+ if (r !== null && typeof r === 'object' && !Array.isArray(r)) {
702
+ out.push(/** @type {Record<string, unknown>} */ (r))
703
+ }
704
+ }
705
+ return out
706
+ }
707
+
708
+ /**
709
+ * Збирає дескриптори ресурсів з **`resources` / `bases` / `components` / `crds`** для одного дерева kustomization.
710
+ * Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок (як у **`collectKustomizeManagedRelPaths`**).
711
+ * @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
712
+ * @param {string} rootNorm нормалізований абсолютний корінь репозиторію
713
+ * @param {Set<string>} visitedKustomization нормалізовані абсолютні шляхи відвіданих **kustomization.yaml**
714
+ * @returns {Promise<KustomizeResourceDescriptor[]>} плоский список дескрипторів із дерева **resources** / **bases** / **components** / **crds**.
715
+ */
716
+ export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visitedKustomization) {
717
+ const normKust = resolve(kustAbs)
718
+ if (visitedKustomization.has(normKust)) {
719
+ return []
720
+ }
721
+ visitedKustomization.add(normKust)
722
+
723
+ let raw
724
+ try {
725
+ raw = await readFile(normKust, 'utf8')
726
+ } catch {
727
+ return []
728
+ }
729
+ const lines = toLines(raw)
730
+ const body =
731
+ lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
732
+
733
+ /** @type {import('yaml').Document[] | undefined} */
734
+ let docs
735
+ try {
736
+ docs = parseAllDocuments(body)
737
+ } catch {
738
+ return []
739
+ }
740
+ const first = docs[0]?.toJSON()
741
+ if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) {
742
+ return []
743
+ }
744
+ const rec = /** @type {Record<string, unknown>} */ (first)
745
+ const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
746
+ const kustDir = dirname(normKust)
747
+ const pathRefs = resourcePathRefsFromKustomizationObject(first)
748
+
749
+ /** @type {KustomizeResourceDescriptor[]} */
750
+ const out = []
751
+
752
+ for (const ref of pathRefs) {
753
+ if (typeof ref === 'string' && !ref.includes('://')) {
754
+ const resolved = resolve(kustDir, ref)
755
+ if (resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
756
+ /** @type {import('node:fs').Stats | undefined} */
757
+ let st
758
+ try {
759
+ st = await stat(resolved)
760
+ } catch {
761
+ st = undefined
762
+ }
763
+ if (st !== undefined) {
764
+ if (st.isFile() && /\.ya?ml$/iu.test(resolved)) {
765
+ const roots = await readK8sYamlDocumentRootsForInventory(resolved)
766
+ for (const o of roots) {
767
+ const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
768
+ if (d !== null) {
769
+ out.push(d)
770
+ }
771
+ }
772
+ } else if (st.isDirectory()) {
773
+ const childK = existsSync(join(resolved, 'kustomization.yaml'))
774
+ ? join(resolved, 'kustomization.yaml')
775
+ : null
776
+ if (childK !== null) {
777
+ const sub = await collectResourceDescriptorsForKustomizationWalk(
778
+ childK,
779
+ rootNorm,
780
+ visitedKustomization
781
+ )
782
+ out.push(...sub)
783
+ }
784
+ }
785
+ }
786
+ }
787
+ }
788
+ }
789
+
790
+ return out
791
+ }
792
+
793
+ /**
794
+ * Витягує записи з явним **target** з **patches** / **patchesJson6902**.
795
+ * @param {unknown} obj перший документ Kustomization
796
+ * @returns {Array<{ section: string, index: number, target: unknown }>} пари **section** + індекс (1-based) і **target** з YAML.
797
+ */
798
+ function extractExplicitPatchTargetsFromKustomization(obj) {
799
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
800
+ return []
801
+ }
802
+ const rec = /** @type {Record<string, unknown>} */ (obj)
803
+ /** @type {Array<{ section: string, index: number, target: unknown }>} */
804
+ const out = []
805
+ /**
806
+ * @param {string} section ім’я поля
807
+ * @param {unknown} arr масив з YAML
808
+ * @returns {void}
809
+ */
810
+ const push = (section, arr) => {
811
+ if (!Array.isArray(arr)) {
812
+ return
813
+ }
814
+ let i = 0
815
+ for (const item of arr) {
816
+ i++
817
+ if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
818
+ const it = /** @type {Record<string, unknown>} */ (item)
819
+ if ('target' in it) {
820
+ out.push({ section, index: i, target: it.target })
821
+ }
822
+ }
823
+ }
824
+ }
825
+ push('patches', rec.patches)
826
+ push('patchesJson6902', rec.patchesJson6902)
827
+ return out
828
+ }
829
+
830
+ /**
831
+ * Людинозчитуваний опис **target** для повідомлення про помилку.
832
+ * @param {unknown} target об’єкт **target**
833
+ * @returns {string} короткий рядок
834
+ */
835
+ function formatKustomizePatchTargetForMessage(target) {
836
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
837
+ return String(target)
838
+ }
839
+ const t = /** @type {Record<string, unknown>} */ (target)
840
+ const parts = []
841
+ const g = t.group
842
+ const v = t.version
843
+ const k = t.kind
844
+ const n = t.name
845
+ const ns = t.namespace
846
+ if (typeof g === 'string' && g.trim() !== '') {
847
+ parts.push(`group=${g.trim()}`)
848
+ }
849
+ if (typeof v === 'string' && v.trim() !== '') {
850
+ parts.push(`version=${v.trim()}`)
851
+ }
852
+ if (typeof k === 'string' && k.trim() !== '') {
853
+ parts.push(`kind=${k.trim()}`)
854
+ }
855
+ if (typeof n === 'string' && n.trim() !== '') {
856
+ parts.push(`name=${n.trim()}`)
857
+ }
858
+ if (typeof ns === 'string' && ns.trim() !== '') {
859
+ parts.push(`namespace=${ns.trim()}`)
860
+ }
861
+ return parts.length > 0 ? parts.join(', ') : JSON.stringify(t)
862
+ }
863
+
864
+ /**
865
+ * Перевіряє всі **`kustomization.yaml`** під **`k8s`**: **target** patch і strategic-merge посилання не вказують на ресурс поза інвентарем **resources** / **bases** / **components** / **crds**.
866
+ * @param {string} root корінь репозиторію
867
+ * @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
868
+ * @param {(msg: string) => void} fail реєстрація помилки
869
+ * @returns {Promise<void>}
870
+ */
871
+ async function validateKustomizationPatchTargetsResolved(root, yamlFilesAbs, fail) {
872
+ const rootNorm = resolve(root)
873
+ for (const kustAbs of yamlFilesAbs) {
874
+ if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
875
+ const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
876
+ /** @type {string | undefined} */
877
+ let raw
878
+ let readOk = false
879
+ try {
880
+ raw = await readFile(kustAbs, 'utf8')
881
+ readOk = true
882
+ } catch (error) {
883
+ const msg = error instanceof Error ? error.message : String(error)
884
+ fail(`${rel}: не вдалося прочитати для перевірки patch target (${msg})`)
885
+ }
886
+ if (readOk && raw !== undefined) {
887
+ const lines = toLines(raw)
888
+ const body =
889
+ lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
890
+ /** @type {import('yaml').Document[] | null} */
891
+ let docs = null
892
+ try {
893
+ docs = parseAllDocuments(body)
894
+ } catch {
895
+ fail(`${rel}: не вдалося розпарсити YAML для перевірки patch target`)
896
+ }
897
+ if (docs !== null) {
898
+ const first = docs[0]?.toJSON()
899
+ if (first !== null && first !== undefined && typeof first === 'object' && !Array.isArray(first)) {
900
+ const rec = /** @type {Record<string, unknown>} */ (first)
901
+ if (rec.kind === 'Kustomization') {
902
+ const visited = new Set()
903
+ const catalog = await collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visited)
904
+ const kustDir = dirname(resolve(kustAbs))
905
+ const kustNs =
906
+ typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
907
+
908
+ for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
909
+ if (
910
+ shouldValidateKustomizePatchTarget(target) &&
911
+ !kustomizeResourceCatalogMatchesPatchTarget(catalog, target)
912
+ ) {
913
+ fail(
914
+ `${rel}: ${section}[${index}].target — немає відповідного ресурсу в resources/bases/components/crds (рекурсивно): ${formatKustomizePatchTargetForMessage(target)}`
915
+ )
916
+ }
917
+ }
918
+
919
+ const patchesOnlyPath = rec.patches
920
+ if (Array.isArray(patchesOnlyPath)) {
921
+ let pIdx = 0
922
+ for (const p of patchesOnlyPath) {
923
+ pIdx++
924
+ if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
925
+ const pr = /** @type {Record<string, unknown>} */ (p)
926
+ const hasTargetKey = 'target' in pr && pr.target !== undefined && pr.target !== null
927
+ const pathStr = typeof pr.path === 'string' ? pr.path.trim() : ''
928
+ const inlinePatch = typeof pr.patch === 'string' && pr.patch.trim() !== ''
929
+ if (!hasTargetKey && pathStr !== '' && !inlinePatch && !pathStr.includes('://')) {
930
+ const resolved = resolve(kustDir, pathStr)
931
+ if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
932
+ /** @type {import('node:fs').Stats | null} */
933
+ let st = null
934
+ try {
935
+ st = await stat(resolved)
936
+ } catch {
937
+ st = null
938
+ }
939
+ if (st !== null && st.isFile() && /\.ya?ml$/iu.test(resolved)) {
940
+ const roots = await readK8sYamlDocumentRootsForInventory(resolved)
941
+ let docIdx = 0
942
+ for (const o of roots) {
943
+ docIdx++
944
+ const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
945
+ if (
946
+ d !== null &&
947
+ !catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))
948
+ ) {
949
+ const relPatch = (relative(root, resolved) || pathStr).replaceAll('\\', '/')
950
+ fail(
951
+ `${rel}: patches[${pIdx}] path «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
952
+ )
953
+ }
954
+ }
955
+ }
956
+ }
957
+ }
958
+ }
959
+ }
960
+ }
961
+
962
+ const sm = rec.patchesStrategicMerge
963
+ if (Array.isArray(sm)) {
964
+ let smIdx = 0
965
+ for (const ref of sm) {
966
+ smIdx++
967
+ if (typeof ref === 'string' && ref.trim() !== '' && !ref.includes('://')) {
968
+ const resolved = resolve(kustDir, ref.trim())
969
+ if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
970
+ /** @type {import('node:fs').Stats | null} */
971
+ let st = null
972
+ try {
973
+ st = await stat(resolved)
974
+ } catch {
975
+ st = null
976
+ }
977
+ if (st !== null && st.isFile() && /\.ya?ml$/iu.test(resolved)) {
978
+ const roots = await readK8sYamlDocumentRootsForInventory(resolved)
979
+ let docIdx = 0
980
+ for (const o of roots) {
981
+ docIdx++
982
+ const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
983
+ if (
984
+ d !== null &&
985
+ !catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))
986
+ ) {
987
+ const relPatch = (relative(root, resolved) || ref).replaceAll('\\', '/')
988
+ fail(
989
+ `${rel}: patchesStrategicMerge[${smIdx}] «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
990
+ )
991
+ }
992
+ }
993
+ }
994
+ }
995
+ }
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+
474
1006
  /**
475
1007
  * Чи це **`k8s/base/kustomization.yaml`** (перевірка обов’язкового непорожнього **`namespace:`**).
476
1008
  * @param {string} rel шлях від кореня репозиторію
@@ -1808,6 +2340,8 @@ export async function check() {
1808
2340
 
1809
2341
  await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
1810
2342
 
2343
+ await validateKustomizationPatchTargetsResolved(root, yamlFiles, fail)
2344
+
1811
2345
  await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
1812
2346
 
1813
2347
  return reporter.getExitCode()