Metadata-Version: 2.5
Name: as-engine
Version: 0.1.1
Summary: Spec-driven engine for the *-as command-line tools: enriched OpenAPI documents compiled to an operation index, a generic call surface, tag-driven transforms, a guard, and self-describing help.
Project-URL: Repository, https://github.com/grandcamel/as-engine
Author: grandcamel
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Requires-Dist: assistant-skills-lib>=1.0.0
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: hatchling; extra == 'dev'
Requires-Dist: jsonschema>=4.18; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-requests; extra == 'dev'
Description-Content-Type: text/markdown

# as-engine

The spec-driven engine behind the `*-as` command-line tools (jira-as, confluence-as).

It reads Atlassian's published OpenAPI documents as Base Documents, applies
Enrichment Entries (an OpenAPI Overlay in a small target subset) where the
documents are silent or wrong, compiles the Enriched Spec into a compact
operation index at build time, and interprets that index at runtime: a
Generic Surface that can call any operation by its published name, tag-driven
transforms (rich text, paging, prerequisite resolution), a guard that enforces
project scope, and progressive-disclosure help. Products stay thin: they hold
their Base Documents, overlays, Wrapper Verbs and configuration.

Status: bootstrap (JAS-32). The design is the spec on JAS-31 in the
grand-camel-platform tracker; the decisions live on the wayfinder map JAS-6.

## Index API

Products build their vendored OpenAPI documents with
`as_engine.build.compile_product(spec_dir, out_dir)`. The source directory
contains `manifest.json`; each document entry records its source filename,
declared API version (`info.version`), SHA-256, tier, overlay filenames, and explicitly
stripped top-level extensions. The build writes deterministic `<id>.index.json`
files and `catalog.json`. The supported input format is OpenAPI 3; local
component references are resolved without network access.

Overlay targets support dot keys, quoted bracket keys, and nonnegative bracket
indexes (including chained brackets). Updates merge objects and append arrays;
removals delete the selected element. Targets must exist; add properties by
updating their parent object. Filters, wildcards, recursive descent, slices,
and `copy` actions are refused explicitly. Metadata on actions is preserved
without enforcement; enrichment validation belongs to the product.

`compile_document(document, overlays, normalizers=...)` also accepts defect
normalization callables, applied after overlays. No vendor defects are patched
implicitly. JSON uses sorted keys, fixed separators and one trailing newline.
The returned catalog is authoritative; packaging should include its listed
indexes and catalog.json, not arbitrary older files in the output directory.

At runtime, `load_index(path)` returns an `OperationIndex` whose `operations`
map holds `Operation` records. `ProductIndexes(directory)` loads catalog entries
marked `primary` at construction; `get(document_id)` loads lower tiers on demand.

Discovery imports stay lightweight: importing the surface, output and help modules
does not load `requests`, `assistant_skills_lib` or `jsonschema`. Transform
implementations load when the registry is first used, and the HTTP stack loads
when an HTTP transport is constructed. Table output keeps the shared formatter's
simple-table and fallback behavior locally so discovery can render tables without
loading the shared package's HTTP dependencies.

## Development

**Development prerequisites:** `pip install -e '.[dev]'` includes hatchling and
build; check local wheel and source builds with `python -m build --no-isolation`.
For Base Document refreshes, install oasdiff 1.31.0: on macOS run
`curl --fail --location --retry 3 --output oasdiff.tar.gz https://github.com/oasdiff/oasdiff/releases/download/1.31.0/oasdiff_1.31.0_darwin_all.tar.gz`;
on Linux run
`curl --fail --location --retry 3 --output oasdiff.tar.gz https://github.com/oasdiff/oasdiff/releases/download/1.31.0/oasdiff_1.31.0_linux_amd64.tar.gz`.
Then run `tar -xzf oasdiff.tar.gz oasdiff` and `chmod +x oasdiff`, and place the
binary on PATH (required by the refresh acceptance test), or set
`OASDIFF=/path/to/oasdiff` for the refresh script.

```bash
pip install -e ".[dev]"
pytest
ruff check .
```

Releases are tagged `v<version>`; a published GitHub release builds the wheel,
runs the downstream suites of confluence-as and jira-as against it, and only
then publishes to PyPI. This package is never promoted on its own: a product's
pinned build carries the core version it was released with (ADR 0013 in
grand-camel-platform).

## Generic Surface

`Surface(ProductIndexes(directory), transport_factory)` provides `call`, `search`,
`describe` and `topics`. The factory receives `(document_id, OperationIndex)` and
returns a transport implementing
`call(operation: Operation, parameters: Mapping[str, Any], body: Any) -> Response`.
`Response` carries `status`, `body` and `headers`. HTTP domain exceptions reuse
assistant-skills-lib; the product can pass its existing HTTP error mapper.
The surface converts failures to `SurfaceError`, including the operation note.
It closes transports that provide `close()` after each call. A direct Python
consumer can keep an `HTTPTransport` context open for pooled sequential calls.

Parameters are checked before the factory is called: required values, primitive
and array/object types, enums and retained bounds. `kebab_case` maps canonical
operation/parameter names to flags. Body fields are JSON-typed when parseable;
`--field 'spaceId="5"'` therefore supplies a string while `--field spaceId=5`
supplies an integer. Files and stdin must contain JSON. Conflicting dotted paths
are refused rather than overwritten. `parse_call_flags` accepts repeated arrays,
JSON arrays or comma-separated arrays, explicit `true`/`false`, and `--name=value`.
If a spec parameter collides with `body`, `field`, `format`, `validate-body` or
`help`, its flag is prefixed `--parameter-`. Duplicate scalar flags are refused.

Bodies are checked only with `validate_body=True` or after a 400. The small
checker supports local references, required/properties, additionalProperties:false,
nullable, enums, primitive types, bounds, arrays and allOf/oneOf/anyOf. Unsupported
validation keywords yield explicit diagnostics; format annotations are not
assertions. It neither imports jsonschema nor fetches references. Validation
reflects the indexed schema, including upstream defects such as overlapping
oneOf branches. Schema repair belongs in a product overlay.

`HTTPTransport` serializes path/query/header/cookie parameters, pools requests,
applies timeouts, and retries explicit 429 and all 5xx responses with exponential
backoff and numeric/date Retry-After. It never retries a 409 or connection exception.
Retries on mutation responses follow the existing product policy. JSON bodies retain
their encoding; non-JSON `multipart/form-data` operations accept object fields with
`@path` file parts. Binary-tagged operations stream to atomic files, return metadata,
and allow one same-origin redirect; all cross-origin redirects are refused. Python
consumers select destinations with `Surface.call(..., output=path)`. Responder,
cassette and simulation modes cover both encodings offline; see
[the binary and multipart contract](docs/binary.md).
`Surface.call(..., all_pages=True, limit=120)` follows `x-as-paging`, returns
its merged items array, and reports `count=120` through the warning callback;
one page is the default and the operation's `parameters["limit"]` remains page
size. Prerequisite aliases resolve exact keys through the declared lookup;
version tags inject current+1 or the draft override unless a version is supplied.
Conflicting aliases/ids fail before lookup; 409 exits with conflict code 7 and
never refreshes or retries. `Responder.seed(operation_id, responses)` queues
copied bodies or `Response` objects and `requests` records calls; exhausted
seeded queues fail. See [transform hooks](docs/transforms.md) for the ordered,
per-Surface registry and extension contract.

`Responder(index, status=200, body=...)` implements the identical call interface.
An explicit body wins, including null; otherwise forced error statuses produce a
message, and successes use a media/schema example or bounded deterministic schema
generation. It does not persist state or infer a root from reachable schemas.
Without an indexed 200 schema/example it returns null. Generated values are
representative, not a guarantee that every arbitrary schema constraint is met.

`SimulationStore` and `Simulation` provide an opt-in, stateful transport double
for product wrapper tests. It keeps a detached JSON snapshot, a shared copied
call log, and only the explicitly documented Confluence operation semantics;
unknown operations fail instead of reaching a responder or HTTP. See
[docs/simulation.md](docs/simulation.md) for its seed and CQL subset.

Search and topics visit primary indexes only. Search is case-insensitive over ID,
summary, path, tags and x-as-note, with every supplied word required to match.
Deprecated operations require `include_deprecated=True`. Naming a lower-tier
operation loads that tier on demand. Describe returns a JSON-ready document
(method/path, description, parameters, body outline, response schema and extensions)
that `describe_markdown` renders. All x-as tags and vendor scope tags remain visible.
`x-as-deprecation`/`x-as-deprecated` replacement metadata overrides the standard
OpenAPI deprecated flag; `x-as-topic` accepts a topic string or list.

### Additive index metadata

Old records load unchanged. New projections add fields only when source metadata
exists: `response_schema` for an inline 200 schema (the original `response_200`
remains the named reference or null), `response_example` for a media example,
`deprecated`, `request_body_required`, and `request_media_types`. Parameters retain
explicit `style`/`explode`. The original requestBody representation is unchanged.
These runtime additions require regenerating the product's packaged indexes through
its build hook; there is no runtime Base Document fallback.

See [exit codes](docs/exit-codes.md) for the machine-readable failure contract.

Progressive help is provided by `as_engine.help`: pure document builders for a
product template, group/topic pages, operation details and existing enrichment
examples, rendered consistently as Markdown or JSON. Descriptions support
`full=True`; topic membership uses only `x-as-topic`, and `x-as-risk` is exposed
for the product CLI's confirmation policy. See [docs/help.md](docs/help.md) for
the tag shapes, character-based token caps and golden regeneration command.

Rich-text tags now connect the shared converters to the Surface: tagged Markdown
fields become storage or ADF, and tagged responses render as Markdown with lossless
placeholders while retaining metadata. `representation=` chooses a declared write
representation and `raw=True` returns stored response data; neither option changes
vendor query parameters. `build_body(..., operation=operation)` preserves tagged
Markdown fields and reads UTF-8 `@file` values. Date/duration tags use the scalar
parser registry. See [rich text](docs/richtext.md) for encoding, lookup isolation,
CLI integration and the optional final-body validation contract.

Jira consumers can use exact OpenAPI parameter flags alongside kebab aliases.
Paging also handles POST body continuation fields, optional token last-page
signals, and decimal offsets whose published parameter schema is a string;
the responder also decodes serialized JSON examples when the response schema
declares a matching object or array. These additions preserve the existing
Confluence call and paging contracts.
