Implement v2 graph API surface and contract/test coverage
This commit is contained in:
parent
d1e82e71f9
commit
b8c0a6e6d0
|
|
@ -327,6 +327,16 @@ add_test(NAME amduatd_fed_ingest
|
||||||
)
|
)
|
||||||
set_tests_properties(amduatd_fed_ingest PROPERTIES SKIP_RETURN_CODE 77)
|
set_tests_properties(amduatd_fed_ingest PROPERTIES SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
|
add_test(NAME amduatd_graph_queries
|
||||||
|
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_queries.sh
|
||||||
|
)
|
||||||
|
set_tests_properties(amduatd_graph_queries PROPERTIES SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
|
add_test(NAME amduatd_graph_contract
|
||||||
|
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_contract.sh
|
||||||
|
)
|
||||||
|
set_tests_properties(amduatd_graph_contract PROPERTIES SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
add_executable(amduatd_test_space_doctor
|
add_executable(amduatd_test_space_doctor
|
||||||
tests/test_amduatd_space_doctor.c
|
tests/test_amduatd_space_doctor.c
|
||||||
src/amduatd_space_doctor.c
|
src/amduatd_space_doctor.c
|
||||||
|
|
|
||||||
101
README.md
101
README.md
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
`amduat-api` builds `amduatd`, a minimal HTTP server over a Unix domain socket that exposes Amduat substrate operations for a **single ASL store root**.
|
`amduat-api` builds `amduatd`, a minimal HTTP server over a Unix domain socket that exposes Amduat substrate operations for a **single ASL store root**.
|
||||||
|
|
||||||
|
## App Developer Handoff
|
||||||
|
|
||||||
|
For a compact, implementation-focused guide for external app teams, use:
|
||||||
|
|
||||||
|
- `docs/v2-app-developer-guide.md`
|
||||||
|
- `registry/amduatd-api-contract.v2.json` (machine-readable contract)
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
@ -444,6 +451,57 @@ curl --unix-socket amduatd.sock -X POST http://localhost/v1/pel/run \
|
||||||
-d '{"program_ref":"<program_ref>","input_refs":["<input_ref_0>"],"params_ref":"<params_ref>"}'
|
-d '{"program_ref":"<program_ref>","input_refs":["<input_ref_0>"],"params_ref":"<params_ref>"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run a v2 PEL execute request with inline artifact ingest:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --unix-socket amduatd.sock -X POST http://localhost/v2/pel/execute \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"scheme_ref":"dag",
|
||||||
|
"program_ref":"<program_ref>",
|
||||||
|
"inputs":{
|
||||||
|
"refs":["<input_ref_0>"],
|
||||||
|
"inline_artifacts":[{"body_hex":"48656c6c6f2c20763221","type_tag":"0x00000000"}]
|
||||||
|
},
|
||||||
|
"receipt":{
|
||||||
|
"input_manifest_ref":"<manifest_ref>",
|
||||||
|
"environment_ref":"<env_ref>",
|
||||||
|
"evaluator_id":"local-amduatd",
|
||||||
|
"executor_ref":"<executor_ref>",
|
||||||
|
"started_at":1731000000,
|
||||||
|
"completed_at":1731000001
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The v2 execute response returns `run_ref` (and `result_ref` alias),
|
||||||
|
`receipt_ref`, `stored_input_refs[]`, `output_refs[]`, and `status`.
|
||||||
|
|
||||||
|
Simplified async v2 operations (PEL-backed under the hood):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# put (returns job_id)
|
||||||
|
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/put \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"body_hex":"48656c6c6f","type_tag":"0x00000000"}'
|
||||||
|
|
||||||
|
# concat (returns job_id)
|
||||||
|
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/concat \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"left_ref":"<ref_a>","right_ref":"<ref_b>"}'
|
||||||
|
|
||||||
|
# slice (returns job_id)
|
||||||
|
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/slice \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"ref":"<ref>","offset":1,"length":3}'
|
||||||
|
|
||||||
|
# poll job
|
||||||
|
curl --unix-socket amduatd.sock http://localhost/v2/jobs/1
|
||||||
|
|
||||||
|
# get bytes
|
||||||
|
curl --unix-socket amduatd.sock http://localhost/v2/get/<ref>
|
||||||
|
```
|
||||||
|
|
||||||
When derivation indexing is enabled, successful PEL runs record derivations under
|
When derivation indexing is enabled, successful PEL runs record derivations under
|
||||||
`<root>/index/derivations/by_artifact/` keyed by output refs (plus result/trace/receipt refs).
|
`<root>/index/derivations/by_artifact/` keyed by output refs (plus result/trace/receipt refs).
|
||||||
|
|
||||||
|
|
@ -560,6 +618,49 @@ When the daemon uses the `fs` store backend, index-only checks are reported as
|
||||||
- request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (`program_ref`/`input_refs`/`params_ref` accept hex refs or concept names; omit `scheme_ref` to use `dag`)
|
- request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (`program_ref`/`input_refs`/`params_ref` accept hex refs or concept names; omit `scheme_ref` to use `dag`)
|
||||||
- request receipt (optional): `{receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at, sbom_ref?, parity_digest_hex?, executor_fingerprint_ref?, run_id_hex?, limits?, logs?, determinism_level?, rng_seed_hex?, signature_hex?}}`
|
- request receipt (optional): `{receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at, sbom_ref?, parity_digest_hex?, executor_fingerprint_ref?, run_id_hex?, limits?, logs?, determinism_level?, rng_seed_hex?, signature_hex?}}`
|
||||||
- response: `{result_ref, trace_ref?, receipt_ref?, output_refs[], status}`
|
- response: `{result_ref, trace_ref?, receipt_ref?, output_refs[], status}`
|
||||||
|
- `POST /v2/pel/execute` (first v2 slice)
|
||||||
|
- request: `{program_ref, scheme_ref?, params_ref?, inputs:{refs[], inline_artifacts:[{body_hex, type_tag?}]}, receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at}}`
|
||||||
|
- response: `{run_ref, result_ref, trace_ref?, receipt_ref, stored_input_refs[], output_refs[], status}`
|
||||||
|
- `POST /v2/ops/put` → async enqueue `{job_id, status}`
|
||||||
|
- request: `{body_hex, type_tag?}`
|
||||||
|
- `POST /v2/ops/concat` → async enqueue `{job_id, status}`
|
||||||
|
- request: `{left_ref, right_ref}` (refs or concept names)
|
||||||
|
- `POST /v2/ops/slice` → async enqueue `{job_id, status}`
|
||||||
|
- request: `{ref, offset, length}` (`ref` accepts ref or concept name)
|
||||||
|
- `GET /v2/jobs/{id}` → `{job_id, kind, status, created_at_ms, started_at_ms, completed_at_ms, result_ref, error}`
|
||||||
|
- `GET /v2/get/{ref}` → artifact bytes (alias to `/v1/artifacts/{ref}`)
|
||||||
|
- `GET /v2/healthz` → liveness probe `{ok, status, time_ms}`
|
||||||
|
- `GET /v2/readyz` → readiness probe `{ok, status, components:{graph_index, federation}}` (`503` when not ready)
|
||||||
|
- `GET /v2/metrics` → Prometheus text metrics (`text/plain; version=0.0.4`)
|
||||||
|
- `POST /v2/graph/nodes` → create/seed graph node via concept `{name, ref?}`
|
||||||
|
- `POST /v2/graph/nodes/{name}/versions` → publish node version `{ref, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive)
|
||||||
|
- `POST /v2/graph/nodes/{name}/versions/tombstone` → tombstone a previously published node version `{ref, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive; append-only)
|
||||||
|
- `GET /v2/graph/nodes/{name}/versions?as_of=&include_tombstoned=` → get node versions (same shape as node read; defaults hide tombstoned versions from `latest_ref` and `versions[]`; set `include_tombstoned=true` to include them)
|
||||||
|
- `GET /v2/graph/nodes/{name}/neighbors?dir=&predicate=&limit=&cursor=&as_of=&provenance_ref=&include_tombstoned=&expand_names=&expand_artifacts=` → neighbor scan (paged, snapshot-bounded; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; add names/latest refs via expansion flags)
|
||||||
|
- `GET /v2/graph/search?name_prefix=&limit=&cursor=&as_of=` → search nodes by name prefix (paged, snapshot-bounded)
|
||||||
|
- `GET /v2/graph/paths?from=&to=&max_depth=&predicate=&as_of=&k=&expand_names=&expand_artifacts=&include_tombstoned=&provenance_ref=&max_fanout=&max_result_bytes=&include_stats=` → shortest directed path query (bounded BFS, snapshot-bounded, returns up to `k` paths with hop metadata and optional names/latest refs; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; supports fanout/response-size guards and optional stats)
|
||||||
|
- `GET /v2/graph/subgraph?roots[]=&max_depth=&max_fanout=&predicates[]=&dir=&as_of=&include_versions=&include_tombstoned=&limit_nodes=&limit_edges=&cursor=&provenance_ref=&max_result_bytes=&include_stats=` → bounded multi-hop subgraph retrieval from roots (snapshot-bounded, paged by opaque `next_cursor`; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; supports fanout/response-size guards and optional stats; returns `{nodes[], edges[], frontier?[], next_cursor, truncated}`)
|
||||||
|
- `POST /v2/graph/edges` → append graph edge `{subject, predicate, object, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive)
|
||||||
|
- `POST /v2/graph/edges/tombstone` → append tombstone for an existing edge `{edge_ref, metadata_ref?|provenance?}` (append-only correction/retraction; `metadata_ref` and `provenance` are mutually exclusive)
|
||||||
|
- `POST /v2/graph/batch` → apply mixed graph mutations `{idempotency_key?, mode?, nodes?, versions?, edges?}` (versions/edges items support `metadata_ref?|provenance?`; mode: `fail_fast|continue_on_error`; deterministic replay for repeated `idempotency_key` + identical payload; returns `{ok, applied, results[]}` with per-item status/error)
|
||||||
|
- `POST /v2/graph/query` → unified graph query `{where?, predicates?, direction?, include_versions?, include_tombstoned?, include_stats?, max_result_bytes?, as_of?, limit?, cursor?}` returning `{nodes[], edges[], paging, stats?}` (`nodes[].versions[]` included when `include_versions=true`; `where.provenance_ref` filters edges linked via `ms.has_provenance`; tombstoned edges are excluded as-of snapshot unless `include_tombstoned=true`)
|
||||||
|
- `POST /v2/graph/retrieve` → agent-oriented bounded retrieval `{roots[], goal_predicates?[], max_depth?, as_of?, provenance_min_confidence?, include_versions?, include_tombstoned?, max_fanout?, limit_nodes?, limit_edges?, max_result_bytes?}` returning `{nodes[], edges[], explanations[], truncated, stats}` (multi-hop traversal from roots with optional predicate targeting and provenance-confidence gating; each explanation includes edge inclusion reasons and traversal depth)
|
||||||
|
- `POST /v2/graph/export` → paged graph envelope export `{as_of?, cursor?, limit?, predicates?[], roots?[], include_tombstoned?, max_result_bytes?}` returning `{items[], next_cursor, has_more, snapshot_as_of, stats}` (`items[]` preserve edge refs/order plus predicate, tombstone flag, and attached `metadata_ref` when present)
|
||||||
|
- `POST /v2/graph/import` → ordered graph envelope replay `{mode?, items[]}` (`mode=fail_fast|continue_on_error`) returning `{ok, applied, results[]}` with per-item `index/status/code/error/edge_ref`
|
||||||
|
- `GET /v2/graph/schema/predicates` → current graph governance policy `{mode, provenance_mode, predicates[]}` where `mode` is `strict|warn|off` and `provenance_mode` is `optional|required`
|
||||||
|
- `POST /v2/graph/schema/predicates` → update graph governance `{mode?, provenance_mode?, predicates?[]}` (`predicates[]` entries accept `{predicate_ref|predicate, domain?, range?}`; predicate validation mode is enforced for edge writes in `POST /v2/graph/edges`, batch edge items, and graph import edge items; `provenance_mode=required` enforces provenance attachment (`metadata_ref` or `provenance`) for version/edge/tombstone writes; policy is persisted per space root)
|
||||||
|
- `GET /v2/graph/stats` → graph/index health summary `{edges_total, aliases_total, index{...}, tombstones{edges, ratio}}`
|
||||||
|
- `GET /v2/graph/capabilities` → graph feature/version negotiation payload `{contract, graph{version, features, limits, modes}, runtime{...}}`
|
||||||
|
- `GET /v2/graph/changes?since_cursor=&since_as_of=&limit=&wait_ms=&event_types[]=&predicates[]=&roots[]=` → incremental appended graph events with strict cursor replay window (returns `410` when cursor falls outside retained window), resumable cursor tokens, optional long-poll (`wait_ms`), and event/predicate/root filters; response `{events[], next_cursor, has_more}`
|
||||||
|
- `GET /v2/graph/edges?subject=&predicate=&object=&dir=&limit=&cursor=&as_of=&expand_names=&expand_artifacts=&provenance_ref=&include_tombstoned=&max_result_bytes=&include_stats=` → filtered edge scan with paging (`dir=any|outgoing|incoming`, snapshot-bounded; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; add names/latest refs via expansion flags; supports response-size guards and optional stats)
|
||||||
|
- graph paging cursors are opaque tokens (`g1_*`); legacy numeric cursors are still accepted
|
||||||
|
- `GET /v2/graph/nodes/{name}?as_of=&include_tombstoned=` → `{name, concept_ref, latest_ref, versions[], outgoing[], incoming[]}` (`latest_ref` is resolved from visible versions at `as_of`; tombstoned versions excluded by default)
|
||||||
|
- `GET /v2/graph/history/{name}?as_of=&include_tombstoned=` → event timeline for node versions/edges (snapshot-bounded; `latest_ref` and tombstoned fact events both respect same visibility rules)
|
||||||
|
|
||||||
|
Graph client helper examples for agent flows are in `scripts/graph_client_helpers.sh`:
|
||||||
|
- `batch-ingest` (idempotent batch write helper)
|
||||||
|
- `sync-once` (incremental changes cursor step)
|
||||||
|
- `subgraph` (bounded retrieval helper)
|
||||||
- `POST /v1/pel/programs`
|
- `POST /v1/pel/programs`
|
||||||
- request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes)
|
- request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes)
|
||||||
- response: `{program_ref}`
|
- response: `{program_ref}`
|
||||||
|
|
|
||||||
196
docs/amduatd-api-v2-design.md
Normal file
196
docs/amduatd-api-v2-design.md
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
# amduatd API v2 Design (PEL-only writes)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Draft proposal for discussion.
|
||||||
|
First server slice implemented: `POST /v2/pel/execute` with `inputs.refs`,
|
||||||
|
`inputs.inline_artifacts[{body_hex,type_tag?}]`, and required `receipt`.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Keep `amduatd` thin: transport, parsing, auth, and mapping to core calls.
|
||||||
|
- Make **PEL the only write primitive** in HTTP v2.
|
||||||
|
- Enforce artifact provenance for writes: adding new artifacts must go through:
|
||||||
|
1. store inputs,
|
||||||
|
2. run a PEL program,
|
||||||
|
3. persist and return receipt.
|
||||||
|
- Keep `/v1/*` behavior unchanged while v2 is introduced.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Re-implementing PEL semantics in `amduat-api`.
|
||||||
|
- Creating new substrate semantics in daemon code.
|
||||||
|
- Removing v1 immediately.
|
||||||
|
|
||||||
|
## High-level model
|
||||||
|
|
||||||
|
v2 separates read and write concerns:
|
||||||
|
|
||||||
|
- Read paths remain direct and deterministic (`GET /v2/meta`, `GET /v2/artifacts/{ref}`, federation/space read endpoints).
|
||||||
|
- All state-changing operations are executed as PEL runs.
|
||||||
|
|
||||||
|
In v2, `POST /v1/artifacts` equivalent is replaced by a mutation endpoint that always drives a PEL run.
|
||||||
|
|
||||||
|
For simplified clients, v2 also exposes async operation endpoints:
|
||||||
|
|
||||||
|
- `POST /v2/ops/put`
|
||||||
|
- `POST /v2/ops/concat`
|
||||||
|
- `POST /v2/ops/slice`
|
||||||
|
- `GET /v2/jobs/{id}`
|
||||||
|
|
||||||
|
These enqueue work and execute PEL in the daemon background loop.
|
||||||
|
|
||||||
|
Graph-oriented endpoints can be added as thin wrappers over concepts/relations:
|
||||||
|
|
||||||
|
- `POST /v2/graph/nodes` (maps to concept create/publish)
|
||||||
|
- `POST /v2/graph/edges` (maps to relation edge append)
|
||||||
|
- `GET /v2/graph/nodes/{name}` (node + versions + incoming/outgoing edges)
|
||||||
|
- `GET /v2/graph/history/{name}` (version + edge timeline)
|
||||||
|
|
||||||
|
## Versioning and contract id
|
||||||
|
|
||||||
|
- Base path: `/v2`
|
||||||
|
- Contract id: `AMDUATD/API/2`
|
||||||
|
- Keep the same store-seeded contract model used in v1.
|
||||||
|
|
||||||
|
## Endpoint shape
|
||||||
|
|
||||||
|
### Read-only endpoints (v2)
|
||||||
|
|
||||||
|
- `GET /v2/meta`
|
||||||
|
- `GET /v2/contract`
|
||||||
|
- `GET /v2/artifacts/{ref}`
|
||||||
|
- `HEAD /v2/artifacts/{ref}`
|
||||||
|
- `GET /v2/artifacts/{ref}?format=info`
|
||||||
|
- Space/federation read endpoints can be mirrored under `/v2/*` unchanged where possible.
|
||||||
|
|
||||||
|
### Write endpoints (v2)
|
||||||
|
|
||||||
|
- `POST /v2/pel/execute`
|
||||||
|
|
||||||
|
No direct artifact write endpoint in v2 (`POST /v2/artifacts` is intentionally absent).
|
||||||
|
|
||||||
|
## `POST /v2/pel/execute`
|
||||||
|
|
||||||
|
Single mutation primitive for v2.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"program_ref": "<hex-ref-or-name>",
|
||||||
|
"scheme_ref": "dag",
|
||||||
|
"params_ref": "<optional-ref-or-name>",
|
||||||
|
"inputs": {
|
||||||
|
"refs": ["<hex-ref-or-name>"],
|
||||||
|
"inline_artifacts": [
|
||||||
|
{
|
||||||
|
"content_type": "application/octet-stream",
|
||||||
|
"type_tag": "0x00000000",
|
||||||
|
"body_hex": "48656c6c6f"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receipt": {
|
||||||
|
"input_manifest_ref": "<ref-or-name>",
|
||||||
|
"environment_ref": "<ref-or-name>",
|
||||||
|
"evaluator_id": "local-amduatd",
|
||||||
|
"executor_ref": "<ref-or-name>",
|
||||||
|
"started_at": 1731000000,
|
||||||
|
"completed_at": 1731000005
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"publish_outputs": true,
|
||||||
|
"append_fed_log": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Processing contract
|
||||||
|
|
||||||
|
Server behavior is deterministic and ordered:
|
||||||
|
|
||||||
|
1. Resolve `program_ref`, `scheme_ref`, `params_ref`, and `inputs.refs` to refs.
|
||||||
|
2. Decode and store `inputs.inline_artifacts` into ASL store.
|
||||||
|
3. Build effective `input_refs = refs + stored_inline_refs`.
|
||||||
|
4. Execute PEL using core `pel_surf_run_with_result`.
|
||||||
|
5. Build FER receipt from run result + receipt fields.
|
||||||
|
6. Store receipt artifact and return `receipt_ref`.
|
||||||
|
7. Apply requested side effects (`publish_outputs`, `append_fed_log`) after successful run.
|
||||||
|
|
||||||
|
If any step fails:
|
||||||
|
|
||||||
|
- return non-2xx,
|
||||||
|
- do not publish outputs,
|
||||||
|
- do not append federation publish records.
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"run_ref": "<pel1-result-ref>",
|
||||||
|
"trace_ref": "<optional-trace-ref>",
|
||||||
|
"receipt_ref": "<fer1-receipt-ref>",
|
||||||
|
"stored_input_refs": ["<ref>"],
|
||||||
|
"output_refs": ["<ref>"],
|
||||||
|
"status": "OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `stored_input_refs` are refs created from `inline_artifacts`.
|
||||||
|
- `receipt_ref` is required for successful writes in v2.
|
||||||
|
|
||||||
|
## Error model
|
||||||
|
|
||||||
|
- `400` invalid request or schema violation.
|
||||||
|
- `404` referenced input/program/params not found.
|
||||||
|
- `409` conflict in post-run side effects (e.g. cursor/update conflicts).
|
||||||
|
- `422` run completed but business policy failed (reserved for policy checks).
|
||||||
|
- `500` internal failure.
|
||||||
|
|
||||||
|
Error payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "invalid_input_ref",
|
||||||
|
"message": "input ref not found",
|
||||||
|
"retryable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavioral differences from v1
|
||||||
|
|
||||||
|
- Removed write path: direct `POST /artifacts`.
|
||||||
|
- New requirement: every write returns a receipt ref tied to a PEL run.
|
||||||
|
- `pel/run` semantics become the write gateway rather than an optional compute endpoint.
|
||||||
|
|
||||||
|
## Compatibility and rollout
|
||||||
|
|
||||||
|
1. Add `/v2/pel/execute` while keeping `/v1/*` unchanged.
|
||||||
|
2. Mirror core read endpoints under `/v2/*`.
|
||||||
|
3. Mark `POST /v1/artifacts` as deprecated in docs.
|
||||||
|
4. Migrate clients to v2 execute flow.
|
||||||
|
5. Remove v1 write endpoints in a later major release.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should v2 require exactly one output for write operations, or allow multiple outputs with one receipt?
|
||||||
|
- Should inline artifact body support both `body_hex` and `body_base64`, or move to multipart for large payloads?
|
||||||
|
- Should `publish_outputs` default to `true` or be explicit-only?
|
||||||
|
- Do we need async execution (`202 + run status endpoint`) for long-running programs in v2?
|
||||||
|
|
||||||
|
## Minimal implementation plan
|
||||||
|
|
||||||
|
1. Add v2 contract bytes (`registry/amduatd-api-contract.v2.json`).
|
||||||
|
2. Add `/v2/pel/execute` handler by adapting existing `POST /v1/pel/run` + artifact ingest path.
|
||||||
|
3. Factor shared receipt parsing/building from current v1 pel handler.
|
||||||
|
4. Add tests:
|
||||||
|
- inline artifact -> stored -> run -> receipt success,
|
||||||
|
- failure before run,
|
||||||
|
- failure during run,
|
||||||
|
- side effect gating (no publish on failure).
|
||||||
|
5. Document deprecation notice for `POST /v1/artifacts`.
|
||||||
237
docs/v2-app-developer-guide.md
Normal file
237
docs/v2-app-developer-guide.md
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
# Amduat v2 App Developer Guide
|
||||||
|
|
||||||
|
This is the compact handoff guide for building an application against `amduatd` v2.
|
||||||
|
|
||||||
|
For machine-readable contracts, see `registry/amduatd-api-contract.v2.json`.
|
||||||
|
|
||||||
|
## 1) Runtime Model
|
||||||
|
|
||||||
|
- One daemon instance serves one ASL store root.
|
||||||
|
- Transport is local Unix socket HTTP.
|
||||||
|
- Auth is currently filesystem/socket permission based.
|
||||||
|
- All graph APIs are under `/v2/graph/*`.
|
||||||
|
|
||||||
|
Minimal local run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./vendor/amduat/build/amduat-asl init --root .amduat-asl
|
||||||
|
./build/amduatd --root .amduat-asl --sock amduatd.sock --store-backend index
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) Request Conventions
|
||||||
|
|
||||||
|
- Use `X-Amduat-Space: <space_id>` for app isolation.
|
||||||
|
- Treat graph cursors as opaque tokens (`g1_*`), do not parse.
|
||||||
|
- Use `as_of` for snapshot-consistent reads.
|
||||||
|
- Use `include_tombstoned=true` only when you explicitly want retracted facts.
|
||||||
|
|
||||||
|
## 3) Core App Flows
|
||||||
|
|
||||||
|
### A. High-throughput ingest
|
||||||
|
|
||||||
|
Use `POST /v2/graph/batch` with:
|
||||||
|
|
||||||
|
- `idempotency_key` for deterministic retries
|
||||||
|
- `mode=continue_on_error` for partial-apply behavior
|
||||||
|
- per-item `metadata_ref` or `provenance` for trust/debug
|
||||||
|
|
||||||
|
Expect response:
|
||||||
|
|
||||||
|
- `ok` (overall)
|
||||||
|
- `applied` aggregate counts
|
||||||
|
- `results[]` with `{kind,index,status,code,error}`
|
||||||
|
|
||||||
|
### B. Multi-hop retrieval for agents
|
||||||
|
|
||||||
|
Primary endpoints:
|
||||||
|
|
||||||
|
- `GET /v2/graph/subgraph`
|
||||||
|
- `POST /v2/graph/retrieve`
|
||||||
|
- `POST /v2/graph/query` for declarative filtering
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
- `as_of` for stable reasoning snapshots
|
||||||
|
- `max_depth`, `max_fanout`, `limit_nodes`, `limit_edges`, `max_result_bytes`
|
||||||
|
- provenance filters where needed (`provenance_ref` / `provenance_min_confidence`)
|
||||||
|
|
||||||
|
### C. Incremental sync loop
|
||||||
|
|
||||||
|
Use `GET /v2/graph/changes`:
|
||||||
|
|
||||||
|
- Start with `since_cursor` (or bootstrap with `since_as_of`)
|
||||||
|
- Persist returned `next_cursor` after successful processing
|
||||||
|
- Handle `410` as replay-window expiry (full resync required)
|
||||||
|
- Optional long poll: `wait_ms`
|
||||||
|
|
||||||
|
### D. Fact correction
|
||||||
|
|
||||||
|
- Edge retraction: `POST /v2/graph/edges/tombstone`
|
||||||
|
- Node-version retraction: `POST /v2/graph/nodes/{name}/versions/tombstone`
|
||||||
|
|
||||||
|
Reads default to exclude tombstoned facts on retrieval surfaces unless `include_tombstoned=true`.
|
||||||
|
|
||||||
|
## 4) Endpoint Map (what to use when)
|
||||||
|
|
||||||
|
- Write node: `POST /v2/graph/nodes`
|
||||||
|
- Write version: `POST /v2/graph/nodes/{name}/versions`
|
||||||
|
- Write edge: `POST /v2/graph/edges`
|
||||||
|
- Batch write: `POST /v2/graph/batch`
|
||||||
|
- Point-ish read: `GET /v2/graph/nodes/{name}`
|
||||||
|
- Edge scan: `GET /v2/graph/edges`
|
||||||
|
- Neighbor scan: `GET /v2/graph/nodes/{name}/neighbors`
|
||||||
|
- Path lookup: `GET /v2/graph/paths`
|
||||||
|
- Subgraph: `GET /v2/graph/subgraph`
|
||||||
|
- Declarative query: `POST /v2/graph/query`
|
||||||
|
- Agent retrieval: `POST /v2/graph/retrieve`
|
||||||
|
- Changes feed: `GET /v2/graph/changes`
|
||||||
|
- Export: `POST /v2/graph/export`
|
||||||
|
- Import: `POST /v2/graph/import`
|
||||||
|
- Predicate policy: `GET/POST /v2/graph/schema/predicates`
|
||||||
|
- Health/readiness/metrics: `GET /v2/healthz`, `GET /v2/readyz`, `GET /v2/metrics`
|
||||||
|
- Graph runtime/capability: `GET /v2/graph/stats`, `GET /v2/graph/capabilities`
|
||||||
|
|
||||||
|
## 5) Provenance and Policy
|
||||||
|
|
||||||
|
Provenance object fields for writes:
|
||||||
|
|
||||||
|
- required: `source_uri`, `extractor`, `observed_at`, `ingested_at`, `trace_id`
|
||||||
|
- optional: `confidence`, `license`
|
||||||
|
|
||||||
|
Policy endpoint:
|
||||||
|
|
||||||
|
- `POST /v2/graph/schema/predicates`
|
||||||
|
|
||||||
|
Key modes:
|
||||||
|
|
||||||
|
- predicate validation: `strict|warn|off`
|
||||||
|
- provenance enforcement: `optional|required`
|
||||||
|
|
||||||
|
## 6) Error Handling and Retry Rules
|
||||||
|
|
||||||
|
- Retry-safe writes: only retries with same `idempotency_key` and identical payload.
|
||||||
|
- Validation failures: `400` or `422` (do not blind-retry).
|
||||||
|
- Not found for references/nodes: `404`.
|
||||||
|
- Cursor window expired: `410` on `/changes` (rebootstrap sync state).
|
||||||
|
- Result guard triggered: `422` (`max_result_bytes` or traversal/search limits).
|
||||||
|
- Internal errors: `500` (retry with backoff).
|
||||||
|
|
||||||
|
## 7) Performance and Safety Defaults
|
||||||
|
|
||||||
|
Recommended client defaults:
|
||||||
|
|
||||||
|
- Set explicit `limit` on scans.
|
||||||
|
- Always pass `max_result_bytes` on large retrieval requests.
|
||||||
|
- Keep `max_depth` conservative (start with 2-4).
|
||||||
|
- Enable `include_stats=true` in development to monitor scanned/returned counts and selected plan.
|
||||||
|
- Call `/v2/graph/capabilities` once at startup for feature/limit negotiation.
|
||||||
|
|
||||||
|
## 8) Minimal Startup Checklist (for external app)
|
||||||
|
|
||||||
|
1. Probe `GET /v2/readyz`.
|
||||||
|
2. Read `GET /v2/graph/capabilities`.
|
||||||
|
3. Configure schema/provenance policy (`POST /v2/graph/schema/predicates`) if your app owns policy.
|
||||||
|
4. Start ingest path (`/v2/graph/batch` idempotent).
|
||||||
|
5. Start change-consumer loop (`/v2/graph/changes`).
|
||||||
|
6. Serve retrieval via `/v2/graph/retrieve` and `/v2/graph/subgraph`.
|
||||||
|
7. Monitor `/v2/metrics` and `/v2/graph/stats`.
|
||||||
|
|
||||||
|
## 9) Useful Local Helpers
|
||||||
|
|
||||||
|
- `scripts/graph_client_helpers.sh` contains practical shell helpers for:
|
||||||
|
- idempotent batch ingest
|
||||||
|
- one-step changes sync
|
||||||
|
- subgraph retrieval
|
||||||
|
|
||||||
|
For integration tests/examples:
|
||||||
|
|
||||||
|
- `scripts/test_graph_queries.sh`
|
||||||
|
- `scripts/test_graph_contract.sh`
|
||||||
|
|
||||||
|
## 10) Copy/Paste Integration Skeleton
|
||||||
|
|
||||||
|
Set local defaults:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
SOCK="amduatd.sock"
|
||||||
|
SPACE="app1"
|
||||||
|
BASE="http://localhost"
|
||||||
|
```
|
||||||
|
|
||||||
|
Startup probes:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --unix-socket "${SOCK}" -sS "${BASE}/v2/readyz"
|
||||||
|
curl --unix-socket "${SOCK}" -sS "${BASE}/v2/graph/capabilities"
|
||||||
|
```
|
||||||
|
|
||||||
|
Idempotent batch ingest:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --unix-socket "${SOCK}" -sS -X POST "${BASE}/v2/graph/batch" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Amduat-Space: ${SPACE}" \
|
||||||
|
-d '{
|
||||||
|
"idempotency_key":"app1-batch-0001",
|
||||||
|
"mode":"continue_on_error",
|
||||||
|
"nodes":[{"name":"doc:1"}],
|
||||||
|
"edges":[
|
||||||
|
{"subject":"doc:1","predicate":"ms.within_domain","object":"topic:alpha",
|
||||||
|
"provenance":{"source_uri":"urn:app:seed","extractor":"app-loader","observed_at":1,"ingested_at":2,"trace_id":"trace-1"}}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Incremental changes loop (bash skeleton):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cursor=""
|
||||||
|
while true; do
|
||||||
|
if [ -n "${cursor}" ]; then
|
||||||
|
path="/v2/graph/changes?since_cursor=${cursor}&limit=200&wait_ms=15000"
|
||||||
|
else
|
||||||
|
path="/v2/graph/changes?limit=200&wait_ms=15000"
|
||||||
|
fi
|
||||||
|
|
||||||
|
resp="$(curl --unix-socket "${SOCK}" -sS "${BASE}${path}" -H "X-Amduat-Space: ${SPACE}")" || break
|
||||||
|
|
||||||
|
# TODO: parse and process resp.events[] in your app.
|
||||||
|
next="$(printf '%s\n' "${resp}" | sed -n 's/.*"next_cursor":"\([^"]*\)".*/\1/p')"
|
||||||
|
[ -n "${next}" ] && cursor="${next}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent retrieval call:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --unix-socket "${SOCK}" -sS -X POST "${BASE}/v2/graph/retrieve" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Amduat-Space: ${SPACE}" \
|
||||||
|
-d '{
|
||||||
|
"roots":["doc:1"],
|
||||||
|
"goal_predicates":["ms.within_domain"],
|
||||||
|
"max_depth":2,
|
||||||
|
"max_fanout":1024,
|
||||||
|
"limit_nodes":200,
|
||||||
|
"limit_edges":400,
|
||||||
|
"max_result_bytes":1048576
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Subgraph snapshot read:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --unix-socket "${SOCK}" -sS \
|
||||||
|
"${BASE}/v2/graph/subgraph?roots[]=doc:1&max_depth=2&dir=outgoing&limit_nodes=200&limit_edges=400&include_stats=true&max_result_bytes=1048576" \
|
||||||
|
-H "X-Amduat-Space: ${SPACE}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Edge correction (tombstone):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
EDGE_REF="<edge_ref_to_retract>"
|
||||||
|
curl --unix-socket "${SOCK}" -sS -X POST "${BASE}/v2/graph/edges/tombstone" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Amduat-Space: ${SPACE}" \
|
||||||
|
-d "{\"edge_ref\":\"${EDGE_REF}\"}"
|
||||||
|
```
|
||||||
|
|
@ -17,6 +17,7 @@ acts as the version identifier.
|
||||||
- `api-contract.schema.md` — JSONL manifest schema for API contracts.
|
- `api-contract.schema.md` — JSONL manifest schema for API contracts.
|
||||||
- `api-contract.jsonl` — manifest of published contracts.
|
- `api-contract.jsonl` — manifest of published contracts.
|
||||||
- `amduatd-api-contract.v1.json` — contract bytes (v1).
|
- `amduatd-api-contract.v1.json` — contract bytes (v1).
|
||||||
|
- `amduatd-api-contract.v2.json` — draft contract bytes (v2, PEL-only writes).
|
||||||
|
|
||||||
Receipt note:
|
Receipt note:
|
||||||
- `/v1/pel/run` accepts optional receipt v1.1 fields (executor fingerprint, run id,
|
- `/v1/pel/run` accepts optional receipt v1.1 fields (executor fingerprint, run id,
|
||||||
|
|
|
||||||
820
registry/amduatd-api-contract.v2.json
Normal file
820
registry/amduatd-api-contract.v2.json
Normal file
|
|
@ -0,0 +1,820 @@
|
||||||
|
{
|
||||||
|
"contract": "AMDUATD/API/2",
|
||||||
|
"base_path": "/v2",
|
||||||
|
"notes": "Draft v2: PEL-only write surface. Direct artifact write endpoint removed.",
|
||||||
|
"endpoints": [
|
||||||
|
{"method": "GET", "path": "/v2/meta"},
|
||||||
|
{"method": "HEAD", "path": "/v2/meta"},
|
||||||
|
{"method": "GET", "path": "/v2/contract"},
|
||||||
|
{"method": "GET", "path": "/v2/healthz"},
|
||||||
|
{"method": "GET", "path": "/v2/readyz"},
|
||||||
|
{"method": "GET", "path": "/v2/metrics"},
|
||||||
|
{"method": "GET", "path": "/v2/artifacts/{ref}"},
|
||||||
|
{"method": "HEAD", "path": "/v2/artifacts/{ref}"},
|
||||||
|
{"method": "GET", "path": "/v2/artifacts/{ref}?format=info"},
|
||||||
|
{"method": "POST", "path": "/v2/pel/execute"},
|
||||||
|
{"method": "POST", "path": "/v2/ops/put"},
|
||||||
|
{"method": "POST", "path": "/v2/ops/concat"},
|
||||||
|
{"method": "POST", "path": "/v2/ops/slice"},
|
||||||
|
{"method": "GET", "path": "/v2/jobs/{id}"},
|
||||||
|
{"method": "GET", "path": "/v2/get/{ref}"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/nodes"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/nodes/{name}/versions"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/nodes/{name}/versions/tombstone"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/nodes/{name}/versions"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/nodes/{name}/neighbors"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/search"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/paths"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/subgraph"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/edges"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/edges/tombstone"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/batch"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/query"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/retrieve"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/export"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/import"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/schema/predicates"},
|
||||||
|
{"method": "POST", "path": "/v2/graph/schema/predicates"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/stats"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/capabilities"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/changes"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/edges"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/nodes/{name}"},
|
||||||
|
{"method": "GET", "path": "/v2/graph/history/{name}"}
|
||||||
|
],
|
||||||
|
"schemas": {
|
||||||
|
"job_enqueue_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["job_id", "status"],
|
||||||
|
"properties": {
|
||||||
|
"job_id": {"type": "integer"},
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"job_status_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["job_id", "kind", "status", "created_at_ms"],
|
||||||
|
"properties": {
|
||||||
|
"job_id": {"type": "integer"},
|
||||||
|
"kind": {"type": "string"},
|
||||||
|
"status": {"type": "string"},
|
||||||
|
"created_at_ms": {"type": "integer"},
|
||||||
|
"started_at_ms": {"type": ["integer", "null"]},
|
||||||
|
"completed_at_ms": {"type": ["integer", "null"]},
|
||||||
|
"result_ref": {"type": ["string", "null"]},
|
||||||
|
"error": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthz_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ok", "status", "time_ms"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"status": {"type": "string"},
|
||||||
|
"time_ms": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readyz_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ok", "status", "components"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"status": {"type": "string"},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["graph_index", "federation"],
|
||||||
|
"properties": {
|
||||||
|
"graph_index": {"type": "boolean"},
|
||||||
|
"federation": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["body_hex"],
|
||||||
|
"properties": {
|
||||||
|
"body_hex": {"type": "string"},
|
||||||
|
"type_tag": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concat_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["left_ref", "right_ref"],
|
||||||
|
"properties": {
|
||||||
|
"left_ref": {"type": "string"},
|
||||||
|
"right_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slice_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ref", "offset", "length"],
|
||||||
|
"properties": {
|
||||||
|
"ref": {"type": "string"},
|
||||||
|
"offset": {"type": "integer"},
|
||||||
|
"length": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_node_create_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"ref": {"type": "string", "description": "optional initial published ref"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_node_create_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "concept_ref"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"concept_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_provenance": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["source_uri", "extractor", "observed_at", "ingested_at", "trace_id"],
|
||||||
|
"properties": {
|
||||||
|
"source_uri": {"type": "string"},
|
||||||
|
"extractor": {"type": "string"},
|
||||||
|
"confidence": {"type": ["string", "number", "integer"]},
|
||||||
|
"observed_at": {"type": "integer"},
|
||||||
|
"ingested_at": {"type": "integer"},
|
||||||
|
"license": {"type": "string"},
|
||||||
|
"trace_id": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_edge_create_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["subject", "predicate", "object"],
|
||||||
|
"properties": {
|
||||||
|
"subject": {"type": "string", "description": "concept name or hex ref"},
|
||||||
|
"predicate": {"type": "string", "description": "relation alias/name or hex ref"},
|
||||||
|
"object": {"type": "string", "description": "concept name or hex ref"},
|
||||||
|
"metadata_ref": {"type": "string", "description": "optional artifact ref"},
|
||||||
|
"provenance": {"$ref": "#/schemas/graph_provenance"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_edge_create_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_edge_tombstone_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"},
|
||||||
|
"provenance": {"$ref": "#/schemas/graph_provenance"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_edge_tombstone_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ok", "target_edge_ref", "tombstone_edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"target_edge_ref": {"type": "string"},
|
||||||
|
"tombstone_edge_ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_node_version_tombstone_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ref"],
|
||||||
|
"properties": {
|
||||||
|
"ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"},
|
||||||
|
"provenance": {"$ref": "#/schemas/graph_provenance"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_node_version_tombstone_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ok", "name", "ref", "target_edge_ref", "tombstone_edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"ref": {"type": "string"},
|
||||||
|
"target_edge_ref": {"type": "string"},
|
||||||
|
"tombstone_edge_ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_batch_request": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"idempotency_key": {"type": "string"},
|
||||||
|
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
|
||||||
|
"nodes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "ref"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"},
|
||||||
|
"provenance": {"$ref": "#/schemas/graph_provenance"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edges": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["subject", "predicate", "object"],
|
||||||
|
"properties": {
|
||||||
|
"subject": {"type": "string"},
|
||||||
|
"predicate": {"type": "string"},
|
||||||
|
"object": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"},
|
||||||
|
"provenance": {"$ref": "#/schemas/graph_provenance"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_batch_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ok", "applied", "results"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"idempotency_key": {"type": "string"},
|
||||||
|
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
|
||||||
|
"applied": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["nodes", "versions", "edges"],
|
||||||
|
"properties": {
|
||||||
|
"nodes": {"type": "integer"},
|
||||||
|
"versions": {"type": "integer"},
|
||||||
|
"edges": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["kind", "index", "status", "code", "error"],
|
||||||
|
"properties": {
|
||||||
|
"kind": {"type": "string", "enum": ["node", "version", "edge"]},
|
||||||
|
"index": {"type": "integer"},
|
||||||
|
"status": {"type": "string", "enum": ["applied", "error"]},
|
||||||
|
"code": {"type": "integer"},
|
||||||
|
"error": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_query_request": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"where": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"subject": {"type": "string"},
|
||||||
|
"object": {"type": "string"},
|
||||||
|
"node": {"type": "string"},
|
||||||
|
"provenance_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"predicates": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"direction": {"type": "string", "enum": ["any", "outgoing", "incoming"]},
|
||||||
|
"include_versions": {"type": "boolean"},
|
||||||
|
"include_tombstoned": {"type": "boolean"},
|
||||||
|
"include_stats": {"type": "boolean"},
|
||||||
|
"max_result_bytes": {"type": "integer"},
|
||||||
|
"as_of": {"type": ["string", "integer"]},
|
||||||
|
"limit": {"type": "integer"},
|
||||||
|
"cursor": {"type": ["string", "integer"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_query_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["nodes", "edges", "paging"],
|
||||||
|
"properties": {
|
||||||
|
"nodes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["concept_ref", "name", "latest_ref"],
|
||||||
|
"properties": {
|
||||||
|
"concept_ref": {"type": "string"},
|
||||||
|
"name": {"type": ["string", "null"]},
|
||||||
|
"latest_ref": {"type": ["string", "null"]},
|
||||||
|
"versions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["edge_ref", "ref"],
|
||||||
|
"properties": {
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edges": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"edge_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paging": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["next_cursor", "has_more"],
|
||||||
|
"properties": {
|
||||||
|
"next_cursor": {"type": ["string", "null"]},
|
||||||
|
"has_more": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"scanned_edges": {"type": "integer"},
|
||||||
|
"returned_edges": {"type": "integer"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_retrieve_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["roots"],
|
||||||
|
"properties": {
|
||||||
|
"roots": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"goal_predicates": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"max_depth": {"type": "integer"},
|
||||||
|
"max_fanout": {"type": "integer"},
|
||||||
|
"include_versions": {"type": "boolean"},
|
||||||
|
"include_tombstoned": {"type": "boolean"},
|
||||||
|
"as_of": {"type": ["string", "integer"]},
|
||||||
|
"provenance_min_confidence": {"type": ["string", "number", "integer"]},
|
||||||
|
"limit_nodes": {"type": "integer"},
|
||||||
|
"limit_edges": {"type": "integer"},
|
||||||
|
"max_result_bytes": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_retrieve_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["nodes", "edges", "explanations", "truncated", "stats"],
|
||||||
|
"properties": {
|
||||||
|
"nodes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["concept_ref", "name", "latest_ref"],
|
||||||
|
"properties": {
|
||||||
|
"concept_ref": {"type": "string"},
|
||||||
|
"name": {"type": ["string", "null"]},
|
||||||
|
"latest_ref": {"type": ["string", "null"]},
|
||||||
|
"versions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["edge_ref", "ref"],
|
||||||
|
"properties": {
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edges": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"edge_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"explanations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["edge_ref", "depth", "reasons"],
|
||||||
|
"properties": {
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"depth": {"type": "integer"},
|
||||||
|
"reasons": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"confidence": {"type": ["number", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"truncated": {"type": "boolean"},
|
||||||
|
"stats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"scanned_edges": {"type": "integer"},
|
||||||
|
"traversed_edges": {"type": "integer"},
|
||||||
|
"returned_nodes": {"type": "integer"},
|
||||||
|
"returned_edges": {"type": "integer"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_export_request": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"as_of": {"type": ["string", "integer"]},
|
||||||
|
"cursor": {"type": ["string", "integer"]},
|
||||||
|
"limit": {"type": "integer"},
|
||||||
|
"predicates": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"roots": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"include_tombstoned": {"type": "boolean"},
|
||||||
|
"max_result_bytes": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_export_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["items", "next_cursor", "has_more", "snapshot_as_of", "stats"],
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["seq", "edge_ref", "subject_ref", "predicate_ref", "predicate", "object_ref", "tombstoned"],
|
||||||
|
"properties": {
|
||||||
|
"seq": {"type": "integer"},
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"predicate": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"tombstoned": {"type": "boolean"},
|
||||||
|
"metadata_ref": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"next_cursor": {"type": ["string", "null"]},
|
||||||
|
"has_more": {"type": "boolean"},
|
||||||
|
"snapshot_as_of": {"type": "string"},
|
||||||
|
"stats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"scanned_edges": {"type": "integer"},
|
||||||
|
"exported_items": {"type": "integer"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_import_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["items"],
|
||||||
|
"properties": {
|
||||||
|
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"subject": {"type": "string"},
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"predicate": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"object": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_import_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ok", "applied", "results"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"applied": {"type": "integer"},
|
||||||
|
"results": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["index", "status", "code", "error", "edge_ref"],
|
||||||
|
"properties": {
|
||||||
|
"index": {"type": "integer"},
|
||||||
|
"status": {"type": "string", "enum": ["applied", "error"]},
|
||||||
|
"code": {"type": "integer"},
|
||||||
|
"error": {"type": ["string", "null"]},
|
||||||
|
"edge_ref": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_schema_predicates_request": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mode": {"type": "string", "enum": ["strict", "warn", "off"]},
|
||||||
|
"provenance_mode": {"type": "string", "enum": ["optional", "required"]},
|
||||||
|
"predicates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"predicate": {"type": "string"},
|
||||||
|
"domain": {"type": "string"},
|
||||||
|
"range": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_schema_predicates_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["mode", "provenance_mode", "predicates"],
|
||||||
|
"properties": {
|
||||||
|
"mode": {"type": "string", "enum": ["strict", "warn", "off"]},
|
||||||
|
"provenance_mode": {"type": "string", "enum": ["optional", "required"]},
|
||||||
|
"predicates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["predicate_ref", "domain", "range"],
|
||||||
|
"properties": {
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"domain": {"type": ["string", "null"]},
|
||||||
|
"range": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_stats_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["edges_total", "aliases_total", "index", "tombstones"],
|
||||||
|
"properties": {
|
||||||
|
"edges_total": {"type": "integer"},
|
||||||
|
"aliases_total": {"type": "integer"},
|
||||||
|
"index": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["built_for_edges", "src_buckets", "dst_buckets", "predicate_buckets", "src_predicate_buckets", "dst_predicate_buckets", "healthy"],
|
||||||
|
"properties": {
|
||||||
|
"built_for_edges": {"type": "integer"},
|
||||||
|
"src_buckets": {"type": "integer"},
|
||||||
|
"dst_buckets": {"type": "integer"},
|
||||||
|
"predicate_buckets": {"type": "integer"},
|
||||||
|
"src_predicate_buckets": {"type": "integer"},
|
||||||
|
"dst_predicate_buckets": {"type": "integer"},
|
||||||
|
"healthy": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tombstones": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["edges", "ratio"],
|
||||||
|
"properties": {
|
||||||
|
"edges": {"type": "integer"},
|
||||||
|
"ratio": {"type": "number"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_capabilities_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["contract", "graph", "runtime"],
|
||||||
|
"properties": {
|
||||||
|
"contract": {"type": "string"},
|
||||||
|
"graph": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "features", "limits", "modes"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string"},
|
||||||
|
"features": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"limits": {"type": "object"},
|
||||||
|
"modes": {"type": "object"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtime": {"type": "object"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_changes_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["events", "next_cursor", "has_more"],
|
||||||
|
"properties": {
|
||||||
|
"events": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["event", "cursor", "edge_ref", "subject_ref", "predicate_ref", "object_ref"],
|
||||||
|
"properties": {
|
||||||
|
"event": {"type": "string", "enum": ["edge_appended", "version_published", "tombstone_applied"]},
|
||||||
|
"cursor": {"type": "string"},
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"concept_ref": {"type": "string"},
|
||||||
|
"ref": {"type": "string"},
|
||||||
|
"tombstoned_edge_ref": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"next_cursor": {"type": ["string", "null"]},
|
||||||
|
"has_more": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_node_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "concept_ref", "latest_ref", "versions", "outgoing", "incoming"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"concept_ref": {"type": "string"},
|
||||||
|
"latest_ref": {"type": ["string", "null"]},
|
||||||
|
"versions": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"outgoing": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["predicate_ref", "object_ref"],
|
||||||
|
"properties": {
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"object_ref": {"type": "string"},
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incoming": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["predicate_ref", "subject_ref"],
|
||||||
|
"properties": {
|
||||||
|
"predicate_ref": {"type": "string"},
|
||||||
|
"subject_ref": {"type": "string"},
|
||||||
|
"edge_ref": {"type": "string"},
|
||||||
|
"metadata_ref": {"type": ["string", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graph_history_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "events"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"events": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["event", "at_ms"],
|
||||||
|
"properties": {
|
||||||
|
"event": {"type": "string"},
|
||||||
|
"at_ms": {"type": "integer"},
|
||||||
|
"ref": {"type": ["string", "null"]},
|
||||||
|
"edge_ref": {"type": ["string", "null"]},
|
||||||
|
"details": {"type": "object"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pel_execute_request": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["program_ref", "inputs", "receipt"],
|
||||||
|
"properties": {
|
||||||
|
"program_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"scheme_ref": {"type": "string", "description": "hex ref or 'dag'"},
|
||||||
|
"params_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"inputs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "description": "hex ref or concept name"}
|
||||||
|
},
|
||||||
|
"inline_artifacts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["body_hex"],
|
||||||
|
"properties": {
|
||||||
|
"content_type": {"type": "string"},
|
||||||
|
"type_tag": {"type": "string", "description": "hex tag id, optional"},
|
||||||
|
"body_hex": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receipt": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"input_manifest_ref",
|
||||||
|
"environment_ref",
|
||||||
|
"evaluator_id",
|
||||||
|
"executor_ref",
|
||||||
|
"started_at",
|
||||||
|
"completed_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"input_manifest_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"environment_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"evaluator_id": {"type": "string"},
|
||||||
|
"executor_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"sbom_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"parity_digest_hex": {"type": "string"},
|
||||||
|
"executor_fingerprint_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"run_id_hex": {"type": "string"},
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["cpu_ms", "wall_ms", "max_rss_kib", "io_reads", "io_writes"],
|
||||||
|
"properties": {
|
||||||
|
"cpu_ms": {"type": "integer"},
|
||||||
|
"wall_ms": {"type": "integer"},
|
||||||
|
"max_rss_kib": {"type": "integer"},
|
||||||
|
"io_reads": {"type": "integer"},
|
||||||
|
"io_writes": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["kind", "log_ref", "sha256_hex"],
|
||||||
|
"properties": {
|
||||||
|
"kind": {"type": "integer"},
|
||||||
|
"log_ref": {"type": "string", "description": "hex ref or concept name"},
|
||||||
|
"sha256_hex": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"determinism_level": {"type": "integer", "description": "0-255"},
|
||||||
|
"rng_seed_hex": {"type": "string"},
|
||||||
|
"signature_hex": {"type": "string"},
|
||||||
|
"started_at": {"type": "integer"},
|
||||||
|
"completed_at": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"publish_outputs": {"type": "boolean"},
|
||||||
|
"append_fed_log": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pel_execute_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["run_ref", "receipt_ref", "stored_input_refs", "output_refs", "status"],
|
||||||
|
"properties": {
|
||||||
|
"run_ref": {"type": "string", "description": "hex ref"},
|
||||||
|
"trace_ref": {"type": "string", "description": "hex ref"},
|
||||||
|
"receipt_ref": {"type": "string", "description": "hex ref"},
|
||||||
|
"stored_input_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
|
||||||
|
"output_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["error"],
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["code", "message", "retryable"],
|
||||||
|
"properties": {
|
||||||
|
"code": {"type": "string"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"retryable": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
scripts/graph_client_helpers.sh
Executable file
186
scripts/graph_client_helpers.sh
Executable file
|
|
@ -0,0 +1,186 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Reusable HTTP and graph client helpers for local unix-socket amduatd usage.
|
||||||
|
|
||||||
|
graph_helpers_init() {
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "usage: graph_helpers_init ROOT_DIR" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
GRAPH_HELPERS_ROOT_DIR="$1"
|
||||||
|
GRAPH_HELPERS_HTTP="${GRAPH_HELPERS_ROOT_DIR}/build/amduatd_http_unix"
|
||||||
|
GRAPH_HELPERS_USE_HTTP=0
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
|
||||||
|
GRAPH_HELPERS_USE_HTTP=0
|
||||||
|
else
|
||||||
|
GRAPH_HELPERS_USE_HTTP=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
GRAPH_HELPERS_USE_HTTP=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 && ! -x "${GRAPH_HELPERS_HTTP}" ]]; then
|
||||||
|
echo "missing http transport (need curl --unix-socket or build/amduatd_http_unix)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_http_get() {
|
||||||
|
local sock="$1"
|
||||||
|
local path="$2"
|
||||||
|
shift 2
|
||||||
|
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
|
||||||
|
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method GET --path "${path}" "$@"
|
||||||
|
else
|
||||||
|
curl --silent --show-error --fail \
|
||||||
|
--unix-socket "${sock}" \
|
||||||
|
"$@" \
|
||||||
|
"http://localhost${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_http_get_allow() {
|
||||||
|
local sock="$1"
|
||||||
|
local path="$2"
|
||||||
|
shift 2
|
||||||
|
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
|
||||||
|
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method GET --path "${path}" --allow-status "$@"
|
||||||
|
else
|
||||||
|
curl --silent --show-error \
|
||||||
|
--unix-socket "${sock}" \
|
||||||
|
"$@" \
|
||||||
|
"http://localhost${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_http_post() {
|
||||||
|
local sock="$1"
|
||||||
|
local path="$2"
|
||||||
|
local data="$3"
|
||||||
|
shift 3
|
||||||
|
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
|
||||||
|
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method POST --path "${path}" --data "${data}" "$@"
|
||||||
|
else
|
||||||
|
curl --silent --show-error --fail \
|
||||||
|
--unix-socket "${sock}" \
|
||||||
|
"$@" \
|
||||||
|
--data-binary "${data}" \
|
||||||
|
"http://localhost${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_http_post_allow() {
|
||||||
|
local sock="$1"
|
||||||
|
local path="$2"
|
||||||
|
local data="$3"
|
||||||
|
shift 3
|
||||||
|
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
|
||||||
|
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method POST --path "${path}" --data "${data}" --allow-status "$@"
|
||||||
|
else
|
||||||
|
curl --silent --show-error \
|
||||||
|
--unix-socket "${sock}" \
|
||||||
|
"$@" \
|
||||||
|
--data-binary "${data}" \
|
||||||
|
"http://localhost${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_wait_for_ready() {
|
||||||
|
local sock="$1"
|
||||||
|
local pid="$2"
|
||||||
|
local log_path="$3"
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 120); do
|
||||||
|
if ! kill -0 "${pid}" >/dev/null 2>&1; then
|
||||||
|
if [[ -f "${log_path}" ]] && grep -q "bind: Operation not permitted" "${log_path}"; then
|
||||||
|
echo "skip: bind not permitted for unix socket" >&2
|
||||||
|
return 77
|
||||||
|
fi
|
||||||
|
if [[ -f "${log_path}" ]]; then
|
||||||
|
cat "${log_path}" >&2
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -S "${sock}" ]] && graph_http_get "${sock}" "/v1/meta" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_batch_ingest() {
|
||||||
|
local sock="$1"
|
||||||
|
local space="$2"
|
||||||
|
local payload="$3"
|
||||||
|
graph_http_post "${sock}" "/v2/graph/batch" "${payload}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space}"
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_changes_sync_once() {
|
||||||
|
local sock="$1"
|
||||||
|
local space="$2"
|
||||||
|
local cursor="$3"
|
||||||
|
local limit="$4"
|
||||||
|
local path="/v2/graph/changes?limit=${limit}"
|
||||||
|
if [[ -n "${cursor}" ]]; then
|
||||||
|
path+="&since_cursor=${cursor}"
|
||||||
|
fi
|
||||||
|
graph_http_get "${sock}" "${path}" --header "X-Amduat-Space: ${space}"
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_subgraph_fetch() {
|
||||||
|
local sock="$1"
|
||||||
|
local space="$2"
|
||||||
|
local root="$3"
|
||||||
|
local max_depth="$4"
|
||||||
|
local predicates="${5:-}"
|
||||||
|
local path="/v2/graph/subgraph?roots[]=${root}&max_depth=${max_depth}&dir=outgoing&limit_nodes=256&limit_edges=256"
|
||||||
|
if [[ -n "${predicates}" ]]; then
|
||||||
|
path+="&predicates[]=${predicates}"
|
||||||
|
fi
|
||||||
|
graph_http_get "${sock}" "${path}" --header "X-Amduat-Space: ${space}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "usage: $0 COMMAND ..." >&2
|
||||||
|
echo "commands: batch-ingest, sync-once, subgraph" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
cmd="$1"
|
||||||
|
shift
|
||||||
|
: "${AMDUATD_ROOT:?set AMDUATD_ROOT to repo root}"
|
||||||
|
graph_helpers_init "${AMDUATD_ROOT}"
|
||||||
|
case "${cmd}" in
|
||||||
|
batch-ingest)
|
||||||
|
if [[ $# -ne 3 ]]; then
|
||||||
|
echo "usage: $0 batch-ingest SOCK SPACE PAYLOAD_JSON" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
graph_batch_ingest "$1" "$2" "$3"
|
||||||
|
;;
|
||||||
|
sync-once)
|
||||||
|
if [[ $# -ne 4 ]]; then
|
||||||
|
echo "usage: $0 sync-once SOCK SPACE CURSOR LIMIT" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
graph_changes_sync_once "$1" "$2" "$3" "$4"
|
||||||
|
;;
|
||||||
|
subgraph)
|
||||||
|
if [[ $# -lt 4 || $# -gt 5 ]]; then
|
||||||
|
echo "usage: $0 subgraph SOCK SPACE ROOT MAX_DEPTH [PREDICATE]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
graph_subgraph_fetch "$1" "$2" "$3" "$4" "${5:-}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown command: ${cmd}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
379
scripts/test_graph_contract.sh
Executable file
379
scripts/test_graph_contract.sh
Executable file
|
|
@ -0,0 +1,379 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TMPDIR="${TMPDIR:-/tmp}"
|
||||||
|
mkdir -p "${TMPDIR}"
|
||||||
|
|
||||||
|
if ! command -v grep >/dev/null 2>&1; then
|
||||||
|
echo "skip: grep not found" >&2
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
if ! command -v awk >/dev/null 2>&1; then
|
||||||
|
echo "skip: awk not found" >&2
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
|
||||||
|
graph_helpers_init "${ROOT_DIR}"
|
||||||
|
|
||||||
|
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
|
||||||
|
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
|
||||||
|
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
|
||||||
|
echo "missing binaries; build amduatd and amduat-asl first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-contract-XXXXXX)"
|
||||||
|
root="${tmp_root}/root"
|
||||||
|
sock="${tmp_root}/amduatd.sock"
|
||||||
|
space_id="graphcontract"
|
||||||
|
log_file="${tmp_root}/amduatd.log"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${pid:-}" ]]; then
|
||||||
|
kill "${pid}" >/dev/null 2>&1 || true
|
||||||
|
wait "${pid}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
rm -rf "${tmp_root}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
extract_json_string() {
|
||||||
|
local key="$1"
|
||||||
|
awk -v k="\"${key}\":\"" '
|
||||||
|
{
|
||||||
|
pos = index($0, k);
|
||||||
|
if (pos > 0) {
|
||||||
|
rest = substr($0, pos + length(k));
|
||||||
|
end = index(rest, "\"");
|
||||||
|
if (end > 0) {
|
||||||
|
print substr(rest, 1, end - 1);
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor_to_num() {
|
||||||
|
local token="$1"
|
||||||
|
if [[ -z "${token}" ]]; then
|
||||||
|
echo 0
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "${token}" == g1_* ]]; then
|
||||||
|
echo "${token#g1_}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "${token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor_plus_one() {
|
||||||
|
local token="$1"
|
||||||
|
local n
|
||||||
|
if [[ -z "${token}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "${token}" == g1_* ]]; then
|
||||||
|
n="${token#g1_}"
|
||||||
|
printf 'g1_%s' "$((n + 1))"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s' "$((token + 1))"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_artifact() {
|
||||||
|
local payload="$1"
|
||||||
|
local resp
|
||||||
|
local ref
|
||||||
|
resp="$({
|
||||||
|
graph_http_post "${sock}" "/v1/artifacts" "${payload}" \
|
||||||
|
--header "Content-Type: application/octet-stream" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")"
|
||||||
|
if [[ -z "${ref}" ]]; then
|
||||||
|
echo "failed to parse artifact ref: ${resp}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s' "${ref}"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_node() {
|
||||||
|
local name="$1"
|
||||||
|
local ref="$2"
|
||||||
|
graph_http_post "${sock}" "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
create_edge() {
|
||||||
|
local s="$1"
|
||||||
|
local p="$2"
|
||||||
|
local o="$3"
|
||||||
|
graph_http_post "${sock}" "/v2/graph/edges" "{\"subject\":\"${s}\",\"predicate\":\"${p}\",\"object\":\"${o}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "${root}"
|
||||||
|
"${ASL_BIN}" init --root "${root}" >/dev/null
|
||||||
|
|
||||||
|
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
|
||||||
|
>"${log_file}" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
|
||||||
|
ready_rc=0
|
||||||
|
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
|
||||||
|
if [[ ${ready_rc} -eq 77 ]]; then
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
if [[ ${ready_rc} -ne 0 ]]; then
|
||||||
|
echo "daemon not ready" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ref_a="$(create_artifact "contract-a")"
|
||||||
|
ref_b="$(create_artifact "contract-b")"
|
||||||
|
ref_c="$(create_artifact "contract-c")"
|
||||||
|
ref_v1="$(create_artifact "contract-v1")"
|
||||||
|
ref_v2="$(create_artifact "contract-v2")"
|
||||||
|
|
||||||
|
create_node "gc-a" "${ref_a}"
|
||||||
|
create_node "gc-b" "${ref_b}"
|
||||||
|
create_node "gc-c" "${ref_c}"
|
||||||
|
create_node "gc-v" "${ref_v1}"
|
||||||
|
|
||||||
|
create_edge "gc-a" "ms.computed_by" "gc-b"
|
||||||
|
create_edge "gc-b" "ms.computed_by" "gc-c"
|
||||||
|
|
||||||
|
graph_http_post "${sock}" "/v2/graph/nodes/gc-v/versions" "{\"ref\":\"${ref_v2}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
versions_cutoff_raw="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/changes?event_types[]=version_published&limit=100" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
versions_cutoff="$(printf '%s\n' "${versions_cutoff_raw}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${versions_cutoff}" ]]; then
|
||||||
|
echo "missing version cutoff cursor: ${versions_cutoff_raw}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
versions_cutoff="$(cursor_plus_one "${versions_cutoff}")"
|
||||||
|
|
||||||
|
graph_http_post "${sock}" "/v2/graph/nodes/gc-v/versions/tombstone" "{\"ref\":\"${ref_v2}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
node_default="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/nodes/gc-v" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
node_default_latest="$(printf '%s\n' "${node_default}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${node_default_latest}" != "${ref_v1}" ]]; then
|
||||||
|
echo "node default latest_ref mismatch (want ${ref_v1}): ${node_default}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if echo "${node_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
|
||||||
|
echo "node default should hide tombstoned ${ref_v2}: ${node_default}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
node_all="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/nodes/gc-v?include_tombstoned=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
node_all_latest="$(printf '%s\n' "${node_all}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${node_all_latest}" != "${ref_v2}" ]]; then
|
||||||
|
echo "node include_tombstoned latest_ref mismatch (want ${ref_v2}): ${node_all}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${node_all}" | grep -q "\"ref\":\"${ref_v2}\"" || {
|
||||||
|
echo "node include_tombstoned should include ${ref_v2}: ${node_all}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
node_asof="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/nodes/gc-v?as_of=${versions_cutoff}" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
node_asof_latest="$(printf '%s\n' "${node_asof}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${node_asof_latest}" != "${ref_v2}" ]]; then
|
||||||
|
echo "node as_of before tombstone latest_ref mismatch (want ${ref_v2}): ${node_asof}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${node_asof}" | grep -q "\"ref\":\"${ref_v2}\"" || {
|
||||||
|
echo "node as_of before tombstone should include ${ref_v2}: ${node_asof}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
history_default="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/history/gc-v" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${history_default_latest}" != "${ref_v1}" ]]; then
|
||||||
|
echo "history default latest_ref mismatch (want ${ref_v1}): ${history_default}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
|
||||||
|
echo "history default should hide tombstoned ${ref_v2}: ${history_default}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
history_all="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/history/gc-v?include_tombstoned=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
history_all_latest="$(printf '%s\n' "${history_all}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${history_all_latest}" != "${ref_v2}" ]]; then
|
||||||
|
echo "history include_tombstoned latest_ref mismatch (want ${ref_v2}): ${history_all}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${history_all}" | grep -q "\"ref\":\"${ref_v2}\"" || {
|
||||||
|
echo "history include_tombstoned should include ${ref_v2}: ${history_all}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# schema strict: block predicate not in allowed list.
|
||||||
|
strict_policy='{"mode":"strict","predicates":[{"predicate":"ms.computed_by"}]}'
|
||||||
|
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${strict_policy}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
strict_reject="$({
|
||||||
|
graph_http_post_allow "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.within_domain","object":"gc-c"}' \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
echo "${strict_reject}" | grep -q 'predicate rejected by schema policy' || {
|
||||||
|
echo "strict mode should reject disallowed predicate: ${strict_reject}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# schema warn: same write should pass.
|
||||||
|
warn_policy='{"mode":"warn","predicates":[{"predicate":"ms.computed_by"}]}'
|
||||||
|
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${warn_policy}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
warn_accept="$({
|
||||||
|
graph_http_post "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.within_domain","object":"gc-c"}' \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
echo "${warn_accept}" | grep -q '"edge_ref":"' || {
|
||||||
|
echo "warn mode should allow disallowed predicate: ${warn_accept}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# provenance required: writes without metadata/provenance must fail with 422.
|
||||||
|
required_policy='{"mode":"warn","provenance_mode":"required","predicates":[{"predicate":"ms.computed_by"}]}'
|
||||||
|
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${required_policy}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
schema_required="$(
|
||||||
|
graph_http_get "${sock}" "/v2/graph/schema/predicates" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${schema_required}" | grep -q '"provenance_mode":"required"' || {
|
||||||
|
echo "schema provenance_mode did not persist: ${schema_required}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
required_reject="$({
|
||||||
|
graph_http_post_allow "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b"}' \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
echo "${required_reject}" | grep -q 'provenance required by schema policy' || {
|
||||||
|
echo "required provenance should reject missing attachment: ${required_reject}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
required_version_reject="$({
|
||||||
|
graph_http_post_allow "${sock}" "/v2/graph/nodes/gc-a/versions" "{\"ref\":\"${ref_v1}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
echo "${required_version_reject}" | grep -q 'provenance required by schema policy' || {
|
||||||
|
echo "required provenance should reject version write without attachment: ${required_version_reject}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
required_accept="$({
|
||||||
|
graph_http_post "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b","provenance":{"source_uri":"urn:test","extractor":"contract-test","observed_at":1,"ingested_at":2,"trace_id":"trace-required-1"}}' \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
echo "${required_accept}" | grep -q '"edge_ref":"' || {
|
||||||
|
echo "required provenance should allow explicit provenance payload: ${required_accept}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# reset to optional so remaining tests can use minimal payloads.
|
||||||
|
optional_policy='{"mode":"warn","provenance_mode":"optional","predicates":[{"predicate":"ms.computed_by"}]}'
|
||||||
|
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${optional_policy}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
# batch idempotency replay must be deterministic.
|
||||||
|
idem_payload='{"idempotency_key":"gc-idem-1","mode":"continue_on_error","edges":[{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-c"},{"subject":"gc-missing","predicate":"ms.computed_by","object":"gc-c"}]}'
|
||||||
|
idem_first="$(graph_batch_ingest "${sock}" "${space_id}" "${idem_payload}")"
|
||||||
|
idem_second="$(graph_batch_ingest "${sock}" "${space_id}" "${idem_payload}")"
|
||||||
|
if [[ "${idem_first}" != "${idem_second}" ]]; then
|
||||||
|
echo "idempotent replay mismatch" >&2
|
||||||
|
echo "first=${idem_first}" >&2
|
||||||
|
echo "second=${idem_second}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# payload mismatch on same idempotency key must fail.
|
||||||
|
idem_conflict="$({
|
||||||
|
graph_http_post_allow "${sock}" "/v2/graph/batch" '{"idempotency_key":"gc-idem-1","mode":"continue_on_error","edges":[{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b"}]}' \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
})"
|
||||||
|
echo "${idem_conflict}" | grep -q 'idempotency_key reuse with different payload' || {
|
||||||
|
echo "idempotency conflict missing expected error: ${idem_conflict}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# changes sync helper: cursor monotonic + resumable loop.
|
||||||
|
changes_1="$(graph_changes_sync_once "${sock}" "${space_id}" "" 2)"
|
||||||
|
cursor_1="$(printf '%s\n' "${changes_1}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${cursor_1}" ]]; then
|
||||||
|
echo "changes first page missing next_cursor: ${changes_1}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
changes_2="$(graph_changes_sync_once "${sock}" "${space_id}" "${cursor_1}" 2)"
|
||||||
|
cursor_2="$(printf '%s\n' "${changes_2}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${cursor_2}" ]]; then
|
||||||
|
echo "changes second page missing next_cursor: ${changes_2}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
num_1="$(cursor_to_num "${cursor_1}")"
|
||||||
|
num_2="$(cursor_to_num "${cursor_2}")"
|
||||||
|
if [[ "${num_2}" -lt "${num_1}" ]]; then
|
||||||
|
echo "changes cursor regressed: ${cursor_1} -> ${cursor_2}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# subgraph helper should return connected nodes.
|
||||||
|
subgraph_resp="$(graph_subgraph_fetch "${sock}" "${space_id}" "gc-a" 2 "ms.computed_by")"
|
||||||
|
echo "${subgraph_resp}" | grep -q '"name":"gc-a"' || {
|
||||||
|
echo "subgraph missing gc-a: ${subgraph_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${subgraph_resp}" | grep -q '"name":"gc-b"' || {
|
||||||
|
echo "subgraph missing gc-b: ${subgraph_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "ok: v2 graph contract tests passed"
|
||||||
778
scripts/test_graph_queries.sh
Executable file
778
scripts/test_graph_queries.sh
Executable file
|
|
@ -0,0 +1,778 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
|
||||||
|
USE_HTTP_HELPER=0
|
||||||
|
TMPDIR="${TMPDIR:-/tmp}"
|
||||||
|
mkdir -p "${TMPDIR}"
|
||||||
|
|
||||||
|
if ! command -v grep >/dev/null 2>&1; then
|
||||||
|
echo "skip: grep not found" >&2
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
if ! command -v awk >/dev/null 2>&1; then
|
||||||
|
echo "skip: awk not found" >&2
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
|
||||||
|
USE_HTTP_HELPER=0
|
||||||
|
else
|
||||||
|
USE_HTTP_HELPER=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
USE_HTTP_HELPER=1
|
||||||
|
fi
|
||||||
|
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
|
||||||
|
echo "skip: curl lacks --unix-socket support and helper missing" >&2
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
|
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
|
||||||
|
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
|
||||||
|
|
||||||
|
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
|
||||||
|
echo "missing binaries; build amduatd and amduat-asl first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-queries-XXXXXX)"
|
||||||
|
root="${tmp_root}/root"
|
||||||
|
sock="${tmp_root}/amduatd.sock"
|
||||||
|
space_id="graphq"
|
||||||
|
log_file="${tmp_root}/amduatd.log"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${pid:-}" ]]; then
|
||||||
|
kill "${pid}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
rm -rf "${tmp_root}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
http_get() {
|
||||||
|
local path="$1"
|
||||||
|
shift
|
||||||
|
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
|
||||||
|
"${HTTP_HELPER}" --sock "${sock}" --method GET --path "${path}" "$@"
|
||||||
|
else
|
||||||
|
curl --silent --show-error --fail \
|
||||||
|
--unix-socket "${sock}" \
|
||||||
|
"$@" \
|
||||||
|
"http://localhost${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
http_post() {
|
||||||
|
local path="$1"
|
||||||
|
local data="$2"
|
||||||
|
shift 2
|
||||||
|
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
|
||||||
|
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "${path}" \
|
||||||
|
--data "${data}" \
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
curl --silent --show-error --fail \
|
||||||
|
--unix-socket "${sock}" \
|
||||||
|
"$@" \
|
||||||
|
--data-binary "${data}" \
|
||||||
|
"http://localhost${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_json_string() {
|
||||||
|
local key="$1"
|
||||||
|
awk -v k="\"${key}\":\"" '
|
||||||
|
{
|
||||||
|
pos = index($0, k);
|
||||||
|
if (pos > 0) {
|
||||||
|
rest = substr($0, pos + length(k));
|
||||||
|
end = index(rest, "\"");
|
||||||
|
if (end > 0) {
|
||||||
|
print substr(rest, 1, end - 1);
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor_plus_one() {
|
||||||
|
local token="$1"
|
||||||
|
local n
|
||||||
|
if [[ -z "${token}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "${token}" == g1_* ]]; then
|
||||||
|
n="${token#g1_}"
|
||||||
|
printf 'g1_%s' "$((n + 1))"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s' "$((token + 1))"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_ready() {
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 100); do
|
||||||
|
if ! kill -0 "${pid}" >/dev/null 2>&1; then
|
||||||
|
if [[ -f "${log_file}" ]] && grep -q "bind: Operation not permitted" "${log_file}"; then
|
||||||
|
echo "skip: bind not permitted for unix socket" >&2
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -S "${sock}" ]] && http_get "/v1/meta" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
create_artifact() {
|
||||||
|
local payload="$1"
|
||||||
|
local resp
|
||||||
|
local ref
|
||||||
|
resp="$(
|
||||||
|
http_post "/v1/artifacts" "${payload}" \
|
||||||
|
--header "Content-Type: application/octet-stream" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")"
|
||||||
|
if [[ -z "${ref}" ]]; then
|
||||||
|
echo "failed to parse artifact ref: ${resp}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s' "${ref}"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_node() {
|
||||||
|
local name="$1"
|
||||||
|
local ref="$2"
|
||||||
|
http_post "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
create_edge() {
|
||||||
|
local subject="$1"
|
||||||
|
local predicate="$2"
|
||||||
|
local object="$3"
|
||||||
|
http_post "/v2/graph/edges" \
|
||||||
|
"{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
create_edge_with_metadata() {
|
||||||
|
local subject="$1"
|
||||||
|
local predicate="$2"
|
||||||
|
local object="$3"
|
||||||
|
local metadata_ref="$4"
|
||||||
|
http_post "/v2/graph/edges" \
|
||||||
|
"{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\",\"metadata_ref\":\"${metadata_ref}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
start_daemon() {
|
||||||
|
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
|
||||||
|
>"${log_file}" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
if ! wait_for_ready; then
|
||||||
|
echo "daemon not ready" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_daemon() {
|
||||||
|
if [[ -n "${pid:-}" ]]; then
|
||||||
|
kill "${pid}" >/dev/null 2>&1 || true
|
||||||
|
wait "${pid}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
start_daemon
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "${root}"
|
||||||
|
"${ASL_BIN}" init --root "${root}"
|
||||||
|
start_daemon
|
||||||
|
|
||||||
|
ref_a="$(create_artifact "payload-a")"
|
||||||
|
ref_b="$(create_artifact "payload-b")"
|
||||||
|
ref_c="$(create_artifact "payload-c")"
|
||||||
|
ref_d="$(create_artifact "payload-d")"
|
||||||
|
ref_e="$(create_artifact "payload-e")"
|
||||||
|
ref_e2="$(create_artifact "payload-e2")"
|
||||||
|
ref_v1="$(create_artifact "payload-v1")"
|
||||||
|
ref_v2="$(create_artifact "payload-v2")"
|
||||||
|
ref_p1="$(create_artifact "payload-provenance-1")"
|
||||||
|
ref_p2="$(create_artifact "payload-provenance-2")"
|
||||||
|
|
||||||
|
create_node "gq-a" "${ref_a}"
|
||||||
|
create_node "gq-b" "${ref_b}"
|
||||||
|
create_node "gq-c" "${ref_c}"
|
||||||
|
create_node "gq-d" "${ref_d}"
|
||||||
|
create_node "gq-e" "${ref_e}"
|
||||||
|
create_node "gq-v" "${ref_v1}"
|
||||||
|
create_node "gq-prov1" "${ref_p1}"
|
||||||
|
create_node "gq-prov2" "${ref_p2}"
|
||||||
|
|
||||||
|
# Seed path and neighbor data in a controlled edge order.
|
||||||
|
create_edge "gq-a" "ms.within_domain" "gq-b"
|
||||||
|
create_edge "gq-b" "ms.within_domain" "gq-c"
|
||||||
|
create_edge "gq-a" "ms.computed_by" "gq-b"
|
||||||
|
|
||||||
|
cutoff_resp="$(
|
||||||
|
http_get "/v2/graph/changes?event_types[]=edge_appended&limit=100" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
cutoff_cursor="$(printf '%s\n' "${cutoff_resp}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${cutoff_cursor}" ]]; then
|
||||||
|
echo "failed to parse cutoff cursor: ${cutoff_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cutoff_cursor="$(cursor_plus_one "${cutoff_cursor}")"
|
||||||
|
|
||||||
|
create_edge "gq-a" "ms.computed_by" "gq-d"
|
||||||
|
create_edge "gq-a" "ms.within_domain" "gq-c"
|
||||||
|
|
||||||
|
search_page1="$(
|
||||||
|
http_get "/v2/graph/search?name_prefix=gq-&limit=2" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
search_cursor="$(printf '%s\n' "${search_page1}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${search_cursor}" ]]; then
|
||||||
|
echo "missing search cursor in page1: ${search_page1}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
search_page2="$(
|
||||||
|
http_get "/v2/graph/search?name_prefix=gq-&limit=10&cursor=${search_cursor}" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
search_joined="${search_page1} ${search_page2}"
|
||||||
|
echo "${search_joined}" | grep -q '"name":"gq-a"' || { echo "search missing gq-a" >&2; exit 1; }
|
||||||
|
echo "${search_joined}" | grep -q '"name":"gq-b"' || { echo "search missing gq-b" >&2; exit 1; }
|
||||||
|
echo "${search_joined}" | grep -q '"name":"gq-c"' || { echo "search missing gq-c" >&2; exit 1; }
|
||||||
|
echo "${search_joined}" | grep -q '"name":"gq-d"' || { echo "search missing gq-d" >&2; exit 1; }
|
||||||
|
|
||||||
|
neighbors_page1="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=1&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
neighbors_cursor="$(printf '%s\n' "${neighbors_page1}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${neighbors_cursor}" ]]; then
|
||||||
|
echo "missing neighbors cursor in page1: ${neighbors_page1}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
neighbors_page2="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&cursor=${neighbors_cursor}&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
neighbors_joined="${neighbors_page1} ${neighbors_page2}"
|
||||||
|
echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors missing gq-b" >&2; exit 1; }
|
||||||
|
echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-d"' || { echo "neighbors missing gq-d" >&2; exit 1; }
|
||||||
|
|
||||||
|
neighbors_asof="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&as_of=${cutoff_cursor}&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors as_of missing gq-b" >&2; exit 1; }
|
||||||
|
if echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-d"'; then
|
||||||
|
echo "neighbors as_of unexpectedly includes gq-d" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
paths_latest="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_latest}" | grep -q '"depth":1' || {
|
||||||
|
echo "paths latest expected direct depth 1: ${paths_latest}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
paths_asof="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&as_of=${cutoff_cursor}&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_asof}" | grep -q '"depth":2' || {
|
||||||
|
echo "paths as_of expected historical depth 2: ${paths_asof}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph_page1="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=1&limit_nodes=10&include_versions=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
subgraph_cursor="$(printf '%s\n' "${subgraph_page1}" | extract_json_string "next_cursor")"
|
||||||
|
if [[ -z "${subgraph_cursor}" ]]; then
|
||||||
|
echo "missing subgraph cursor in page1: ${subgraph_page1}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
subgraph_page2="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10&cursor=${subgraph_cursor}" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
subgraph_joined="${subgraph_page1} ${subgraph_page2}"
|
||||||
|
echo "${subgraph_joined}" | grep -q '"name":"gq-a"' || { echo "subgraph missing root node gq-a" >&2; exit 1; }
|
||||||
|
echo "${subgraph_joined}" | grep -q '"name":"gq-b"' || { echo "subgraph missing gq-b" >&2; exit 1; }
|
||||||
|
echo "${subgraph_joined}" | grep -q '"name":"gq-d"' || { echo "subgraph missing gq-d" >&2; exit 1; }
|
||||||
|
echo "${subgraph_joined}" | grep -q '"versions":\[' || { echo "subgraph include_versions missing versions" >&2; exit 1; }
|
||||||
|
|
||||||
|
subgraph_asof="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&as_of=${cutoff_cursor}&limit_edges=10&limit_nodes=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${subgraph_asof}" | grep -q '"name":"gq-b"' || { echo "subgraph as_of missing gq-b" >&2; exit 1; }
|
||||||
|
if echo "${subgraph_asof}" | grep -q '"name":"gq-d"'; then
|
||||||
|
echo "subgraph as_of unexpectedly includes gq-d" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gqd_edge_resp="$(
|
||||||
|
http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&object=gq-d&dir=outgoing&limit=1" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
gqd_edge_ref="$(printf '%s\n' "${gqd_edge_resp}" | extract_json_string "edge_ref")"
|
||||||
|
if [[ -z "${gqd_edge_ref}" ]]; then
|
||||||
|
echo "failed to parse gq-a->gq-d edge ref: ${gqd_edge_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
http_post "/v2/graph/edges/tombstone" \
|
||||||
|
"{\"edge_ref\":\"${gqd_edge_ref}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
subgraph_after_tombstone="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
if echo "${subgraph_after_tombstone}" | grep -q '"name":"gq-d"'; then
|
||||||
|
echo "subgraph after tombstone unexpectedly includes gq-d: ${subgraph_after_tombstone}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
subgraph_include_tombstoned="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&include_tombstoned=true&limit_edges=10&limit_nodes=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${subgraph_include_tombstoned}" | grep -q '"name":"gq-d"' || {
|
||||||
|
echo "subgraph include_tombstoned missing gq-d: ${subgraph_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
edges_include_tombstoned="$(
|
||||||
|
http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&dir=outgoing&include_tombstoned=true&limit=10&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${edges_include_tombstoned}" | grep -q '"object_name":"gq-d"' || {
|
||||||
|
echo "edges include_tombstoned missing gq-d: ${edges_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
neighbors_after_tombstone="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
if echo "${neighbors_after_tombstone}" | grep -q '"neighbor_name":"gq-d"'; then
|
||||||
|
echo "neighbors default should exclude tombstoned gq-d edge: ${neighbors_after_tombstone}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
neighbors_include_tombstoned="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&include_tombstoned=true&limit=10&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${neighbors_include_tombstoned}" | grep -q '"neighbor_name":"gq-d"' || {
|
||||||
|
echo "neighbors include_tombstoned missing gq-d: ${neighbors_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
paths_excluding_tombstoned="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_excluding_tombstoned}" | grep -q '"paths":\[\]' || {
|
||||||
|
echo "paths default should exclude tombstoned edge: ${paths_excluding_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
paths_include_tombstoned="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2&include_tombstoned=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_include_tombstoned}" | grep -q '"depth":1' || {
|
||||||
|
echo "paths include_tombstoned expected depth 1 path: ${paths_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
create_edge_with_metadata "gq-b" "ms.computed_by" "gq-d" "gq-prov1"
|
||||||
|
create_edge_with_metadata "gq-b" "ms.computed_by" "gq-a" "gq-prov2"
|
||||||
|
|
||||||
|
neighbors_provenance="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov1&limit=10&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${neighbors_provenance}" | grep -q '"neighbor_name":"gq-d"' || {
|
||||||
|
echo "neighbors provenance filter missing gq-d: ${neighbors_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
neighbors_provenance_missing="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov-missing&limit=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${neighbors_provenance_missing}" | grep -q '"neighbors":\[\]' || {
|
||||||
|
echo "neighbors unresolved provenance expected empty result: ${neighbors_provenance_missing}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
paths_provenance_match="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-b&to=gq-d&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_provenance_match}" | grep -q '"depth":1' || {
|
||||||
|
echo "paths provenance filter expected depth 1 path: ${paths_provenance_match}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${paths_provenance_match}" | grep -q '"object_name":"gq-d"' || {
|
||||||
|
echo "paths provenance filter missing gq-d: ${paths_provenance_match}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
paths_provenance_excluded="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-b&to=gq-a&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_provenance_excluded}" | grep -q '"paths":\[\]' || {
|
||||||
|
echo "paths provenance filter unexpectedly includes gq-b->gq-a path: ${paths_provenance_excluded}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_resp="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-d\",\"metadata_ref\":\"gq-prov1\"}]}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${batch_resp}" | grep -q '"ok":true' || {
|
||||||
|
echo "batch edge with metadata_ref failed: ${batch_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_resp}" | grep -q '"results":\[' || {
|
||||||
|
echo "batch response missing results array: ${batch_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_resp}" | grep -q '"status":"applied"' || {
|
||||||
|
echo "batch response missing applied item status: ${batch_resp}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_version_provenance="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"{\"versions\":[{\"name\":\"gq-e\",\"ref\":\"${ref_e2}\",\"provenance\":{\"source_uri\":\"urn:test:gq-e-v2\",\"extractor\":\"graph-test\",\"confidence\":\"0.91\",\"observed_at\":1730000000000,\"ingested_at\":1730000000100,\"license\":\"test-only\",\"trace_id\":\"trace-gq-e-v2\"}}]}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${batch_version_provenance}" | grep -q '"ok":true' || {
|
||||||
|
echo "batch version with provenance failed: ${batch_version_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_version_provenance}" | grep -q '"status":"applied"' || {
|
||||||
|
echo "batch version with provenance missing applied status: ${batch_version_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_edge_provenance="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-a\",\"provenance\":{\"source_uri\":\"urn:test:gq-c-a\",\"extractor\":\"graph-test\",\"confidence\":\"0.87\",\"observed_at\":1730000000200,\"ingested_at\":1730000000300,\"trace_id\":\"trace-gq-c-a\"}}]}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${batch_edge_provenance}" | grep -q '"ok":true' || {
|
||||||
|
echo "batch edge with provenance failed: ${batch_edge_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_edge_provenance}" | grep -q '"status":"applied"' || {
|
||||||
|
echo "batch edge with provenance missing applied status: ${batch_edge_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
http_post "/v2/graph/nodes/gq-v/versions" \
|
||||||
|
"{\"ref\":\"${ref_v2}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
http_post "/v2/graph/nodes/gq-v/versions/tombstone" \
|
||||||
|
"{\"ref\":\"${ref_v2}\"}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}" >/dev/null
|
||||||
|
|
||||||
|
gqv_after_tombstone="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-v" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
gqv_latest="$(printf '%s\n' "${gqv_after_tombstone}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${gqv_latest}" != "${ref_v1}" ]]; then
|
||||||
|
echo "version tombstone expected latest_ref=${ref_v1}, got ${gqv_latest}: ${gqv_after_tombstone}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if echo "${gqv_after_tombstone}" | grep -q "\"ref\":\"${ref_v2}\""; then
|
||||||
|
echo "version tombstone expected ${ref_v2} hidden by default: ${gqv_after_tombstone}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gqv_include_tombstoned="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-v?include_tombstoned=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
gqv_latest_all="$(printf '%s\n' "${gqv_include_tombstoned}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${gqv_latest_all}" != "${ref_v2}" ]]; then
|
||||||
|
echo "include_tombstoned expected latest_ref=${ref_v2}, got ${gqv_latest_all}: ${gqv_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${gqv_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || {
|
||||||
|
echo "include_tombstoned expected historical version ${ref_v2}: ${gqv_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
history_default="$(
|
||||||
|
http_get "/v2/graph/history/gq-v" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${history_default_latest}" != "${ref_v1}" ]]; then
|
||||||
|
echo "history default expected latest_ref=${ref_v1}, got ${history_default_latest}: ${history_default}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
|
||||||
|
echo "history default expected tombstoned version ${ref_v2} hidden: ${history_default}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
history_include_tombstoned="$(
|
||||||
|
http_get "/v2/graph/history/gq-v?include_tombstoned=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
history_all_latest="$(printf '%s\n' "${history_include_tombstoned}" | extract_json_string "latest_ref")"
|
||||||
|
if [[ "${history_all_latest}" != "${ref_v2}" ]]; then
|
||||||
|
echo "history include_tombstoned expected latest_ref=${ref_v2}, got ${history_all_latest}: ${history_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${history_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || {
|
||||||
|
echo "history include_tombstoned expected tombstoned version ${ref_v2}: ${history_include_tombstoned}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_continue="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"{\"mode\":\"continue_on_error\",\"edges\":[{\"subject\":\"gq-a\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"},{\"subject\":\"gq-missing\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"}]}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${batch_continue}" | grep -q '"ok":false' || {
|
||||||
|
echo "batch continue_on_error expected ok=false: ${batch_continue}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_continue}" | grep -q '"mode":"continue_on_error"' || {
|
||||||
|
echo "batch continue_on_error mode echo missing: ${batch_continue}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_continue}" | grep -q '"status":"applied"' || {
|
||||||
|
echo "batch continue_on_error missing applied result: ${batch_continue}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${batch_continue}" | grep -q '"status":"error"' || {
|
||||||
|
echo "batch continue_on_error missing error result: ${batch_continue}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
idem_payload='{"idempotency_key":"gq-idem-1","mode":"continue_on_error","edges":[{"subject":"gq-b","predicate":"ms.computed_by","object":"gq-c"},{"subject":"gq-nope","predicate":"ms.computed_by","object":"gq-c"}]}'
|
||||||
|
idem_first="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"${idem_payload}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
idem_second="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"${idem_payload}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
if [[ "${idem_first}" != "${idem_second}" ]]; then
|
||||||
|
echo "idempotency replay response mismatch" >&2
|
||||||
|
echo "first=${idem_first}" >&2
|
||||||
|
echo "second=${idem_second}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
restart_daemon
|
||||||
|
idem_third="$(
|
||||||
|
http_post "/v2/graph/batch" \
|
||||||
|
"${idem_payload}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
if [[ "${idem_first}" != "${idem_third}" ]]; then
|
||||||
|
echo "idempotency replay after restart mismatch" >&2
|
||||||
|
echo "first=${idem_first}" >&2
|
||||||
|
echo "third=${idem_third}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
query_include_versions="$(
|
||||||
|
http_post "/v2/graph/query" \
|
||||||
|
"{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_versions\":true,\"limit\":10}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${query_include_versions}" | grep -q '"versions":\[' || {
|
||||||
|
echo "graph query include_versions missing versions: ${query_include_versions}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
query_with_stats="$(
|
||||||
|
http_post "/v2/graph/query" \
|
||||||
|
"{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_stats\":true,\"max_result_bytes\":1048576,\"limit\":10}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${query_with_stats}" | grep -q '"stats":{' || {
|
||||||
|
echo "graph query include_stats missing stats block: ${query_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${query_with_stats}" | grep -q '"plan":"' || {
|
||||||
|
echo "graph query include_stats missing plan: ${query_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
query_provenance="$(
|
||||||
|
http_post "/v2/graph/query" \
|
||||||
|
"{\"where\":{\"subject\":\"gq-b\",\"provenance_ref\":\"gq-prov1\"},\"predicates\":[\"ms.computed_by\"],\"direction\":\"outgoing\",\"limit\":10}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${query_provenance}" | grep -q '"name":"gq-d"' || {
|
||||||
|
echo "graph query provenance filter missing expected node gq-d: ${query_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if echo "${query_provenance}" | grep -q '"name":"gq-a"'; then
|
||||||
|
echo "graph query provenance filter unexpectedly includes gq-a: ${query_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
query_provenance_count="$(printf '%s\n' "${query_provenance}" | grep -o '"edge_ref":"' | wc -l | awk '{print $1}')"
|
||||||
|
if [[ "${query_provenance_count}" != "1" ]]; then
|
||||||
|
echo "graph query provenance expected exactly one edge, got ${query_provenance_count}: ${query_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
edges_provenance="$(
|
||||||
|
http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit=10&expand_names=true" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${edges_provenance}" | grep -q '"object_name":"gq-d"' || {
|
||||||
|
echo "graph edges provenance filter missing gq-d: ${edges_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if echo "${edges_provenance}" | grep -q '"object_name":"gq-a"'; then
|
||||||
|
echo "graph edges provenance filter unexpectedly includes gq-a: ${edges_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
edges_with_stats="$(
|
||||||
|
http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&include_stats=true&max_result_bytes=1048576&limit=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${edges_with_stats}" | grep -q '"stats":{' || {
|
||||||
|
echo "graph edges include_stats missing stats block: ${edges_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${edges_with_stats}" | grep -q '"plan":"' || {
|
||||||
|
echo "graph edges include_stats missing plan: ${edges_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph_provenance="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-b&max_depth=1&predicates[]=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit_edges=10&limit_nodes=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${subgraph_provenance}" | grep -q '"name":"gq-d"' || {
|
||||||
|
echo "subgraph provenance filter missing gq-d: ${subgraph_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if echo "${subgraph_provenance}" | grep -q '"name":"gq-a"'; then
|
||||||
|
echo "subgraph provenance filter unexpectedly includes gq-a: ${subgraph_provenance}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
subgraph_with_stats="$(
|
||||||
|
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&max_fanout=4096&include_stats=true&max_result_bytes=1048576&limit_edges=10&limit_nodes=10" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${subgraph_with_stats}" | grep -q '"stats":{' || {
|
||||||
|
echo "subgraph include_stats missing stats block: ${subgraph_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${subgraph_with_stats}" | grep -q '"plan":"' || {
|
||||||
|
echo "subgraph include_stats missing plan: ${subgraph_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
paths_with_stats="$(
|
||||||
|
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&include_stats=true&max_fanout=4096&max_result_bytes=1048576" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${paths_with_stats}" | grep -q '"stats":{' || {
|
||||||
|
echo "paths include_stats missing stats block: ${paths_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "${paths_with_stats}" | grep -q '"plan":"' || {
|
||||||
|
echo "paths include_stats missing plan: ${paths_with_stats}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
gqb_node="$(
|
||||||
|
http_get "/v2/graph/nodes/gq-b" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
gqb_ref="$(printf '%s\n' "${gqb_node}" | extract_json_string "concept_ref")"
|
||||||
|
if [[ -z "${gqb_ref}" ]]; then
|
||||||
|
echo "failed to resolve gq-b concept ref: ${gqb_node}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
changes_tombstone="$(
|
||||||
|
http_get "/v2/graph/changes?event_types[]=tombstone_applied&limit=100" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${changes_tombstone}" | grep -q '"event":"tombstone_applied"' || {
|
||||||
|
echo "changes tombstone filter missing tombstone event: ${changes_tombstone}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
changes_filtered="$(
|
||||||
|
http_get "/v2/graph/changes?since_as_of=${cutoff_cursor}&predicates[]=ms.computed_by&roots[]=gq-b&limit=100" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${changes_filtered}" | grep -q "\"${gqb_ref}\"" || {
|
||||||
|
echo "changes root/predicate filter missing gq-b involvement: ${changes_filtered}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if echo "${changes_filtered}" | grep -q '"event":"version_published"'; then
|
||||||
|
echo "changes predicate filter unexpectedly includes version events: ${changes_filtered}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
changes_wait_empty="$(
|
||||||
|
http_get "/v2/graph/changes?since_cursor=g1_999999&wait_ms=1&limit=1" \
|
||||||
|
--header "X-Amduat-Space: ${space_id}"
|
||||||
|
)"
|
||||||
|
echo "${changes_wait_empty}" | grep -q '"events":\[\]' || {
|
||||||
|
echo "changes wait_ms empty poll expected no events: ${changes_wait_empty}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "ok: v2 graph query tests passed"
|
||||||
2236
src/amduatd.c
2236
src/amduatd.c
File diff suppressed because it is too large
Load diff
12173
src/amduatd_concepts.c
12173
src/amduatd_concepts.c
File diff suppressed because it is too large
Load diff
|
|
@ -25,6 +25,46 @@ typedef struct {
|
||||||
size_t cap;
|
size_t cap;
|
||||||
} amduatd_edge_list_t;
|
} amduatd_edge_list_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
amduat_reference_t ref;
|
||||||
|
size_t *edge_indices;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
} amduatd_ref_edge_bucket_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
amduat_reference_t left_ref;
|
||||||
|
amduat_reference_t right_ref;
|
||||||
|
size_t *edge_indices;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
} amduatd_ref_pair_edge_bucket_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
size_t built_for_edges_len;
|
||||||
|
size_t *alias_edge_indices;
|
||||||
|
size_t alias_len;
|
||||||
|
size_t alias_cap;
|
||||||
|
amduatd_ref_edge_bucket_t *src_buckets;
|
||||||
|
size_t src_len;
|
||||||
|
size_t src_cap;
|
||||||
|
amduatd_ref_edge_bucket_t *dst_buckets;
|
||||||
|
size_t dst_len;
|
||||||
|
size_t dst_cap;
|
||||||
|
amduatd_ref_edge_bucket_t *predicate_buckets;
|
||||||
|
size_t predicate_len;
|
||||||
|
size_t predicate_cap;
|
||||||
|
amduatd_ref_pair_edge_bucket_t *src_predicate_buckets;
|
||||||
|
size_t src_predicate_len;
|
||||||
|
size_t src_predicate_cap;
|
||||||
|
amduatd_ref_pair_edge_bucket_t *dst_predicate_buckets;
|
||||||
|
size_t dst_predicate_len;
|
||||||
|
size_t dst_predicate_cap;
|
||||||
|
amduatd_ref_edge_bucket_t *tombstoned_src_buckets;
|
||||||
|
size_t tombstoned_src_len;
|
||||||
|
size_t tombstoned_src_cap;
|
||||||
|
} amduatd_query_index_t;
|
||||||
|
|
||||||
typedef struct amduatd_concepts_t {
|
typedef struct amduatd_concepts_t {
|
||||||
const char *root_path;
|
const char *root_path;
|
||||||
char edges_path[1024];
|
char edges_path[1024];
|
||||||
|
|
@ -36,8 +76,10 @@ typedef struct amduatd_concepts_t {
|
||||||
amduat_reference_t rel_within_domain_ref;
|
amduat_reference_t rel_within_domain_ref;
|
||||||
amduat_reference_t rel_computed_by_ref;
|
amduat_reference_t rel_computed_by_ref;
|
||||||
amduat_reference_t rel_has_provenance_ref;
|
amduat_reference_t rel_has_provenance_ref;
|
||||||
|
amduat_reference_t rel_tombstones_ref;
|
||||||
amduat_asl_collection_store_t edge_collection;
|
amduat_asl_collection_store_t edge_collection;
|
||||||
amduatd_edge_list_t edges;
|
amduatd_edge_list_t edges;
|
||||||
|
amduatd_query_index_t qindex;
|
||||||
} amduatd_concepts_t;
|
} amduatd_concepts_t;
|
||||||
|
|
||||||
bool amduatd_concepts_init(amduatd_concepts_t *c,
|
bool amduatd_concepts_init(amduatd_concepts_t *c,
|
||||||
|
|
@ -51,6 +93,8 @@ void amduatd_concepts_free(amduatd_concepts_t *c);
|
||||||
bool amduatd_concepts_refresh_edges(amduatd_ctx_t *ctx,
|
bool amduatd_concepts_refresh_edges(amduatd_ctx_t *ctx,
|
||||||
size_t max_new_entries);
|
size_t max_new_entries);
|
||||||
|
|
||||||
|
bool amduatd_concepts_ensure_query_index_ready(amduatd_concepts_t *c);
|
||||||
|
|
||||||
bool amduatd_concepts_can_handle(const amduatd_http_req_t *req);
|
bool amduatd_concepts_can_handle(const amduatd_http_req_t *req);
|
||||||
|
|
||||||
bool amduatd_concepts_handle(amduatd_ctx_t *ctx,
|
bool amduatd_concepts_handle(amduatd_ctx_t *ctx,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue