@kabran-tecnologia/kabran-config 1.8.0 → 1.10.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kabran-tecnologia/kabran-config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Shared quality configurations, enforcement scripts, and CI/CD tooling for Kabran projects",
|
|
5
5
|
"author": "Kabran",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"bin": {
|
|
12
12
|
"kabran-setup": "src/scripts/setup.mjs",
|
|
13
13
|
"kabran-ci": "src/scripts/ci/ci-runner.sh",
|
|
14
|
-
"kabran-pr-comment": "src/scripts/pr-quality-comment.mjs"
|
|
14
|
+
"kabran-pr-comment": "src/scripts/pr-quality-comment.mjs",
|
|
15
|
+
"kabran-traceability": "src/scripts/traceability/validate-traceability.sh",
|
|
16
|
+
"kabran-coverage": "src/scripts/traceability/coverage-report.sh"
|
|
15
17
|
},
|
|
16
18
|
"exports": {
|
|
17
19
|
"./eslint": "./src/eslint.mjs",
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
"./scripts/env-validator": "./src/scripts/env-validator.mjs",
|
|
30
32
|
"./scripts/ci/*": "./src/scripts/ci/*",
|
|
31
33
|
"./scripts/deploy/*": "./src/scripts/deploy/*",
|
|
34
|
+
"./scripts/traceability/*": "./src/scripts/traceability/*",
|
|
32
35
|
"./scripts/setup": "./src/scripts/setup.mjs",
|
|
33
36
|
"./scripts/quality-standard-validator": "./src/scripts/quality-standard-validator.mjs",
|
|
34
37
|
"./scripts/generate-ci-result": "./src/scripts/generate-ci-result.mjs",
|
|
@@ -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
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ==============================================================================
|
|
3
|
+
# Kabran Traceability Coverage Report
|
|
4
|
+
# Part of @kabran-owner/kabran-config
|
|
5
|
+
# Implements PROP-006: JSDoc Traceability Tags
|
|
6
|
+
#
|
|
7
|
+
# Generates a report of spec coverage in the codebase.
|
|
8
|
+
# Shows which specs have code implementing them and which ACs are covered.
|
|
9
|
+
# ==============================================================================
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
# Source core functions
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
# shellcheck source=traceability-core.sh
|
|
16
|
+
source "$SCRIPT_DIR/traceability-core.sh"
|
|
17
|
+
|
|
18
|
+
# ==============================================================================
|
|
19
|
+
# Configuration
|
|
20
|
+
# ==============================================================================
|
|
21
|
+
|
|
22
|
+
SEARCH_PATH="${1:-.}"
|
|
23
|
+
SPEC_FILTER="${2:-}"
|
|
24
|
+
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
|
|
25
|
+
|
|
26
|
+
# ==============================================================================
|
|
27
|
+
# Report Functions
|
|
28
|
+
# ==============================================================================
|
|
29
|
+
|
|
30
|
+
# Generate report for a specific spec
|
|
31
|
+
report_spec() {
|
|
32
|
+
local spec="$1"
|
|
33
|
+
local files
|
|
34
|
+
|
|
35
|
+
files=$(grep -rl "@spec $spec" --include="*.ts" --include="*.tsx" "$SEARCH_PATH" 2>/dev/null || true)
|
|
36
|
+
|
|
37
|
+
if [ -z "$files" ]; then
|
|
38
|
+
return
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo ""
|
|
42
|
+
echo "=== $spec ==="
|
|
43
|
+
echo ""
|
|
44
|
+
echo "Files:"
|
|
45
|
+
echo "$files" | while read -r file; do
|
|
46
|
+
[ -n "$file" ] && echo " - $file"
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
echo "ACs Implemented:"
|
|
51
|
+
echo "$files" | while read -r file; do
|
|
52
|
+
[ -n "$file" ] && extract_implements "$file"
|
|
53
|
+
done | sort -u | while read -r ac; do
|
|
54
|
+
[ -n "$ac" ] && echo " - $ac"
|
|
55
|
+
done
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Generate summary report
|
|
59
|
+
report_summary() {
|
|
60
|
+
log_section "Traceability Coverage Report v${TRACEABILITY_VERSION}"
|
|
61
|
+
log_info "Search path: $SEARCH_PATH"
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
local total_files
|
|
65
|
+
local tagged_files
|
|
66
|
+
local specs
|
|
67
|
+
|
|
68
|
+
total_files=$(count_total_files "$SEARCH_PATH")
|
|
69
|
+
tagged_files=$(count_tagged_files "$SEARCH_PATH")
|
|
70
|
+
|
|
71
|
+
echo "## Overview"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "| Metric | Value |"
|
|
74
|
+
echo "|--------|-------|"
|
|
75
|
+
echo "| Total TypeScript files | $total_files |"
|
|
76
|
+
echo "| Files with @spec | $tagged_files |"
|
|
77
|
+
|
|
78
|
+
if [ "$total_files" -gt 0 ]; then
|
|
79
|
+
local coverage=$((tagged_files * 100 / total_files))
|
|
80
|
+
echo "| Coverage | $coverage% |"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "## Specs Found"
|
|
85
|
+
echo ""
|
|
86
|
+
|
|
87
|
+
# Find all unique specs
|
|
88
|
+
specs=$(grep -roh '@spec S[0-9]*' --include="*.ts" --include="*.tsx" "$SEARCH_PATH" 2>/dev/null | sed 's/@spec //' | sort -u || true)
|
|
89
|
+
|
|
90
|
+
if [ -z "$specs" ]; then
|
|
91
|
+
echo "No @spec tags found in codebase."
|
|
92
|
+
return
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
echo "$specs" | while read -r spec; do
|
|
96
|
+
if [ -n "$spec" ]; then
|
|
97
|
+
local file_count
|
|
98
|
+
file_count=$(grep -rl "@spec $spec" --include="*.ts" --include="*.tsx" "$SEARCH_PATH" 2>/dev/null | wc -l)
|
|
99
|
+
echo "- **$spec**: $file_count files"
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Generate detailed report for all specs
|
|
105
|
+
report_detailed() {
|
|
106
|
+
report_summary
|
|
107
|
+
|
|
108
|
+
echo ""
|
|
109
|
+
echo "## Detailed Coverage"
|
|
110
|
+
|
|
111
|
+
local specs
|
|
112
|
+
specs=$(grep -roh '@spec S[0-9]*' --include="*.ts" --include="*.tsx" "$SEARCH_PATH" 2>/dev/null | sed 's/@spec //' | sort -u || true)
|
|
113
|
+
|
|
114
|
+
if [ -n "$specs" ]; then
|
|
115
|
+
echo "$specs" | while read -r spec; do
|
|
116
|
+
[ -n "$spec" ] && report_spec "$spec"
|
|
117
|
+
done
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Generate JSON report
|
|
122
|
+
report_json() {
|
|
123
|
+
local total_files
|
|
124
|
+
local tagged_files
|
|
125
|
+
local specs
|
|
126
|
+
|
|
127
|
+
total_files=$(count_total_files "$SEARCH_PATH")
|
|
128
|
+
tagged_files=$(count_tagged_files "$SEARCH_PATH")
|
|
129
|
+
specs=$(grep -roh '@spec S[0-9]*' --include="*.ts" --include="*.tsx" "$SEARCH_PATH" 2>/dev/null | sed 's/@spec //' | sort -u || true)
|
|
130
|
+
|
|
131
|
+
local coverage=0
|
|
132
|
+
if [ "$total_files" -gt 0 ]; then
|
|
133
|
+
coverage=$((tagged_files * 100 / total_files))
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
echo "{"
|
|
137
|
+
echo " \"version\": \"${TRACEABILITY_VERSION}\","
|
|
138
|
+
echo " \"searchPath\": \"$SEARCH_PATH\","
|
|
139
|
+
echo " \"totalFiles\": $total_files,"
|
|
140
|
+
echo " \"taggedFiles\": $tagged_files,"
|
|
141
|
+
echo " \"coverage\": $coverage,"
|
|
142
|
+
echo " \"specs\": ["
|
|
143
|
+
|
|
144
|
+
local first=true
|
|
145
|
+
if [ -n "$specs" ]; then
|
|
146
|
+
echo "$specs" | while read -r spec; do
|
|
147
|
+
if [ -n "$spec" ]; then
|
|
148
|
+
local file_count
|
|
149
|
+
file_count=$(grep -rl "@spec $spec" --include="*.ts" --include="*.tsx" "$SEARCH_PATH" 2>/dev/null | wc -l)
|
|
150
|
+
if [ "$first" = true ]; then
|
|
151
|
+
first=false
|
|
152
|
+
else
|
|
153
|
+
echo ","
|
|
154
|
+
fi
|
|
155
|
+
echo -n " { \"id\": \"$spec\", \"files\": $file_count }"
|
|
156
|
+
fi
|
|
157
|
+
done
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
echo ""
|
|
161
|
+
echo " ]"
|
|
162
|
+
echo "}"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ==============================================================================
|
|
166
|
+
# Help
|
|
167
|
+
# ==============================================================================
|
|
168
|
+
|
|
169
|
+
show_help() {
|
|
170
|
+
cat << EOF
|
|
171
|
+
Kabran Traceability Coverage Report v${TRACEABILITY_VERSION}
|
|
172
|
+
|
|
173
|
+
Usage: coverage-report.sh [path] [spec] [options]
|
|
174
|
+
|
|
175
|
+
Arguments:
|
|
176
|
+
path Directory to scan (default: current directory)
|
|
177
|
+
spec Filter by specific spec ID (e.g., S25)
|
|
178
|
+
|
|
179
|
+
Environment Variables:
|
|
180
|
+
OUTPUT_FORMAT Output format: text, detailed, json (default: text)
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
coverage-report.sh ./src
|
|
184
|
+
coverage-report.sh ./src S25
|
|
185
|
+
OUTPUT_FORMAT=json coverage-report.sh ./src
|
|
186
|
+
OUTPUT_FORMAT=detailed coverage-report.sh ./src
|
|
187
|
+
|
|
188
|
+
Output Formats:
|
|
189
|
+
text Summary with spec counts
|
|
190
|
+
detailed Full breakdown with files and ACs per spec
|
|
191
|
+
json Machine-readable JSON output
|
|
192
|
+
|
|
193
|
+
EOF
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# ==============================================================================
|
|
197
|
+
# Entry Point
|
|
198
|
+
# ==============================================================================
|
|
199
|
+
|
|
200
|
+
if [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then
|
|
201
|
+
show_help
|
|
202
|
+
exit 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# Handle specific spec filter
|
|
206
|
+
if [ -n "$SPEC_FILTER" ]; then
|
|
207
|
+
report_spec "$SPEC_FILTER"
|
|
208
|
+
exit 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# Generate report based on format
|
|
212
|
+
case "$OUTPUT_FORMAT" in
|
|
213
|
+
json)
|
|
214
|
+
report_json
|
|
215
|
+
;;
|
|
216
|
+
detailed)
|
|
217
|
+
report_detailed
|
|
218
|
+
;;
|
|
219
|
+
*)
|
|
220
|
+
report_summary
|
|
221
|
+
;;
|
|
222
|
+
esac
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ==============================================================================
|
|
3
|
+
# Kabran Traceability Core - Shared Functions
|
|
4
|
+
# Part of @kabran-owner/kabran-config
|
|
5
|
+
# Implements PROP-006: JSDoc Traceability Tags
|
|
6
|
+
# ==============================================================================
|
|
7
|
+
|
|
8
|
+
# Version - Dynamically resolve from package.json
|
|
9
|
+
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
_PKG_JSON="$_SCRIPT_DIR/../../../package.json"
|
|
11
|
+
if [ -f "$_PKG_JSON" ]; then
|
|
12
|
+
TRACEABILITY_VERSION=$(grep '"version":' "$_PKG_JSON" | head -1 | sed -E 's/.*"version": "([^"]+)".*/\1/')
|
|
13
|
+
else
|
|
14
|
+
TRACEABILITY_VERSION="unknown"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Colors
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
BLUE='\033[0;34m'
|
|
22
|
+
GRAY='\033[0;90m'
|
|
23
|
+
NC='\033[0m'
|
|
24
|
+
|
|
25
|
+
# ==============================================================================
|
|
26
|
+
# Logging Functions
|
|
27
|
+
# ==============================================================================
|
|
28
|
+
|
|
29
|
+
log_info() {
|
|
30
|
+
echo -e "${BLUE}[INFO]${NC} $1"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
log_success() {
|
|
34
|
+
echo -e "${GREEN}[PASS]${NC} $1"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log_error() {
|
|
38
|
+
echo -e "${RED}[FAIL]${NC} $1"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log_warn() {
|
|
42
|
+
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log_section() {
|
|
46
|
+
echo -e "\n${BLUE}====${NC} $1 ${BLUE}====${NC}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# ==============================================================================
|
|
50
|
+
# Tag Patterns (PROP-006 Standard)
|
|
51
|
+
# ==============================================================================
|
|
52
|
+
|
|
53
|
+
# Valid @spec format: @spec SXX (ID only, not full name)
|
|
54
|
+
PATTERN_SPEC_VALID='@spec S[0-9]+'
|
|
55
|
+
PATTERN_SPEC_INVALID='@spec S[0-9]+-'
|
|
56
|
+
|
|
57
|
+
# Valid @implements format: @implements AC-XX or @implements SXX:AC-XX
|
|
58
|
+
PATTERN_IMPLEMENTS='@implements'
|
|
59
|
+
|
|
60
|
+
# Valid @task format: @task XXX-NNN
|
|
61
|
+
PATTERN_TASK='@task [A-Z]+-[0-9]+'
|
|
62
|
+
|
|
63
|
+
# Valid @prd format: @prd RF-XXX or @prd RNF-XXX
|
|
64
|
+
PATTERN_PRD='@prd R(N)?F-[0-9]+'
|
|
65
|
+
|
|
66
|
+
# ==============================================================================
|
|
67
|
+
# Validation Functions
|
|
68
|
+
# ==============================================================================
|
|
69
|
+
|
|
70
|
+
# Check if a file has @implements without @spec (integrity error)
|
|
71
|
+
# Usage: check_orphan_implements "path/to/file.ts"
|
|
72
|
+
# Returns: 0 if valid, 1 if orphan found
|
|
73
|
+
check_orphan_implements() {
|
|
74
|
+
local file="$1"
|
|
75
|
+
|
|
76
|
+
if grep -qE "$PATTERN_IMPLEMENTS" "$file" 2>/dev/null; then
|
|
77
|
+
if ! grep -qE "$PATTERN_SPEC_VALID" "$file" 2>/dev/null; then
|
|
78
|
+
return 1 # Orphan found
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
return 0 # Valid
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Check if a file uses invalid @spec format (full name instead of ID)
|
|
85
|
+
# Usage: check_spec_format "path/to/file.ts"
|
|
86
|
+
# Returns: 0 if valid, 1 if invalid format found
|
|
87
|
+
check_spec_format() {
|
|
88
|
+
local file="$1"
|
|
89
|
+
|
|
90
|
+
if grep -qE "$PATTERN_SPEC_INVALID" "$file" 2>/dev/null; then
|
|
91
|
+
return 1 # Invalid format
|
|
92
|
+
fi
|
|
93
|
+
return 0 # Valid
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Extract all @spec tags from a file
|
|
97
|
+
# Usage: extract_specs "path/to/file.ts"
|
|
98
|
+
extract_specs() {
|
|
99
|
+
local file="$1"
|
|
100
|
+
grep -oE '@spec S[0-9]+' "$file" 2>/dev/null | sed 's/@spec //' | sort -u
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Extract all @implements tags from a file
|
|
104
|
+
# Usage: extract_implements "path/to/file.ts"
|
|
105
|
+
extract_implements() {
|
|
106
|
+
local file="$1"
|
|
107
|
+
grep -oE '@implements [^*]+' "$file" 2>/dev/null | sed 's/@implements //' | tr ',' '\n' | sed 's/^ *//' | sort -u
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Count files with traceability tags
|
|
111
|
+
# Usage: count_tagged_files "path/to/search"
|
|
112
|
+
count_tagged_files() {
|
|
113
|
+
local search_path="${1:-.}"
|
|
114
|
+
grep -rl "@spec" --include="*.ts" --include="*.tsx" "$search_path" 2>/dev/null | wc -l
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Count total TypeScript files
|
|
118
|
+
# Usage: count_total_files "path/to/search"
|
|
119
|
+
count_total_files() {
|
|
120
|
+
local search_path="${1:-.}"
|
|
121
|
+
find "$search_path" -type f \( -name "*.ts" -o -name "*.tsx" \) 2>/dev/null | wc -l
|
|
122
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ==============================================================================
|
|
3
|
+
# Kabran Traceability Validator
|
|
4
|
+
# Part of @kabran-owner/kabran-config
|
|
5
|
+
# Implements PROP-006: JSDoc Traceability Tags
|
|
6
|
+
#
|
|
7
|
+
# Validates FORMAT of traceability tags (not presence).
|
|
8
|
+
# Tags are optional - this script only checks:
|
|
9
|
+
# 1. @implements without @spec (integrity error)
|
|
10
|
+
# 2. @spec with full name instead of ID (format warning)
|
|
11
|
+
# ==============================================================================
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
# Source core functions
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
# shellcheck source=traceability-core.sh
|
|
18
|
+
source "$SCRIPT_DIR/traceability-core.sh"
|
|
19
|
+
|
|
20
|
+
# ==============================================================================
|
|
21
|
+
# Configuration
|
|
22
|
+
# ==============================================================================
|
|
23
|
+
|
|
24
|
+
SEARCH_PATH="${1:-.}"
|
|
25
|
+
STRICT_MODE="${STRICT_MODE:-false}"
|
|
26
|
+
EXIT_CODE=0
|
|
27
|
+
|
|
28
|
+
# ==============================================================================
|
|
29
|
+
# Main Validation
|
|
30
|
+
# ==============================================================================
|
|
31
|
+
|
|
32
|
+
main() {
|
|
33
|
+
log_section "Traceability Validation v${TRACEABILITY_VERSION}"
|
|
34
|
+
log_info "Search path: $SEARCH_PATH"
|
|
35
|
+
log_info "Strict mode: $STRICT_MODE"
|
|
36
|
+
|
|
37
|
+
local orphan_count=0
|
|
38
|
+
local format_count=0
|
|
39
|
+
local files_checked=0
|
|
40
|
+
|
|
41
|
+
# Find all TypeScript files
|
|
42
|
+
local files
|
|
43
|
+
files=$(find "$SEARCH_PATH" -type f \( -name "*.ts" -o -name "*.tsx" \) 2>/dev/null || true)
|
|
44
|
+
|
|
45
|
+
if [ -z "$files" ]; then
|
|
46
|
+
log_info "No TypeScript files found"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
while IFS= read -r file; do
|
|
51
|
+
[ -z "$file" ] && continue
|
|
52
|
+
files_checked=$((files_checked + 1))
|
|
53
|
+
|
|
54
|
+
# Check for orphan @implements (ERROR)
|
|
55
|
+
if ! check_orphan_implements "$file"; then
|
|
56
|
+
log_error "Orphan @implements (no @spec): $file"
|
|
57
|
+
orphan_count=$((orphan_count + 1))
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Check for invalid @spec format (WARNING)
|
|
61
|
+
if ! check_spec_format "$file"; then
|
|
62
|
+
log_warn "Invalid @spec format (use ID only): $file"
|
|
63
|
+
format_count=$((format_count + 1))
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
done <<< "$files"
|
|
67
|
+
|
|
68
|
+
# Summary
|
|
69
|
+
log_section "Validation Summary"
|
|
70
|
+
log_info "Files checked: $files_checked"
|
|
71
|
+
|
|
72
|
+
if [ "$orphan_count" -gt 0 ]; then
|
|
73
|
+
log_error "Orphan @implements found: $orphan_count"
|
|
74
|
+
EXIT_CODE=1
|
|
75
|
+
else
|
|
76
|
+
log_success "No orphan @implements found"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [ "$format_count" -gt 0 ]; then
|
|
80
|
+
log_warn "Invalid @spec format: $format_count"
|
|
81
|
+
if [ "$STRICT_MODE" = "true" ]; then
|
|
82
|
+
EXIT_CODE=1
|
|
83
|
+
fi
|
|
84
|
+
else
|
|
85
|
+
log_success "All @spec tags use correct format"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Coverage stats (informational)
|
|
89
|
+
local tagged_files
|
|
90
|
+
local total_files
|
|
91
|
+
tagged_files=$(count_tagged_files "$SEARCH_PATH")
|
|
92
|
+
total_files=$(count_total_files "$SEARCH_PATH")
|
|
93
|
+
|
|
94
|
+
if [ "$total_files" -gt 0 ]; then
|
|
95
|
+
local coverage=$((tagged_files * 100 / total_files))
|
|
96
|
+
log_info "Traceability coverage: $tagged_files/$total_files files ($coverage%)"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
return $EXIT_CODE
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ==============================================================================
|
|
103
|
+
# Help
|
|
104
|
+
# ==============================================================================
|
|
105
|
+
|
|
106
|
+
show_help() {
|
|
107
|
+
cat << EOF
|
|
108
|
+
Kabran Traceability Validator v${TRACEABILITY_VERSION}
|
|
109
|
+
|
|
110
|
+
Usage: validate-traceability.sh [path] [options]
|
|
111
|
+
|
|
112
|
+
Arguments:
|
|
113
|
+
path Directory to validate (default: current directory)
|
|
114
|
+
|
|
115
|
+
Environment Variables:
|
|
116
|
+
STRICT_MODE If "true", format warnings become errors (default: false)
|
|
117
|
+
|
|
118
|
+
Exit Codes:
|
|
119
|
+
0 Validation passed
|
|
120
|
+
1 Validation failed (orphan @implements or strict mode violations)
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
validate-traceability.sh ./src
|
|
124
|
+
STRICT_MODE=true validate-traceability.sh ./src
|
|
125
|
+
|
|
126
|
+
Tags Validated (PROP-006):
|
|
127
|
+
@spec S25 Link to spec (ID only, not full name)
|
|
128
|
+
@implements AC-01 Acceptance criteria implemented
|
|
129
|
+
@task AGT-123 Link to Linear task
|
|
130
|
+
@prd RF-007 Link to PRD requirement
|
|
131
|
+
|
|
132
|
+
Rules:
|
|
133
|
+
- All tags are OPTIONAL
|
|
134
|
+
- @implements WITHOUT @spec is an ERROR
|
|
135
|
+
- @spec with full name (S25-name) is a WARNING
|
|
136
|
+
|
|
137
|
+
EOF
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ==============================================================================
|
|
141
|
+
# Entry Point
|
|
142
|
+
# ==============================================================================
|
|
143
|
+
|
|
144
|
+
if [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then
|
|
145
|
+
show_help
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
main
|
|
150
|
+
exit $EXIT_CODE
|