@kabran-tecnologia/kabran-config 1.9.0 → 1.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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Kabran Config Loader
3
+ *
4
+ * Loads project configuration from kabran.config.mjs/js/json with fallback to defaults.
5
+ * Enables projects to customize validator behavior without modifying kabran-config.
6
+ *
7
+ * @module config-loader
8
+ */
9
+
10
+ import {existsSync} from 'node:fs';
11
+ import {readFile} from 'node:fs/promises';
12
+ import {join} from 'node:path';
13
+ import {pathToFileURL} from 'node:url';
14
+
15
+ /**
16
+ * Default configuration values.
17
+ * These match the current hardcoded values in validators to ensure backwards compatibility.
18
+ */
19
+ export const DEFAULTS = {
20
+ readme: {
21
+ required: ['Installation', 'Usage', 'License'],
22
+ recommended: ['Development', 'Testing', 'Contributing'],
23
+ },
24
+ env: {
25
+ requireExample: true,
26
+ detectPatterns: ['process.env', 'import.meta.env', 'Deno.env', 'os.getenv', '$_ENV'],
27
+ },
28
+ quality: {
29
+ standardPath: 'docs/quality/001-quality-standard.md',
30
+ },
31
+ };
32
+
33
+ /**
34
+ * Configuration file names to search for, in priority order.
35
+ */
36
+ const CONFIG_FILES = ['kabran.config.mjs', 'kabran.config.js', 'kabran.config.json'];
37
+
38
+ /**
39
+ * Deep merge two objects. Arrays are replaced, not merged.
40
+ * @param {object} target - Base object
41
+ * @param {object} source - Object to merge in
42
+ * @returns {object} Merged object
43
+ */
44
+ function deepMerge(target, source) {
45
+ const result = {...target};
46
+
47
+ for (const key of Object.keys(source)) {
48
+ const sourceValue = source[key];
49
+ const targetValue = target[key];
50
+
51
+ if (sourceValue === null || sourceValue === undefined) {
52
+ continue;
53
+ }
54
+
55
+ if (
56
+ typeof sourceValue === 'object' &&
57
+ !Array.isArray(sourceValue) &&
58
+ typeof targetValue === 'object' &&
59
+ !Array.isArray(targetValue)
60
+ ) {
61
+ result[key] = deepMerge(targetValue, sourceValue);
62
+ } else {
63
+ result[key] = sourceValue;
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Load JSON configuration file.
72
+ * @param {string} configPath - Path to JSON config file
73
+ * @returns {Promise<object>} Parsed configuration
74
+ */
75
+ async function loadJsonConfig(configPath) {
76
+ const content = await readFile(configPath, 'utf-8');
77
+ return JSON.parse(content);
78
+ }
79
+
80
+ /**
81
+ * Load JavaScript/ESM configuration file.
82
+ * @param {string} configPath - Path to JS/MJS config file
83
+ * @returns {Promise<object>} Parsed configuration
84
+ */
85
+ async function loadJsConfig(configPath) {
86
+ const configUrl = pathToFileURL(configPath).href;
87
+ const module = await import(configUrl);
88
+ return module.default || module;
89
+ }
90
+
91
+ /**
92
+ * Load project configuration with fallback to defaults.
93
+ *
94
+ * Searches for configuration files in order:
95
+ * 1. kabran.config.mjs
96
+ * 2. kabran.config.js
97
+ * 3. kabran.config.json
98
+ *
99
+ * If no config file is found, returns DEFAULTS.
100
+ *
101
+ * @param {string} [cwd=process.cwd()] - Directory to search for config
102
+ * @returns {Promise<object>} Merged configuration
103
+ *
104
+ * @example
105
+ * const config = await loadConfig();
106
+ * console.log(config.readme.required); // ['Installation', 'Usage', 'License']
107
+ *
108
+ * @example
109
+ * // With custom config in kabran.config.mjs:
110
+ * // export default { readme: { required: ['Setup', 'API'] } }
111
+ * const config = await loadConfig('/path/to/project');
112
+ * console.log(config.readme.required); // ['Setup', 'API']
113
+ */
114
+ export async function loadConfig(cwd = process.cwd()) {
115
+ for (const fileName of CONFIG_FILES) {
116
+ const configPath = join(cwd, fileName);
117
+
118
+ if (existsSync(configPath)) {
119
+ try {
120
+ let projectConfig;
121
+
122
+ if (fileName.endsWith('.json')) {
123
+ projectConfig = await loadJsonConfig(configPath);
124
+ } else {
125
+ projectConfig = await loadJsConfig(configPath);
126
+ }
127
+
128
+ return deepMerge(DEFAULTS, projectConfig);
129
+ } catch (error) {
130
+ // Log warning but continue with defaults
131
+ console.warn(`Warning: Failed to load ${fileName}: ${error.message}`);
132
+ }
133
+ }
134
+ }
135
+
136
+ return DEFAULTS;
137
+ }
138
+
139
+ /**
140
+ * Check if a config file exists in the given directory.
141
+ * @param {string} [cwd=process.cwd()] - Directory to check
142
+ * @returns {{exists: boolean, path?: string, name?: string}} Config file info
143
+ */
144
+ export function findConfigFile(cwd = process.cwd()) {
145
+ for (const fileName of CONFIG_FILES) {
146
+ const configPath = join(cwd, fileName);
147
+
148
+ if (existsSync(configPath)) {
149
+ return {exists: true, path: configPath, name: fileName};
150
+ }
151
+ }
152
+
153
+ return {exists: false};
154
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://kabran.dev/schemas/ci-result.v2.json",
3
+ "$id": "ci-result.v2.json",
4
4
  "title": "CI Result Schema v2",
5
5
  "description": "Unified schema for CI/CD results with quality metrics, timing, and observability data",
6
6
  "type": "object",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # ==============================================================================
3
3
  # Kabran CI Core - Shared Functions
4
- # Part of @kabran-owner/kabran-config
4
+ # Part of @kabran-tecnologia/kabran-config
5
5
  # ==============================================================================
6
6
 
7
7
  # Version
@@ -590,6 +590,279 @@ export_ci_data() {
590
590
  log_debug "CI data exported to: $output_file"
591
591
  }
592
592
 
593
+ # ==============================================================================
594
+ # OpenTelemetry Metrics Export
595
+ # ==============================================================================
596
+
597
+ # Export CI metrics to OTel Collector via OTLP HTTP
598
+ # Usage: export_ci_metrics_to_otel "$CI_DATA_FILE"
599
+ # Environment: OTEL_ENDPOINT must be set (e.g., http://localhost:4318)
600
+ # Returns: 0 on success, 1 on failure (but never fails the build due to || true usage)
601
+ export_ci_metrics_to_otel() {
602
+ local ci_data_file="${1:-}"
603
+
604
+ # Check if OTEL_ENDPOINT is configured
605
+ if [ -z "${OTEL_ENDPOINT:-}" ]; then
606
+ log_debug "OTEL_ENDPOINT not set, skipping metrics export"
607
+ return 0
608
+ fi
609
+
610
+ # Validate input file
611
+ if [ -z "$ci_data_file" ] || [ ! -f "$ci_data_file" ]; then
612
+ log_warn "CI data file not found: $ci_data_file"
613
+ return 1
614
+ fi
615
+
616
+ # Check for curl
617
+ if ! command -v curl &>/dev/null; then
618
+ log_warn "curl not available, skipping OTel metrics export"
619
+ return 1
620
+ fi
621
+
622
+ log_info "Exporting CI metrics to OTel Collector..."
623
+ log_debug " Endpoint: $OTEL_ENDPOINT"
624
+ log_debug " Data file: $ci_data_file"
625
+
626
+ # Extract metrics from CI data
627
+ local total_ms steps_json project_name trace_id ci_passed
628
+ total_ms=$(jq -r '.timing.total_ms // 0' "$ci_data_file" 2>/dev/null)
629
+ steps_json=$(jq -c '.steps // []' "$ci_data_file" 2>/dev/null)
630
+ project_name=$(jq -r '.project.name // "unknown"' "$ci_data_file" 2>/dev/null)
631
+ trace_id=$(jq -r '.trace_context.trace_id // ""' "$ci_data_file" 2>/dev/null)
632
+
633
+ # Determine overall status
634
+ local failed_count
635
+ failed_count=$(echo "$steps_json" | jq '[.[] | select(.status == "fail")] | length' 2>/dev/null || echo "0")
636
+ if [ "$failed_count" -gt 0 ]; then
637
+ ci_passed="false"
638
+ else
639
+ ci_passed="true"
640
+ fi
641
+
642
+ # Get timestamp in nanoseconds (Unix epoch)
643
+ local timestamp_ns
644
+ timestamp_ns=$(date +%s)000000000
645
+
646
+ # Build OTLP metrics payload
647
+ local otlp_payload
648
+ otlp_payload=$(build_otlp_metrics_payload \
649
+ "$project_name" \
650
+ "$total_ms" \
651
+ "$ci_passed" \
652
+ "$steps_json" \
653
+ "$timestamp_ns" \
654
+ "$trace_id")
655
+
656
+ if [ -z "$otlp_payload" ] || [ "$otlp_payload" = "null" ]; then
657
+ log_warn "Failed to build OTLP payload"
658
+ return 1
659
+ fi
660
+
661
+ # Send to OTel Collector with aggressive timeouts
662
+ # --connect-timeout 1: max 1 second to establish connection
663
+ # --max-time 5: max 5 seconds total for the request
664
+ # -f: fail silently on HTTP errors
665
+ local otel_metrics_endpoint="${OTEL_ENDPOINT}/v1/metrics"
666
+
667
+ log_debug "Sending metrics to: $otel_metrics_endpoint"
668
+
669
+ local http_code
670
+ http_code=$(curl -s -o /dev/null -w "%{http_code}" \
671
+ --connect-timeout 1 \
672
+ --max-time 5 \
673
+ -X POST \
674
+ -H "Content-Type: application/json" \
675
+ -d "$otlp_payload" \
676
+ "$otel_metrics_endpoint" 2>/dev/null) || {
677
+ log_warn "Failed to send metrics to OTel Collector (connection error)"
678
+ return 1
679
+ }
680
+
681
+ if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
682
+ log_success "CI metrics exported to OTel Collector (HTTP $http_code)"
683
+ return 0
684
+ else
685
+ log_warn "OTel Collector returned HTTP $http_code"
686
+ return 1
687
+ fi
688
+ }
689
+
690
+ # Build OTLP JSON payload for metrics
691
+ # Usage: build_otlp_metrics_payload "$project" "$duration_ms" "$passed" "$steps_json" "$timestamp_ns" "$trace_id"
692
+ build_otlp_metrics_payload() {
693
+ local project="$1"
694
+ local duration_ms="$2"
695
+ local passed="$3"
696
+ local steps_json="$4"
697
+ local timestamp_ns="$5"
698
+ local trace_id="${6:-}"
699
+
700
+ # Service attributes
701
+ local service_name="ci-runner"
702
+ local service_version="${CI_CORE_VERSION:-unknown}"
703
+
704
+ # Count steps by status
705
+ local pass_count fail_count skip_count
706
+ pass_count=$(echo "$steps_json" | jq '[.[] | select(.status == "pass")] | length' 2>/dev/null || echo "0")
707
+ fail_count=$(echo "$steps_json" | jq '[.[] | select(.status == "fail")] | length' 2>/dev/null || echo "0")
708
+ skip_count=$(echo "$steps_json" | jq '[.[] | select(.status == "skip")] | length' 2>/dev/null || echo "0")
709
+
710
+ # Build step duration data points
711
+ local step_duration_points
712
+ step_duration_points=$(echo "$steps_json" | jq -c --arg ts "$timestamp_ns" '
713
+ [.[] | select(.status != "skip") | {
714
+ attributes: ([
715
+ {key: "step.name", value: {stringValue: .name}},
716
+ {key: "step.category", value: {stringValue: (.category // "custom")}},
717
+ {key: "step.status", value: {stringValue: .status}}
718
+ ] + (if (.component // "") != "" then [{key: "step.component", value: {stringValue: .component}}] else [] end)),
719
+ startTimeUnixNano: $ts,
720
+ timeUnixNano: $ts,
721
+ asDouble: .duration_ms
722
+ }]
723
+ ' 2>/dev/null || echo "[]")
724
+
725
+ # Build resource attributes (conditionally include trace_id)
726
+ local resource_attributes
727
+ if [ -n "$trace_id" ]; then
728
+ resource_attributes=$(jq -n \
729
+ --arg service_name "$service_name" \
730
+ --arg service_version "$service_version" \
731
+ --arg project "$project" \
732
+ --arg trace_id "$trace_id" \
733
+ '[
734
+ {key: "service.name", value: {stringValue: $service_name}},
735
+ {key: "service.version", value: {stringValue: $service_version}},
736
+ {key: "project.name", value: {stringValue: $project}},
737
+ {key: "trace.id", value: {stringValue: $trace_id}}
738
+ ]')
739
+ else
740
+ resource_attributes=$(jq -n \
741
+ --arg service_name "$service_name" \
742
+ --arg service_version "$service_version" \
743
+ --arg project "$project" \
744
+ '[
745
+ {key: "service.name", value: {stringValue: $service_name}},
746
+ {key: "service.version", value: {stringValue: $service_version}},
747
+ {key: "project.name", value: {stringValue: $project}}
748
+ ]')
749
+ fi
750
+
751
+ # Determine status string
752
+ local status_str="fail"
753
+ if [ "$passed" = "true" ]; then
754
+ status_str="pass"
755
+ fi
756
+
757
+ # Build the full OTLP payload
758
+ jq -n \
759
+ --arg service_version "$service_version" \
760
+ --arg project "$project" \
761
+ --arg timestamp_ns "$timestamp_ns" \
762
+ --arg status_str "$status_str" \
763
+ --argjson duration_ms "$duration_ms" \
764
+ --argjson pass_count "$pass_count" \
765
+ --argjson fail_count "$fail_count" \
766
+ --argjson skip_count "$skip_count" \
767
+ --argjson step_duration_points "$step_duration_points" \
768
+ --argjson resource_attributes "$resource_attributes" \
769
+ '{
770
+ resourceMetrics: [{
771
+ resource: {
772
+ attributes: $resource_attributes
773
+ },
774
+ scopeMetrics: [{
775
+ scope: {
776
+ name: "kabran-config/ci-runner",
777
+ version: $service_version
778
+ },
779
+ metrics: [
780
+ {
781
+ name: "ci.build.duration",
782
+ description: "Total duration of CI build in milliseconds",
783
+ unit: "ms",
784
+ gauge: {
785
+ dataPoints: [{
786
+ attributes: [
787
+ {key: "project", value: {stringValue: $project}},
788
+ {key: "status", value: {stringValue: $status_str}}
789
+ ],
790
+ startTimeUnixNano: $timestamp_ns,
791
+ timeUnixNano: $timestamp_ns,
792
+ asDouble: $duration_ms
793
+ }]
794
+ }
795
+ },
796
+ {
797
+ name: "ci.build.status",
798
+ description: "CI build status counter (1 = occurrence)",
799
+ unit: "1",
800
+ sum: {
801
+ dataPoints: [{
802
+ attributes: [
803
+ {key: "project", value: {stringValue: $project}},
804
+ {key: "status", value: {stringValue: $status_str}}
805
+ ],
806
+ startTimeUnixNano: $timestamp_ns,
807
+ timeUnixNano: $timestamp_ns,
808
+ asInt: "1"
809
+ }],
810
+ aggregationTemporality: 2,
811
+ isMonotonic: true
812
+ }
813
+ },
814
+ {
815
+ name: "ci.step.count",
816
+ description: "Count of CI steps by status",
817
+ unit: "1",
818
+ sum: {
819
+ dataPoints: [
820
+ {
821
+ attributes: [
822
+ {key: "project", value: {stringValue: $project}},
823
+ {key: "status", value: {stringValue: "pass"}}
824
+ ],
825
+ startTimeUnixNano: $timestamp_ns,
826
+ timeUnixNano: $timestamp_ns,
827
+ asInt: ($pass_count | tostring)
828
+ },
829
+ {
830
+ attributes: [
831
+ {key: "project", value: {stringValue: $project}},
832
+ {key: "status", value: {stringValue: "fail"}}
833
+ ],
834
+ startTimeUnixNano: $timestamp_ns,
835
+ timeUnixNano: $timestamp_ns,
836
+ asInt: ($fail_count | tostring)
837
+ },
838
+ {
839
+ attributes: [
840
+ {key: "project", value: {stringValue: $project}},
841
+ {key: "status", value: {stringValue: "skip"}}
842
+ ],
843
+ startTimeUnixNano: $timestamp_ns,
844
+ timeUnixNano: $timestamp_ns,
845
+ asInt: ($skip_count | tostring)
846
+ }
847
+ ],
848
+ aggregationTemporality: 2,
849
+ isMonotonic: false
850
+ }
851
+ },
852
+ {
853
+ name: "ci.step.duration",
854
+ description: "Duration of individual CI steps in milliseconds",
855
+ unit: "ms",
856
+ gauge: {
857
+ dataPoints: $step_duration_points
858
+ }
859
+ }
860
+ ]
861
+ }]
862
+ }]
863
+ }'
864
+ }
865
+
593
866
  # ==============================================================================
594
867
  # JSON Output Generation
595
868
  # ==============================================================================
@@ -31,6 +31,7 @@ Environment Variables:
31
31
  CI_OUTPUT_FILE Output file for legacy v1 format
32
32
  CI_OUTPUT_FILE_V2 Output file for v2 format (default: docs/quality/ci-result.json)
33
33
  CI_CONFIG_FILE Path to project ci-config.sh
34
+ OTEL_ENDPOINT OTel Collector endpoint for metrics export (e.g., http://localhost:4318)
34
35
 
35
36
  Examples:
36
37
  # Run all steps
@@ -232,6 +233,14 @@ if [ "$USE_V2" = "true" ]; then
232
233
  generate_ci_json "$OUTPUT_FILE" "$CI_PASSED" "$FAILED" "$PROJECT_NAME" "$METADATA"
233
234
  fi
234
235
 
236
+ # ==============================================================================
237
+ # Export Metrics to OTel Collector (if configured)
238
+ # ==============================================================================
239
+ # Fail-safe: telemetry failures NEVER fail the build (RN-01)
240
+ if [ -n "${OTEL_ENDPOINT:-}" ]; then
241
+ export_ci_metrics_to_otel "$INTERMEDIATE_FILE" || true
242
+ fi
243
+
235
244
  # Cleanup
236
245
  rm -f "$INTERMEDIATE_FILE"
237
246
  else
@@ -51,7 +51,7 @@ export function extractHistoryEntry(result) {
51
51
  export function loadHistory(filePath) {
52
52
  if (!existsSync(filePath)) {
53
53
  return {
54
- $schema: 'https://kabran.dev/schemas/ci-result-history.json',
54
+ $schema: 'ci-result-history.json',
55
55
  version: '1.0.0',
56
56
  meta: {
57
57
  created_at: new Date().toISOString(),
@@ -75,7 +75,7 @@ export function loadHistory(filePath) {
75
75
  } catch (error) {
76
76
  // If file is corrupted, start fresh
77
77
  return {
78
- $schema: 'https://kabran.dev/schemas/ci-result-history.json',
78
+ $schema: 'ci-result-history.json',
79
79
  version: '1.0.0',
80
80
  meta: {
81
81
  created_at: new Date().toISOString(),
@@ -436,7 +436,7 @@ export function createMinimalResult({ projectName, passed = true }) {
436
436
  const now = new Date().toISOString()
437
437
 
438
438
  return {
439
- $schema: 'https://kabran.dev/schemas/ci-result.v2.json',
439
+ $schema: 'ci-result.v2.json',
440
440
  version: '1.0.0',
441
441
  meta: {
442
442
  generated_at: now,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # ==============================================================================
3
3
  # Kabran Deploy Core - Shared Functions
4
- # Part of @kabran-owner/kabran-config
4
+ # Part of @kabran-tecnologia/kabran-config
5
5
  # ==============================================================================
6
6
 
7
7
  # ==============================================================================
@@ -18,9 +18,13 @@ import {exec} from 'node:child_process';
18
18
  import {promisify} from 'node:util';
19
19
  import fs from 'node:fs';
20
20
  import path from 'node:path';
21
+ import {loadConfig, DEFAULTS} from '../core/config-loader.mjs';
21
22
 
22
23
  const execAsync = promisify(exec);
23
24
 
25
+ // Default patterns for backwards compatibility
26
+ export const DEFAULT_DETECT_PATTERNS = DEFAULTS.env.detectPatterns;
27
+
24
28
  /**
25
29
  * Check if .env file is tracked in git (CRITICAL SECURITY ISSUE)
26
30
  * @param {string} [cwd] - Directory to check (defaults to process.cwd())
@@ -40,17 +44,10 @@ export async function checkEnvInGit(cwd = process.cwd()) {
40
44
  /**
41
45
  * Detect if project uses environment variables
42
46
  * @param {string} [cwd] - Directory to check (defaults to process.cwd())
47
+ * @param {string[]} [patterns] - Patterns to search for (defaults to config patterns)
43
48
  * @returns {Promise<{usesEnv: boolean, files: string[]}>}
44
49
  */
45
- export async function detectEnvUsage(cwd = process.cwd()) {
46
- const patterns = [
47
- 'process.env', // Node.js
48
- 'os.getenv', // Python
49
- 'import.meta.env', // Vite/ESM
50
- 'Deno.env', // Deno
51
- '$_ENV', // PHP
52
- ];
53
-
50
+ export async function detectEnvUsage(cwd = process.cwd(), patterns = DEFAULT_DETECT_PATTERNS) {
54
51
  const extensions = ['js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs', 'py', 'php'];
55
52
  const extensionPattern = extensions.join(',');
56
53
 
@@ -157,6 +154,9 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
157
154
  const errors = [];
158
155
  const warnings = [];
159
156
 
157
+ // Load project config
158
+ const config = await loadConfig(cwd);
159
+
160
160
  // CRITICAL: Check if .env is committed to git
161
161
  log('Checking for .env in git...');
162
162
  const envInGit = await checkEnvInGit(cwd);
@@ -171,7 +171,7 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
171
171
 
172
172
  // Detect if project uses environment variables
173
173
  log('Detecting environment variable usage...');
174
- const {usesEnv, files} = await detectEnvUsage(cwd);
174
+ const {usesEnv, files} = await detectEnvUsage(cwd, config.env.detectPatterns);
175
175
 
176
176
  if (!usesEnv) {
177
177
  log(' No environment variable usage detected');
@@ -236,9 +236,10 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
236
236
  * @returns {Promise<Object>} Check result for ci-result.json
237
237
  */
238
238
  export async function getEnvCheckResult(cwd = process.cwd()) {
239
+ const config = await loadConfig(cwd);
239
240
  const envInGit = await checkEnvInGit(cwd);
240
241
  const envExample = checkEnvExampleExists(cwd);
241
- const {usesEnv} = await detectEnvUsage(cwd);
242
+ const {usesEnv} = await detectEnvUsage(cwd, config.env.detectPatterns);
242
243
 
243
244
  let undocumented = [];
244
245
  if (envExample.exists) {
@@ -103,7 +103,7 @@ async function runValidators(projectRoot, options = {}) {
103
103
 
104
104
  if (!options.skipReadme) {
105
105
  try {
106
- results.readme = getReadmeCheckResult(projectRoot)
106
+ results.readme = await getReadmeCheckResult(projectRoot)
107
107
  } catch (err) {
108
108
  results.readme = { status: 'fail', error: err.message }
109
109
  }
@@ -119,7 +119,7 @@ async function runValidators(projectRoot, options = {}) {
119
119
 
120
120
  if (!options.skipQualityStandard) {
121
121
  try {
122
- results.quality_standard = getQualityStandardCheckResult(projectRoot)
122
+ results.quality_standard = await getQualityStandardCheckResult(projectRoot)
123
123
  } catch (err) {
124
124
  results.quality_standard = { status: 'fail', error: err.message }
125
125
  }
@@ -216,7 +216,7 @@ export function generateCiResult(input) {
216
216
 
217
217
  // Build result object
218
218
  const result = {
219
- $schema: 'https://kabran.dev/schemas/ci-result.v2.json',
219
+ $schema: 'ci-result.v2.json',
220
220
  version: '1.0.0',
221
221
 
222
222
  meta,