diff --git a/.rabbit/context.yaml b/.rabbit/context.yaml index d5719ae..7c53354 100644 --- a/.rabbit/context.yaml +++ b/.rabbit/context.yaml @@ -5,8 +5,8 @@ version: udx.dev/dev.kit/v1 generator: tool: dev.kit repo: https://github.com/udx/dev.kit - version: 0.10.0 - generated_at: 2026-05-13T00:55:54Z + version: 0.11.0 + generated_at: 2026-05-19T18:52:32Z repo: name: dev.kit @@ -22,12 +22,14 @@ refs: - ./changes.md - ./Makefile - ./docs/references/command-surfaces.md + - ./docs/real-repo-validation.md - ./docs/references/repo-design.md - ./src/configs/archetypes.yaml - ./src/configs/audit-rules.yaml - ./src/configs/context-config.yaml - ./src/configs/detection-patterns.yaml - ./src/configs/detection-signals.yaml + - ./src/configs/repo-validation.yaml - ./deploy.yml - ./.github/workflows - ./docs @@ -70,14 +72,16 @@ gaps: dependencies: - repo: udx/reusable-workflows kind: reusable workflow - resolved: false + resolved: true + archetype: workflow-repo used_by: - .github/workflows/context7-ops.yml - .github/workflows/npm-release-ops.yml - repo: udx/worker kind: manifest contract (deploy) - resolved: false + resolved: true declared_as: udx.io/worker-v1/deploy + archetype: manifest-repo used_by: - deploy.yml @@ -140,6 +144,22 @@ manifests: - version: udx.dev/dev.kit/v1 - path reference: lib/modules/repo_signals.sh - path reference: tests/suite.sh + - path: src/configs/repo-validation.yaml + kind: repoValidationConfig + description: Public repo probes and validation defaults for dev.kit release checks + declared_as: udx.dev/dev.kit/v1 + used_by: + - docs/real-repo-validation.md + - lib/modules/config_catalog.sh + - tests/real-repos.sh + evidence: + - version: udx.dev/dev.kit/v1 + - github reference: udx/worker + - github reference: udx/reusable-workflows + - github reference: udx/github-rabbit-action + - path reference: docs/real-repo-validation.md + - path reference: lib/modules/config_catalog.sh + - path reference: tests/real-repos.sh - path: deploy.yml kind: workerDeployConfig declared_as: udx.io/worker-v1/deploy @@ -163,4 +183,3 @@ manifests: - path reference: src/configs/context-config.yaml - path reference: src/configs/detection-signals.yaml - path reference: tests/suite.sh - diff --git a/README.md b/README.md index 57379bd..b1f2a60 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ All commands support `--json`. - [Environment Config](docs/environment-config.md) - [Context Coverage](docs/context-coverage.md) - [Integration](docs/integration.md) +- [Real Repo Validation](docs/real-repo-validation.md) - [Smart Dependency Detection](docs/smart-dependency-detection.md) - [Reference Docs](docs/references/README.md) - [Reference: Command and Workflow Surfaces](docs/references/command-surfaces.md) @@ -199,5 +200,5 @@ bash tests/worker-smoke.sh ``` ```bash -bash tests/real-repos.sh /path/to/repo1 /path/to/repo2 +bash tests/real-repos.sh --check /path/to/repo1 /path/to/repo2 ``` diff --git a/bin/dev-kit b/bin/dev-kit index 004bfa4..b1711de 100755 --- a/bin/dev-kit +++ b/bin/dev-kit @@ -309,6 +309,94 @@ dev_kit_home_context_reason() { fi } +dev_kit_home_repo_workflow_status() { + local repo_root="$1" + local context_status="$2" + local context_yaml_path="$3" + local gap_count=0 + + [ -n "$repo_root" ] || { + printf '%s' "unavailable" + return 0 + } + + case "$context_status" in + missing) + printf '%s' "needs_repo_context" + return 0 + ;; + stale) + printf '%s' "stale_context" + return 0 + ;; + esac + + if [ -n "$context_yaml_path" ] && [ -f "$context_yaml_path" ]; then + gap_count="$(dev_kit_context_section_item_count "$context_yaml_path" "gaps" '^ - factor:')" + else + gap_count="$(dev_kit_repo_gap_count "$repo_root")" + fi + + if [ "${gap_count:-0}" -gt 0 ]; then + printf '%s' "needs_repair" + return 0 + fi + + printf '%s' "ready" +} + +dev_kit_home_workflow_status() { + local repo_detected="$1" + local env_status="$2" + local repo_status="$3" + + if [ "$env_status" = "blocked" ]; then + printf '%s' "blocked" + return 0 + fi + + if [ "$repo_detected" != "true" ]; then + printf '%s' "workspace_only" + return 0 + fi + + case "$repo_status" in + needs_repo_context|stale_context) + printf '%s' "needs_repo_context" + ;; + needs_repair) + printf '%s' "needs_repair" + ;; + *) + printf '%s' "ready" + ;; + esac +} + +dev_kit_home_workflow_json() { + local repo_detected="$1" + local repo_root="$2" + local context_status="$3" + local context_yaml_path="$4" + local env_status="" + local repo_status="" + local workflow_status="" + local jobs_json="" + + env_status="$(dev_kit_env_workflow_status)" + repo_status="$(dev_kit_home_repo_workflow_status "$repo_root" "$context_status" "$context_yaml_path")" + workflow_status="$(dev_kit_home_workflow_status "$repo_detected" "$env_status" "$repo_status")" + jobs_json="$(dev_kit_env_workflow_job_json)" + + if [ "$repo_detected" = "true" ]; then + jobs_json="${jobs_json}, $(dev_kit_repo_workflow_job_json "$repo_root" "$repo_status" "write" "$context_status")" + fi + + printf '{ "id": "dev-kit", "label": "Normalize repo and environment", "status": "%s", "jobs": [%s] }' \ + "$(dev_kit_json_escape "$workflow_status")" \ + "$jobs_json" +} + dev_kit_run_home() { local format="${1:-text}" local state="installed" @@ -319,6 +407,9 @@ dev_kit_run_home() { local context_yaml_path="" local context_status="none" local context_reason="" + local env_workflow_status="" + local repo_workflow_status="" + local workflow_status="" repo_root="$(dev_kit_repo_root "$repo_dir")" if [ -n "$repo_root" ]; then @@ -344,11 +435,16 @@ dev_kit_run_home() { fi fi + env_workflow_status="$(dev_kit_env_workflow_status)" + repo_workflow_status="$(dev_kit_home_repo_workflow_status "$repo_root" "$context_status" "$context_yaml_path")" + workflow_status="$(dev_kit_home_workflow_status "$repo_detected" "$env_workflow_status" "$repo_workflow_status")" + if [ "$format" = "json" ]; then printf '{\n' printf ' "name": "dev.kit",\n' printf ' "home": "%s",\n' "$(dev_kit_json_escape "$DEV_KIT_HOME")" printf ' "state": "%s",\n' "$(dev_kit_json_escape "$state")" + printf ' "workflow": %s,\n' "$(dev_kit_home_workflow_json "$repo_detected" "$repo_root" "$context_status" "$context_yaml_path")" printf ' "workspace": {\n' printf ' "path": "%s",\n' "$(dev_kit_json_escape "$repo_dir")" printf ' "repo_detected": %s,\n' "$repo_detected" @@ -392,6 +488,13 @@ dev_kit_run_home() { # Text mode: print title immediately, then compute and print progressively dev_kit_output_title "dev.kit" + dev_kit_output_section "workflow" + dev_kit_output_row "status" "$workflow_status" + dev_kit_output_row "env" "$env_workflow_status" + if [ "$repo_detected" = "true" ]; then + dev_kit_output_row "repo" "$repo_workflow_status" + fi + # ── Environment: list tools by category with what they enable ─────────────── local _env_line _env_cat _env_val _prev_cat="" while IFS= read -r _env_line; do @@ -422,7 +525,6 @@ EOF context_yaml_path="$(dev_kit_context_yaml_path "$repo_root")" dev_kit_output_summary "${summary_name} • ${summary_archetype}" - dev_kit_output_section "context" dev_kit_output_row "path" "$context_yaml_path" dev_kit_output_row "status" "$context_status" diff --git a/bin/scripts/install.sh b/bin/scripts/install.sh index e96d05c..fbf2376 100755 --- a/bin/scripts/install.sh +++ b/bin/scripts/install.sh @@ -173,6 +173,7 @@ main() { find "$DEV_KIT_HOME/bin" -type f -exec chmod +x {} \; ln -sfn "$DEV_KIT_HOME/bin/dev-kit" "$target" + ln -sfn "$DEV_KIT_HOME/bin/dev-kit" "${DEV_KIT_BIN_DIR}/dev-kit" if command -v dev_kit_output_title >/dev/null 2>&1; then dev_kit_output_title "Installed dev.kit" diff --git a/changes.md b/changes.md index a495a50..ac30e80 100644 --- a/changes.md +++ b/changes.md @@ -1,6 +1,10 @@ # Changes -### unreleased +### 0.11.0 + +- Expose repo-centric workflow status in `dev.kit`, `dev.kit env`, and `dev.kit repo` output so environment checks, context refresh, and gap repair appear as one repo-owned loop +- Clarify the repo contract boundary between deterministic structured parsing and interpreted prose/session intent +- Make real-repo validation read-only by default with guarded JSON reports, and move release probe values into repo-owned config ### 0.10.0 diff --git a/docs/context-coverage.md b/docs/context-coverage.md index 8261cee..442a167 100644 --- a/docs/context-coverage.md +++ b/docs/context-coverage.md @@ -14,6 +14,8 @@ It should answer three questions: For the boundary between generated contract data and durable repo docs, see [Repo Contract Boundary](repo-contract-boundary.md). +Structured sources such as YAML, JSON, workflow refs, and manifest metadata should be parsed programmatically where possible. Prose sources such as Markdown docs, prompts, reviews, and session notes should be treated as interpreted intent unless they point to a concrete script, manifest, or command. + Typical sections include: - generator metadata diff --git a/docs/real-repo-validation.md b/docs/real-repo-validation.md new file mode 100644 index 0000000..0cc57b6 --- /dev/null +++ b/docs/real-repo-validation.md @@ -0,0 +1,47 @@ +# Real Repo Validation + +dev.kit is validated at two levels: + +- fixtures verify deterministic contracts inside this repo +- real repos verify whether dev.kit reads repo-owned evidence well enough to guide work + +Keep fixtures small. They should cover stable output shape, gap categories, manifest handling, and known regressions. They should not try to model every UDX repo. + +Use real repos as optimization probes before a release. Run them read-only by default so the probe reports what dev.kit sees without changing the target repo. + +## Local UDX Matrix + +Use the matrix declared in `src/configs/repo-validation.yaml` when the listed repos are available locally. + +Example: + +```bash +bash tests/real-repos.sh --check ./reusable-workflows ./github-rabbit-action +``` + +The summary should be used to compare: + +- home context status +- repo workflow status +- repo context status +- gap count +- read-first ref count + +When one repo looks noisy or inconsistent, repair the strongest repo-owned gap or dev.kit normalization issue, rerun the matrix, and verify the output changed. + +## Public Repo Probes + +Public repos are useful for compatibility checks, but they should be optional and pinned when used for repeatable release evidence. Upstream repos change for reasons unrelated to dev.kit. + +Use public probes to test broad ecosystem recognition: + +- package manifests and scripts +- docs-first repos +- workflow-only repos +- container repos + +Do not assert exact output for moving public repos in the default suite. + +## Write Mode + +`tests/real-repos.sh --write` generates `.rabbit/context.yaml` in the target repo. Use it only for temp clones or repos intentionally selected for context regeneration. diff --git a/docs/repo-contract-boundary.md b/docs/repo-contract-boundary.md index 98c1c6d..70db651 100644 --- a/docs/repo-contract-boundary.md +++ b/docs/repo-contract-boundary.md @@ -20,6 +20,21 @@ Use **scripts and manifests** for programmatic execution: - deploy manifests such as `deploy.yml` - checked-in config examples when they are part of the runnable contract +Use **structured refs** for deterministic parsing: + +- YAML and JSON manifests +- package and tool config files +- workflow references +- version, kind, and description metadata +- explicit command definitions + +Use **prose and session material** for interpreted intent: + +- Markdown docs and design notes +- issue, PR, and review discussions +- agent prompts and session summaries +- manual decisions that are not encoded in scripts yet + Use **`.rabbit/context.yaml`** for generated operational summary: - direct-read refs @@ -38,6 +53,8 @@ If a repo needs a compact generated summary for tooling or safe repo-scoped exec If a repo needs something to be executed safely by tools, keep that in a script or manifest rather than only in prose docs. +When both structured and prose sources exist, `dev.kit` should parse the structured source first and use prose to explain intent, repair guidance, or open decisions. That keeps automation deterministic while still preserving the human and agent context behind the repo design. + ## Repair loop 1. `dev.kit repo` detects a gap or weak contract diff --git a/lib/commands/env.sh b/lib/commands/env.sh index c4f4d68..3a0f839 100644 --- a/lib/commands/env.sh +++ b/lib/commands/env.sh @@ -2,6 +2,76 @@ # @description: Inspect environment tools and dev.kit usage config +dev_kit_env_workflow_status() { + local missing_tools="" + local disabled_tools="" + local disabled_credentials="" + + missing_tools="$(dev_kit_env_missing_base_tools)" + disabled_tools="$(dev_kit_env_config_list "disabled_tools")" + disabled_credentials="$(dev_kit_env_config_list "disabled_credentials")" + + if [ -n "$missing_tools" ]; then + printf '%s' "blocked" + return 0 + fi + + if [ -n "$disabled_tools" ] || [ -n "$disabled_credentials" ]; then + printf '%s' "customized" + return 0 + fi + + printf '%s' "ready" +} + +dev_kit_env_workflow_step_summaries() { + local config_path="" + + config_path="$(dev_kit_env_config_path)" + printf '%s\n' "Detect local tools and auth capabilities." + printf '%s\n' "Resolve repo-facing capabilities from the current machine." + if [ -f "$config_path" ]; then + printf '%s\n' "Review environment config overrides at ${config_path}." + else + printf '%s\n' "Create ${config_path} with dev.kit env --config when overrides are needed." + fi +} + +dev_kit_env_workflow_job_json() { + local config_path="" + local disabled_tools="" + local disabled_credentials="" + local missing_tools="" + local workflow_status="" + local config_status="ready" + + config_path="$(dev_kit_env_config_path)" + disabled_tools="$(dev_kit_env_config_list "disabled_tools")" + disabled_credentials="$(dev_kit_env_config_list "disabled_credentials")" + missing_tools="$(dev_kit_env_missing_base_tools)" + workflow_status="$(dev_kit_env_workflow_status)" + + if [ -n "$disabled_tools" ] || [ -n "$disabled_credentials" ]; then + config_status="customized" + fi + + printf '{ "id": "env", "label": "Resolve local environment", "command": "dev.kit env", "status": "%s", "steps": [' \ + "$(dev_kit_json_escape "$workflow_status")" + printf '{ "id": "detect_tools", "type": "inspect", "label": "Detect local tools", "status": "%s", "missing_required": %s, "tools": %s }, ' \ + "$(if [ -n "$missing_tools" ]; then printf 'blocked'; else printf 'ready'; fi)" \ + "$(printf '%s' "$missing_tools" | tr ' ' '\n' | awk 'NF' | dev_kit_lines_to_json_array)" \ + "$(dev_kit_env_tools_json)" + printf '{ "id": "resolve_capabilities", "type": "inspect", "label": "Resolve capabilities from the current machine", "status": "ready", "capabilities": %s }, ' \ + "$(dev_kit_global_context_capabilities_json)" + printf '{ "id": "review_config", "type": "read", "label": "Review environment config overrides", "status": "%s", "path": "%s", "exists": %s, "disabled_tools": %s, "disabled_credentials": %s }' \ + "$(dev_kit_json_escape "$config_status")" \ + "$(dev_kit_json_escape "$config_path")" \ + "$([ -f "$config_path" ] && printf 'true' || printf 'false')" \ + "$(printf '%s' "$disabled_tools" | dev_kit_lines_to_json_array)" \ + "$(printf '%s' "$disabled_credentials" | dev_kit_lines_to_json_array)" + printf '] }' +} + dev_kit_cmd_env() { local format="${1:-text}" local manage_config=0 @@ -35,6 +105,7 @@ dev_kit_cmd_env() { printf '{\n' printf ' "command": "env",\n' printf ' "home": "%s",\n' "$(dev_kit_json_escape "$DEV_KIT_HOME")" + printf ' "workflow": { "id": "dev-kit", "label": "Normalize repo and environment", "jobs": [%s] },\n' "$(dev_kit_env_workflow_job_json)" printf ' "tools": %s,\n' "$(dev_kit_env_tools_json)" printf ' "capabilities": %s,\n' "$(dev_kit_global_context_capabilities_json)" printf ' "config": {\n' @@ -49,6 +120,13 @@ dev_kit_cmd_env() { dev_kit_output_title "dev.kit env" + dev_kit_output_section "workflow" + dev_kit_output_row "job" "env" + dev_kit_output_row "status" "$(dev_kit_env_workflow_status)" + dev_kit_output_list_from_lines </dev/null || true)" gap_count="${gap_count:-0}" + workflow_status="$(dev_kit_repo_workflow_status "$repo_dir" "$gap_count")" actions_json="$(dev_kit_repo_actions_json "$gap_count")" # JSON mode: compute everything up front then emit template if [ "$format" = "json" ]; then if [ "$mode" = "write" ]; then dev_kit_context_yaml_write "$repo_dir" "$force_resolve" >/dev/null + workflow_context_status="current" + elif [ -f "$context_yaml_path" ]; then + workflow_context_status="existing" + else + workflow_context_status="missing" fi dev_kit_template_render "repo.json" \ "command=repo" \ @@ -93,6 +97,7 @@ dev_kit_cmd_repo() { "factors=$(dev_kit_repo_factor_summary_json "$repo_dir")" \ "gaps=$gaps_json" \ "actions=$actions_json" \ + "workflow={ \"id\": \"dev-kit\", \"label\": \"Normalize repo and environment\", \"jobs\": [$(dev_kit_repo_workflow_job_json "$repo_dir" "$workflow_status" "$mode" "$workflow_context_status" "$gap_count")] }" \ "context=$(dev_kit_json_escape "$context_yaml_path")" \ "dependencies=$(dev_kit_deps_json "$repo_dir")" \ "recommended_repos=$(dev_kit_repo_recommended_repos_json)" @@ -109,6 +114,13 @@ dev_kit_cmd_repo() { dev_kit_output_summary "${repo_name} • ${archetype} • mode: ${mode}" + dev_kit_output_section "workflow" + dev_kit_output_row "job" "repo" + dev_kit_output_row "status" "$workflow_status" + dev_kit_output_list_from_lines </dev/null || true)" + repo_slug="$(dev_kit_repo_current_slug "$repo_dir" "$repo_name" 2>/dev/null || true)" + real_repo_dir="$(cd "$repo_dir" 2>/dev/null && pwd || true)" + + [ "$real_repo_dir" = "$REPO_DIR" ] || { [ "$repo_slug" = "udx/dev.kit" ] && [ "$repo_name" = "dev.kit" ]; } +} + +dev_kit_repo_reference_doc_default() { + local repo_dir="$1" + local first_doc="" + + first_doc="$(dev_kit_repo_first_existing_signal "$repo_dir" "documentation_files" 2>/dev/null || true)" + if [ -n "$first_doc" ]; then + printf '%s' "$first_doc" + return 0 + fi + + if dev_kit_repo_has_dir "$repo_dir" "docs"; then + printf '%s' "docs/" + return 0 + fi + + printf '%s' "README.md" +} + +dev_kit_repo_dependency_reference_local() { + local repo_dir="$1" + local first_manifest="" + local first_workflow="" + + first_manifest="$(dev_kit_repo_contract_manifest_files "$repo_dir" | awk 'NF { print; exit }')" + if [ -n "$first_manifest" ]; then + printf '%s' "$first_manifest" + return 0 + fi + + first_workflow="$(dev_kit_repo_first_existing_signal "$repo_dir" "workflow_primary_files" 2>/dev/null || true)" + if [ -n "$first_workflow" ]; then + printf '%s' "$first_workflow" + return 0 + fi + + if dev_kit_has_file "$repo_dir" "deploy.yml"; then + printf '%s' "deploy.yml" + return 0 + fi + + dev_kit_repo_reference_doc_default "$repo_dir" +} + +dev_kit_repo_config_reference_local() { + local repo_dir="$1" + local runtime_config="" + local config_doc="" + + config_doc="$(dev_kit_repo_documented_env_var_sources "$repo_dir" | awk 'NF { print; exit }')" + if [ -n "$config_doc" ]; then + printf '%s' "$config_doc" + return 0 + fi + + runtime_config="$(dev_kit_repo_first_existing_signal "$repo_dir" "config_runtime_files" 2>/dev/null || true)" + if [ -n "$runtime_config" ]; then + printf '%s' "$runtime_config" + return 0 + fi + + if dev_kit_has_file "$repo_dir" ".env.example"; then + printf '%s' ".env.example" + return 0 + fi + + dev_kit_repo_reference_doc_default "$repo_dir" +} + +dev_kit_repo_pipeline_reference_local() { + local repo_dir="$1" + local verify_source="" + local first_workflow="" + + verify_source="$(dev_kit_repo_command_detection_result "$repo_dir" "verification" 2>/dev/null | cut -d'|' -f3)" + if [ -n "$verify_source" ]; then + printf '%s' "$verify_source" + return 0 + fi + + first_workflow="$(dev_kit_repo_first_existing_signal "$repo_dir" "workflow_primary_files" 2>/dev/null || true)" + if [ -n "$first_workflow" ]; then + printf '%s' "$first_workflow" + return 0 + fi + + if dev_kit_has_file "$repo_dir" "deploy.yml"; then + printf '%s' "deploy.yml" + return 0 + fi + + dev_kit_repo_reference_doc_default "$repo_dir" +} + dev_kit_repo_factor_reference() { + local repo_dir="" local factor="$1" local status="$2" - local maybe_status="$3" - if [ -n "${maybe_status:-}" ]; then + if [ "$#" -ge 3 ]; then + repo_dir="$1" factor="$2" status="$3" fi + if [ -n "$repo_dir" ] && ! dev_kit_repo_prefers_internal_references "$repo_dir"; then + case "${factor}:${status}" in + dependencies:partial|dependencies:missing) + dev_kit_repo_dependency_reference_local "$repo_dir" + ;; + config:partial|config:missing) + dev_kit_repo_config_reference_local "$repo_dir" + ;; + pipeline:partial|pipeline:missing) + dev_kit_repo_pipeline_reference_local "$repo_dir" + ;; + esac + return 0 + fi + case "${factor}:${status}" in dependencies:partial|dependencies:missing) printf '%s' "docs/references/dependency-contracts.md" diff --git a/lib/modules/repo_scaffold.sh b/lib/modules/repo_scaffold.sh index 373215f..a04c404 100644 --- a/lib/modules/repo_scaffold.sh +++ b/lib/modules/repo_scaffold.sh @@ -310,16 +310,9 @@ EOF dev_kit_manifest_source_repo() { local manifest_path="$1" - local comment_repo="" local version_value="" local version_repo="" - comment_repo="$(dev_kit_manifest_comment_repo_refs "$manifest_path" | head -n 1)" - if [ -n "$comment_repo" ]; then - printf '%s\n' "$comment_repo" - return 0 - fi - version_value="$(dev_kit_manifest_version_value "$manifest_path")" if [ -n "$version_value" ]; then version_repo="$(dev_kit_dep_version_repo_slug "$version_value" 2>/dev/null || true)" @@ -1069,7 +1062,7 @@ EOF if [ -n "$_manifests_yaml" ]; then dev_kit_context_section_comment_block "manifests" printf 'manifests:\n' - printf '%b\n' "$_manifests_yaml" + printf '%b' "$_manifests_yaml" fi } > "$context_path" diff --git a/lib/modules/repo_signals.sh b/lib/modules/repo_signals.sh index a99eeb8..a90fe02 100644 --- a/lib/modules/repo_signals.sh +++ b/lib/modules/repo_signals.sh @@ -539,6 +539,9 @@ dev_kit_repo_contract_manifest_refs() { while IFS= read -r manifest_rel; do [ -n "$manifest_rel" ] || continue case "$manifest_rel" in + .rabbit/*/*) + continue + ;; .rabbit/*.yml|.rabbit/*.yaml|*.yml|*.yaml) printf './%s\n' "$manifest_rel" ;; diff --git a/lib/modules/repo_workflows.sh b/lib/modules/repo_workflows.sh index 2fdb8af..6c41ecb 100644 --- a/lib/modules/repo_workflows.sh +++ b/lib/modules/repo_workflows.sh @@ -26,11 +26,50 @@ dev_kit_repo_entrypoint_source() { printf "%s" "$(printf '%s' "$result" | cut -d'|' -f3)" } +dev_kit_repo_gap_count() { + local repo_dir="$1" + local factor="" + local status="" + local gap_count=0 + + while IFS= read -r factor; do + [ -n "$factor" ] || continue + status="$(dev_kit_repo_factor_status "$repo_dir" "$factor")" + case "$status" in + missing|partial) + gap_count=$((gap_count + 1)) + ;; + esac + done <&2; usage >&2; exit 1 ;; + *) repo_paths+=("$1") ;; + esac + shift +done + +if [ "${#repo_paths[@]}" -eq 0 ] && [ -n "$REAL_REPOS_CSV" ]; then while IFS= read -r repo_path; do [ -n "$repo_path" ] || continue repo_paths+=("$repo_path") done <&2 exit 1 fi mkdir -p "$DEV_KIT_BIN_DIR" +mkdir -p "$REPORT_DIR" ln -sfn "$REPO_DIR/bin/dev-kit" "$DEV_KIT_BIN_DIR/dev.kit" export HOME="$TEST_HOME" @@ -64,28 +91,102 @@ export PATH="$DEV_KIT_BIN_DIR:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" unset DEV_KIT_HOME unset DEV_KIT_BIN_DIR +require_jq() { + if ! command -v jq >/dev/null 2>&1; then + printf 'jq is required for real repo report summaries\n' >&2 + exit 1 + fi +} + +report_name_for_repo() { + local repo_path="$1" + local safe_path="" + local path_hash="" + + safe_path="$(printf '%s' "$repo_path" | tr -c 'A-Za-z0-9._-' '-')" + path_hash="$(printf '%s' "$repo_path" | cksum | awk '{ print $1 }')" + printf '%s-%s' "$safe_path" "$path_hash" +} + +print_check_summary() { + local repo_path="$1" + local home_json="$2" + local repo_json="$3" + local refs_count="" + local gap_count="" + local workflow_status="" + local context_status="" + local home_context_status="" + + home_context_status="$(jq -r '.synced.context_status // "none"' "$home_json")" + workflow_status="$(jq -r '.workflow.jobs[] | select(.id == "repo") | .status' "$repo_json")" + context_status="$(jq -r '.workflow.jobs[] | select(.id == "repo") | .context_status' "$repo_json")" + gap_count="$(jq -r '.workflow.jobs[] | select(.id == "repo") | .gap_count' "$repo_json")" + refs_count="$(jq -r '[.workflow.jobs[] | select(.id == "repo") | .steps[]? | select(.id == "read_repo") | .refs[]?] | length' "$repo_json")" + + printf '%s\tmode=%s\thome_context=%s\trepo_status=%s\trepo_context=%s\tgaps=%s\tread_refs=%s\n' \ + "$repo_path" "$MODE" "$home_context_status" "$workflow_status" "$context_status" "$gap_count" "$refs_count" +} + +print_write_summary() { + local repo_path="$1" + local repo_json="$2" + local context_yaml="$repo_path/.rabbit/context.yaml" + local gap_count="" + local manifest_count=0 + local dep_count=0 + + gap_count="$(jq -r '.workflow.jobs[] | select(.id == "repo") | .gap_count' "$repo_json")" + if [ -f "$context_yaml" ]; then + manifest_count="$(awk '/^manifests:/{flag=1;next} flag && /^[^[:space:]#]/{exit} flag && /^ - path:/{count += 1} END{print count + 0}' "$context_yaml")" + dep_count="$(awk '/^dependencies:/{flag=1;next} /^# Manifests/{if(flag) exit} flag && /^ - repo:/{count += 1} END{print count + 0}' "$context_yaml")" + fi + + printf '%s\tmode=%s\tcontext=%s\tgaps=%s\tmanifests=%s\tdependencies=%s\n' \ + "$repo_path" "$MODE" "$context_yaml" "$gap_count" "$manifest_count" "$dep_count" +} + +run_report_command() { + local label="$1" + local output_file="$2" + shift 2 + + local tmp_file="${output_file}.tmp" + rm -f "$tmp_file" + DEV_KIT_SPINNER_DISABLE=1 dev_kit_run_guarded \ + "$label" \ + "$COMMAND_SOFT_TIMEOUT" \ + "$COMMAND_HARD_TIMEOUT" \ + "$label is taking longer than usual; still resolving repo evidence" \ + "$@" >"$tmp_file" + mv "$tmp_file" "$output_file" +} + +require_jq +printf 'report_dir: %s\n' "$REPORT_DIR" +printf 'mode: %s\n' "$MODE" + for repo_path in "${repo_paths[@]}"; do repo_path="$(cd "$repo_path" 2>/dev/null && pwd || true)" [ -n "$repo_path" ] || { printf 'repo not found\n' >&2; exit 1; } [ -d "$repo_path/.git" ] || { printf 'not a git repo: %s\n' "$repo_path" >&2; exit 1; } - run_out="$(mktemp "$TEST_HOME/dev-kit-real.XXXXXX.out")" + repo_report_name="$(report_name_for_repo "$repo_path")" + home_json="$REPORT_DIR/${repo_report_name}.home.json" + repo_json="$REPORT_DIR/${repo_report_name}.repo.json" printf '\n===== %s =====\n' "$repo_path" - ( - cd "$repo_path" - if ! dev.kit >"$run_out"; then - cat "$run_out" >&2 - exit 1 - fi - dev.kit repo >/dev/null - [ -f .rabbit/context.yaml ] || { printf 'missing .rabbit/context.yaml\n' >&2; exit 1; } - - printf 'context: %s\n' "$repo_path/.rabbit/context.yaml" - printf '\nrepo:\n' - sed -n '1,20p' .rabbit/context.yaml - printf '\ngaps:\n' - awk '/^gaps:/{flag=1;next} /^# Dependencies/{if(flag) exit} flag{print}' .rabbit/context.yaml || true - printf '\ndependencies:\n' - awk '/^dependencies:/{flag=1;next} /^# Manifests/{if(flag) exit} flag{print}' .rabbit/context.yaml | sed -n '1,80p' - ) + if [ "$MODE" = "check" ]; then + ( + cd "$repo_path" + run_report_command "dev.kit home check: $repo_path" "$home_json" dev.kit --json + run_report_command "dev.kit repo check: $repo_path" "$repo_json" dev.kit repo --json --check + ) + print_check_summary "$repo_path" "$home_json" "$repo_json" + else + ( + cd "$repo_path" + run_report_command "dev.kit repo write: $repo_path" "$repo_json" dev.kit repo --json + ) + print_write_summary "$repo_path" "$repo_json" + fi done diff --git a/tests/suite.sh b/tests/suite.sh index 7e1c4fc..affd553 100644 --- a/tests/suite.sh +++ b/tests/suite.sh @@ -16,7 +16,7 @@ DOCKER_ACTION_REPO="$TEST_HOME/docker-action-repo" EMPTY_REPO="$TEST_HOME/empty-repo" IGNORED_ACTION_REPO="$TEST_HOME/ignored-action-repo" WORKFLOW_CONTRACT_REPO="$TEST_HOME/workflow-contract-repo" -AVAILABLE_TEST_GROUPS="core" +AVAILABLE_TEST_GROUPS="core repo-contract" TEST_ONLY="${DEV_KIT_TEST_ONLY:-}" cleanup() { @@ -26,10 +26,11 @@ trap cleanup EXIT usage() { cat <<'EOF' -Usage: bash tests/suite.sh [--only core] [--list] +Usage: bash tests/suite.sh [--only core|repo-contract|core,repo-contract] [--list] Groups: - core command flow, output, and context generation checks + core command flow, output, and context generation checks + repo-contract focused generated-context contract regressions EOF } @@ -51,6 +52,11 @@ should_run() { return 1 } +should_run_explicit() { + [ -n "$TEST_ONLY" ] || return 1 + should_run "$1" +} + replace_in_file() { local file_path="$1" local before="$2" @@ -118,6 +124,37 @@ done <