Загрузка данных


#!/usr/bin/env bash
set -euo pipefail

usage() {
  local script_name
  script_name=$(basename "$0")
  printf 'Usage: ./%s OPENAPI_SPEC.json\n\n' "$script_name" >&2
  cat >&2 <<'USAGE'
Print a focused phase-2 TaskTracker OpenAPI extract to stdout.

The extract includes full OpenAPI details only for the paths and schema objects
identified from the phase-1 catalog as needed for OAS operator-ticket runtime
discovery:
- root/unit create, read, find, and suit conversion paths
- space, suit, workflow, and unit-parameter discovery paths
- the known local link-create path when present
- a narrow set of unit link/relation candidate paths, because phase 1 did not
  prove the exact target link-create endpoint
- selected target objects and their transitive internal $ref dependencies

Example:
USAGE
  echo "  ./${script_name} tt-api-schema.json > tt-ticket.phase2.extract.json" >&2
}

if [[ $# -ne 1 ]]; then
  usage
  exit 2
fi

spec_file=$1

if ! command -v jq >/dev/null 2>&1; then
  echo "error: jq is required" >&2
  exit 127
fi

if [[ ! -f "$spec_file" ]]; then
  echo "error: input file not found: $spec_file" >&2
  exit 1
fi

jq '
def wanted_paths:
  [
    "/rest/api/unit/v2/{suit}/create",
    "/rest/api/unit/v2/{code}",
    "/rest/api/unit/v1/find",
    "/rest/api/unit/v1/convert/suit",
    "/rest/api/space/v2/{code}",
    "/rest/api/space/type/v1/{code}",
    "/rest/api/space/v3/{code}/suits",
    "/rest/api/space/configuration/v1/{space}/suits/find",
    "/rest/api/space/v3/manage/parameters/{unitCode}",
    "/rest/api/workflow/v1/suit/{suitCode}/space/{spaceCode}/workflow",
    "/rest/api/workflow/v1/space/type/{spaceTypeCode}/suit/{suitCode}/list",
    "/rest/api/unit/v1/link"
  ];

def wanted_schema_names:
  [
    "UnitWithAttributes",
    "UnitAttributeCodeValueInfo",
    "UserInfoWithDetails",
    "SuitWithAttributesInfo",
    "SpaceInfo",
    "SpaceTypeDetailsDto",
    "SpaceTypeConfigurationDto",
    "LinkTypes",
    "StatusResponseDto",
    "UpdateResponse"
  ];

def pointer_segments:
  sub("^#/"; "")
  | split("/")
  | map(gsub("~1"; "/") | gsub("~0"; "~"));

def internal_refs:
  [.. | objects | .["$ref"]? | strings | select(startswith("#/"))] | unique;

def deref($doc; $ref):
  if ($ref | startswith("#/")) then
    try ($doc | getpath($ref | pointer_segments)) catch null
  else
    null
  end;

def ref_closure($doc; $start):
  {seen: [], pending: ($start | unique)}
  | until((.pending | length) == 0;
      .pending as $todo
      | .seen = ((.seen + $todo) | unique)
      | ([ $todo[] as $ref | deref($doc; $ref) | select(. != null) | internal_refs[] ] | unique) as $next
      | .pending = ($next - .seen)
    )
  | .seen;

def text_blob:
  [.. | strings] | join("\n") | ascii_downcase;

def unit_link_candidate:
  (.key | ascii_downcase) as $path
  | (.value | text_blob) as $body
  | (
      ($path | test("(^|/)(unit[-_/])?links?($|/|-)|(^|/)relations?($|/|-)|parent|child"))
      and
      ($body | test("source|destination|from|to|parent|child|unit|ticket|issue|link|relation"))
    );

def wanted_schema_refs($doc):
  [
    wanted_schema_names[] as $name
    | (
        if (($doc.components.schemas // {}) | has($name)) then
          "#/components/schemas/\($name)"
        else
          empty
        end
      ),
      (
        if (($doc.definitions // {}) | has($name)) then
          "#/definitions/\($name)"
        else
          empty
        end
      )
  ] | unique;

def component_names($refs; $section):
  [
    $refs[]
    | select(startswith("#/components/\($section)/"))
    | pointer_segments[2]
  ] | unique;

def selected_component_section($doc; $refs; $section):
  (component_names($refs; $section)) as $names
  | (($doc.components[$section] // {})
      | with_entries(select(.key as $name | ($names | index($name)) != null)));

def selected_components($doc; $refs):
  reduce (($doc.components // {}) | keys_unsorted[]) as $section ({};
    (selected_component_section($doc; $refs; $section)) as $selected
    | if ($selected | length) > 0 then
        . + {($section): $selected}
      else
        .
      end
  );

def definition_names($refs):
  [
    $refs[]
    | select(startswith("#/definitions/"))
    | pointer_segments[1]
  ] | unique;

def selected_definitions($doc; $refs):
  (definition_names($refs)) as $names
  | (($doc.definitions // {})
      | with_entries(select(.key as $name | ($names | index($name)) != null)));

def top_level_ref_names($refs; $section):
  [
    $refs[]
    | select(startswith("#/\($section)/"))
    | pointer_segments[1]
  ] | unique;

def selected_top_level_ref_section($doc; $refs; $section):
  (top_level_ref_names($refs; $section)) as $names
  | (($doc[$section] // {})
      | with_entries(select(.key as $name | ($names | index($name)) != null)));

. as $spec
| (($spec.paths // {}) | type) as $paths_type
| if $paths_type != "object" then
    error("OpenAPI spec must have object-valued .paths; do not pass the phase-1 catalog as input")
  else
    (wanted_paths) as $wanted_paths
    | (($spec.paths // {})
        | with_entries(select((.key as $path | ($wanted_paths | index($path)) != null) or unit_link_candidate))
      ) as $paths
    | ([
        $wanted_paths[] as $path
        | select((($spec.paths // {}) | has($path)) | not)
        | $path
      ]) as $missing_exact_paths
    | (($paths | keys | sort) - ($wanted_paths | sort)) as $included_link_candidate_paths
    | (($paths | internal_refs) + wanted_schema_refs($spec)) as $start_refs
    | (ref_closure($spec; $start_refs)) as $refs
    | (selected_components($spec; $refs)) as $components
    | (selected_definitions($spec; $refs)) as $definitions
    | (selected_top_level_ref_section($spec; $refs; "parameters")) as $parameters
    | (selected_top_level_ref_section($spec; $refs; "responses")) as $responses
    | ([
        wanted_schema_names[] as $name
        | select(
            ((($spec.components.schemas // {}) | has($name)) or (($spec.definitions // {}) | has($name))) | not
          )
        | $name
      ]) as $missing_schema_names
    | {
        openapi: $spec.openapi,
        swagger: $spec.swagger,
        info: $spec.info,
        servers: $spec.servers,
        security: $spec.security,
        paths: $paths,
        parameters: (
          if ($parameters | length) > 0 then
            $parameters
          else
            null
          end
        ),
        responses: (
          if ($responses | length) > 0 then
            $responses
          else
            null
          end
        ),
        components: (
          if ($components | length) > 0 then
            $components
          else
            null
          end
        ),
        definitions: (
          if ($definitions | length) > 0 then
            $definitions
          else
            null
          end
        ),
        "x-phase2-filter": {
          purpose: "focused full extract for Sprint 45 OAS TaskTracker runtime-discovery spike",
          exact_path_count: ($wanted_paths | length),
          selected_path_count: ($paths | length),
          selected_component_section_count: ($components | length),
          selected_definition_count: ($definitions | length),
          selected_top_level_parameter_count: ($parameters | length),
          selected_top_level_response_count: ($responses | length),
          selected_internal_ref_count: ($refs | length),
          wanted_exact_paths: $wanted_paths,
          missing_exact_paths: $missing_exact_paths,
          included_link_candidate_paths: $included_link_candidate_paths,
          wanted_schema_names: wanted_schema_names,
          missing_schema_names: $missing_schema_names,
          note: "Full path/component bodies are included only for the exact wanted paths, selected schema names, their transitive internal refs, and narrow unit link/relation path candidates."
        }
      }
    | with_entries(select(.key == "paths" or .key == "x-phase2-filter" or .value != null))
  end
' "$spec_file"