Usages:
  conventions: .goga/usages/conventions.md
  prance: .goga/usages/cooks/prance.md
  pyyaml: .goga/usages/cooks/pyyaml.md
  deepdiff: .goga/usages/cooks/deepdiff.md

Annotations: |
  Parsing of OpenAPI/Swagger specifications and extraction of paths and schemas
  for the traceability graph, plus structural diffing of two dereferenced specs
  and classification of endpoint/schema changes.

  The graph operates on paths only — no HTTP methods, no resource abstraction
  (architectural rule: minimal abstraction).
  Swagger 2.0 and OpenAPI 3.x are handled transparently — both store paths under paths.
  The diff sees methods/parameters/schemas for analysis only — classified changes
  surface as paths with descriptions, never as method/schema-level entries.

  Use `conventions` for code writing rules and testing.
  Use `prance` for the resolving parser and path/schema extraction.
  Use `pyyaml` for the lightweight spec-detection heuristic in discover_specs.
  Use `deepdiff` for structural diffing of two dereferenced specs.

---

"parse_spec(spec_path: pathlib.Path) -> spec: dict":
  location: parse_spec.py
  annotations: |
    Parses a specification file into a fully dereferenced dict (all non-cyclic $ref inlined).

    `spec_path`: path to a .yaml/.yml/.json spec file.
    `spec`: dereferenced specification containing paths and schemas.

    Algorithm:
    1. Read the file and parse it via Prance's formats helper.
    2. Construct a Prance reference resolver over the parsed spec (strict mode off, resolve all reference types).
    3. Configure the resolver to terminate reference cycles by emitting a $ref marker instead of raising — this prevents failure on self-referential and mutually-recursive schemas.
    4. Return the dereferenced specification.
    5. On any parser failure, raise `SpecParseError` with the path and the original reason.

    Requirements:
    - Non-cyclic $ref is resolved in memory — downstream code never resolves those references manually.
    - Cyclic $ref terminates at the cycle point with a $ref marker preserving structural information.

    Constraints:
    - RAM usage scales with spec size and reference count (known limitation, accepted).
    - The Prance post-resolve spec validator is intentionally bypassed — it rejects otherwise valid recursive schemas after dereferencing.

"extract_paths(spec: dict) -> paths: list[str]":
  location: extract_paths.py
  annotations: |
    Collects API path templates from a parsed specification, discarding HTTP methods.

    `spec`: dereferenced dict (output of parse_spec).
    `paths`: sorted list of path templates (e.g. /users, /users/{id}).

    Algorithm:
    1. Read the paths mapping from `spec`.
    2. Return its keys as a sorted list.

    Requirements:
    - Result feeds the traceability graph nodes directly — no method-level entries.

"extract_schemas(spec: dict) -> schemas: dict":
  location: extract_schemas.py
  annotations: |
    Collects schema definitions from a parsed specification for LLM context enrichment.

    `spec`: dereferenced dict.
    `schemas`: mapping of schema name to its definition (already inlined by the parser).

    Algorithm:
    1. If `spec` exposes OpenAPI 3.x components, return its schemas sub-mapping.
    2. Otherwise return the Swagger 2.0 definitions sub-mapping.

    Requirements:
    - Transparently distinguishes OpenAPI 3.x (components.schemas) from Swagger 2.0 (definitions).
    - Schemas are used only by build_refine_user_prompt — never stored in the graph.

"discover_specs(root: pathlib.Path) -> specs: list[pathlib.Path]":
  location: discover_specs.py
  annotations: |
    Enumerates spec files under `root` by extension and a lightweight content heuristic.

    `root`: directory to search (typically the local specs path from SpecsConfig).
    `specs`: sorted list of spec file paths.

    Algorithm:
    1. Walk `root` recursively.
    2. Keep files with extension .yaml/.yml/.json.
    3. Keep files whose head contains an openapi or swagger key.

    Requirements:
    - The heuristic is cheap — full parsing happens later via parse_spec.
    - Returned order is deterministic (sorted).

"SpecParseError(path: pathlib.Path, reason: str)":
  location: errors.py
  annotations: |
    Raised by parse_spec when parsing or dereferencing a specification fails.

    `path`: offending spec file path.
    `reason`: original error message from Prance.

"diff_specs(base: dict, current: dict) -> diff: DeepDiff":
  location: diff_specs.py
  annotations: |
    Computes a structural diff between two dereferenced specifications.

    `base`: baseline (local) spec dict — output of `parse_spec`.
    `current`: fresh (cloned repo) spec dict — output of `parse_spec`.
    `diff`: a DeepDiff result consumed by `classify_endpoint_changes`.

    Algorithm:
    1. Build a `DeepDiff` over `base` and `current` with ignore_order and the cutoff option from `deepdiff`.
    2. Return the result unchanged — classification happens in `classify_endpoint_changes`.

    Requirements:
    - Both inputs are fully dereferenced (Prance already inlined $ref via `parse_spec`).
    - List order is ignored — endpoint order is not significant.

    Constraints:
    - Do not classify changes here — this routine only computes the raw structural diff.
    - Avoid verbose_level=2 in the production path.

"classify_endpoint_changes(diff: DeepDiff) -> changes: EndpointDiff":
  location: classify_endpoint_changes.py
  annotations: |
    Classifies a raw structural diff into endpoint-level changes.

    `diff`: DeepDiff result from `diff_specs`.
    `changes`: an `EndpointDiff` aggregating added, removed, and modified endpoints with schema detail.

    Algorithm:
    1. Walk the diff change sets (dictionary_item_added, dictionary_item_removed, values_changed)
       by their path strings. These are the deepdiff categories produced by `diff_specs`.
    2. Paths under the paths key become endpoint changes: added / removed / modified.
    3. Paths under the components/definitions keys are schema changes — because $ref is already
       dereferenced, each affected endpoint surfaces the change directly; attribute it to that endpoint.
    4. Aggregate multiple changes on the same path into a single modified entry with a list of descriptions.
    5. Return the `EndpointDiff`.

    Requirements:
    - Swagger 2.0 (definitions) and OpenAPI 3.x (components.schemas) handled transparently.
    - Modified entries carry human-readable change descriptions for the Impact Report.

    Constraints:
    - Do not emit method-level or schema-level entries as separate paths — paths only.
    - Do not introduce endpoints outside the union of base and current path keys.

"EndpointDiff(added: list[str], removed: list[str], modified: dict[str, list[str]])":
  location: endpoint_diff.py
  annotations: |
    Classified change result between baseline and current specifications.

    `added`: endpoint paths present in the current spec but not in the baseline.
    `removed`: endpoint paths present in the baseline but not in the current spec.
    `modified`: endpoint path -> list of human-readable change descriptions.
  properties:
    "added -> list[str]": |
      Endpoint paths present in the current spec but not in the baseline.
    "removed -> list[str]": |
      Endpoint paths present in the baseline but not in the current spec.
    "modified -> dict[str, list[str]]": |
      Endpoint path -> list of human-readable change descriptions.
  methods:
    "has_changes() -> changed: bool": |
      True when any endpoint was added, removed, or modified.
    "changed_paths() -> paths: list[str]": |
      Union of added, removed, and modified endpoint paths — sorted and deduplicated.
      Deterministic; each path appears once.

---

Author: Goga
CreatedAt: 25/06/26
Description: |
  OpenAPI/Swagger parsing via Prance, extraction of paths and schemas for the
  traceability graph, and structural diffing/classification of endpoint changes.
