Implement amduatd v2 vertical slice and integration harness
This commit is contained in:
commit
7c794d8bab
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Local daemon/store runtime state
|
||||||
|
.amduat-asl/
|
||||||
|
amduatd.sock
|
||||||
|
.cursor
|
||||||
|
|
||||||
|
# Local configuration overrides
|
||||||
|
config/env.local
|
||||||
|
|
||||||
|
# Logs and temp files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Test/runtime artifacts
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# OS/editor metadata
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
59
README.md
Normal file
59
README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# amduat-app-next (Scaffold)
|
||||||
|
|
||||||
|
Starter project scaffold for building an app against `amduatd` v2.
|
||||||
|
|
||||||
|
## Included handoff assets
|
||||||
|
|
||||||
|
- `docs/v2-app-developer-guide.md` (compact app guide)
|
||||||
|
- `contracts/amduatd-api-contract.v2.json` (machine contract)
|
||||||
|
- `scripts/graph_client_helpers.sh` (reusable shell helpers)
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
1. Configure environment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp config/env.example config/env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run startup checks against a running `amduatd` socket:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/bootstrap_check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run sample idempotent batch ingest:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/ingest_example.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run sample changes sync loop:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/sync_loop.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## v2 Vertical Slice CLI
|
||||||
|
|
||||||
|
Use the integrated v2 app flow wrapper:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/v2_app.sh startup-check
|
||||||
|
./scripts/v2_app.sh ingest '{"idempotency_key":"k1","mode":"continue_on_error","nodes":[{"name":"doc:1"}]}'
|
||||||
|
./scripts/v2_app.sh sync-once
|
||||||
|
./scripts/v2_app.sh retrieve 'doc:1' 'ms.within_domain'
|
||||||
|
./scripts/v2_app.sh tombstone '<edge_ref>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Run integration coverage (requires running `amduatd` + `jq`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./tests/integration_v2.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This scaffold assumes local Unix-socket access to `amduatd`.
|
||||||
|
- Graph cursors are opaque and must be persisted exactly as returned.
|
||||||
|
- Keep `contracts/amduatd-api-contract.v2.json` in sync with upstream when you pull updates.
|
||||||
18
config/env.example
Normal file
18
config/env.example
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Copy to config/env.local and edit as needed
|
||||||
|
SOCK="../amduatd.sock"
|
||||||
|
BASE="http://localhost"
|
||||||
|
SPACE="app1"
|
||||||
|
|
||||||
|
# Incremental sync configuration
|
||||||
|
SYNC_LIMIT="200"
|
||||||
|
SYNC_WAIT_MS="15000"
|
||||||
|
CURSOR_FILE=".cursor"
|
||||||
|
|
||||||
|
# Retry policy (applies to retryable server/internal failures)
|
||||||
|
RETRY_MAX_ATTEMPTS="4"
|
||||||
|
RETRY_INITIAL_MS="200"
|
||||||
|
RETRY_MAX_MS="2000"
|
||||||
|
|
||||||
|
# Curl timeouts
|
||||||
|
CURL_CONNECT_TIMEOUT_SECONDS="2"
|
||||||
|
CURL_MAX_TIME_SECONDS="30"
|
||||||
820
contracts/amduatd-api-contract.v2.json
Normal file
820
contracts/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"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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}\"}"
|
||||||
|
```
|
||||||
14
scripts/bootstrap_check.sh
Executable file
14
scripts/bootstrap_check.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ROOT_DIR}/src/client.sh"
|
||||||
|
|
||||||
|
echo "== /v2/readyz =="
|
||||||
|
api_get "/v2/readyz"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== /v2/graph/capabilities =="
|
||||||
|
api_get "/v2/graph/capabilities"
|
||||||
|
echo
|
||||||
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
|
||||||
29
scripts/ingest_example.sh
Executable file
29
scripts/ingest_example.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ROOT_DIR}/src/client.sh"
|
||||||
|
|
||||||
|
payload='{
|
||||||
|
"idempotency_key":"app1-seed-0001",
|
||||||
|
"mode":"continue_on_error",
|
||||||
|
"nodes":[{"name":"doc:1"},{"name":"topic:alpha"}],
|
||||||
|
"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-seed-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
api_post_json "/v2/graph/batch" "${payload}"
|
||||||
|
echo
|
||||||
40
scripts/sync_loop.sh
Executable file
40
scripts/sync_loop.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="${ROOT_DIR}/config/env.local"
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
ENV_FILE="${ROOT_DIR}/config/env.example"
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ENV_FILE}"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ROOT_DIR}/src/client.sh"
|
||||||
|
|
||||||
|
limit="${SYNC_LIMIT:-200}"
|
||||||
|
wait_ms="${SYNC_WAIT_MS:-15000}"
|
||||||
|
cursor_file="${ROOT_DIR}/.cursor"
|
||||||
|
cursor=""
|
||||||
|
|
||||||
|
if [[ -f "${cursor_file}" ]]; then
|
||||||
|
cursor="$(cat "${cursor_file}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if [[ -n "${cursor}" ]]; then
|
||||||
|
path="/v2/graph/changes?since_cursor=${cursor}&limit=${limit}&wait_ms=${wait_ms}"
|
||||||
|
else
|
||||||
|
path="/v2/graph/changes?limit=${limit}&wait_ms=${wait_ms}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
resp="$(api_get "${path}")"
|
||||||
|
echo "${resp}"
|
||||||
|
|
||||||
|
next="$(printf '%s\n' "${resp}" | sed -n 's/.*"next_cursor":"\([^"]*\)".*/\1/p')"
|
||||||
|
if [[ -n "${next}" ]]; then
|
||||||
|
cursor="${next}"
|
||||||
|
printf '%s' "${cursor}" > "${cursor_file}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
67
scripts/v2_app.sh
Executable file
67
scripts/v2_app.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ROOT_DIR}/src/app_v2.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
usage: $0 COMMAND [args]
|
||||||
|
|
||||||
|
commands:
|
||||||
|
startup-check
|
||||||
|
ingest PAYLOAD_JSON
|
||||||
|
sync-once
|
||||||
|
sync-loop
|
||||||
|
retrieve ROOTS_CSV [GOAL_PREDICATES_CSV]
|
||||||
|
tombstone EDGE_REF
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_init
|
||||||
|
|
||||||
|
cmd="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "${cmd}" in
|
||||||
|
startup-check)
|
||||||
|
app_startup_checks
|
||||||
|
;;
|
||||||
|
ingest)
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "usage: $0 ingest PAYLOAD_JSON" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
app_ingest_batch "$1"
|
||||||
|
;;
|
||||||
|
sync-once)
|
||||||
|
app_sync_once
|
||||||
|
;;
|
||||||
|
sync-loop)
|
||||||
|
app_sync_loop
|
||||||
|
;;
|
||||||
|
retrieve)
|
||||||
|
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||||
|
echo "usage: $0 retrieve ROOTS_CSV [GOAL_PREDICATES_CSV]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
app_retrieve_with_fallback "$1" "${2:-}"
|
||||||
|
;;
|
||||||
|
tombstone)
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "usage: $0 tombstone EDGE_REF" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
app_tombstone_edge "$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
138
src/amduat_v2_client.sh
Executable file
138
src/amduat_v2_client.sh
Executable file
|
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/config.sh"
|
||||||
|
|
||||||
|
amduat_client_init() {
|
||||||
|
amduat_config_load
|
||||||
|
}
|
||||||
|
|
||||||
|
amduat_http_raw() {
|
||||||
|
local method="$1"
|
||||||
|
local path="$2"
|
||||||
|
local body="${3:-}"
|
||||||
|
|
||||||
|
local curl_args=(
|
||||||
|
--globoff
|
||||||
|
--silent
|
||||||
|
--show-error
|
||||||
|
--output -
|
||||||
|
--write-out $'\n%{http_code}'
|
||||||
|
--unix-socket "${SOCK}"
|
||||||
|
--connect-timeout "${CURL_CONNECT_TIMEOUT_SECONDS}"
|
||||||
|
--max-time "${CURL_MAX_TIME_SECONDS}"
|
||||||
|
-H "X-Amduat-Space: ${SPACE}"
|
||||||
|
-X "${method}"
|
||||||
|
"${BASE}${path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "${body}" ]]; then
|
||||||
|
curl_args=(
|
||||||
|
--globoff
|
||||||
|
--silent
|
||||||
|
--show-error
|
||||||
|
--output -
|
||||||
|
--write-out $'\n%{http_code}'
|
||||||
|
--unix-socket "${SOCK}"
|
||||||
|
--connect-timeout "${CURL_CONNECT_TIMEOUT_SECONDS}"
|
||||||
|
--max-time "${CURL_MAX_TIME_SECONDS}"
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
-H "X-Amduat-Space: ${SPACE}"
|
||||||
|
-X "${method}"
|
||||||
|
--data-binary "${body}"
|
||||||
|
"${BASE}${path}"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl "${curl_args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_amduat_backoff_sleep() {
|
||||||
|
local delay_ms="$1"
|
||||||
|
local delay_secs
|
||||||
|
delay_secs="$(awk "BEGIN {printf \"%.3f\", ${delay_ms}/1000}")"
|
||||||
|
sleep "${delay_secs}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_amduat_should_retry_status() {
|
||||||
|
local status="$1"
|
||||||
|
case "${status}" in
|
||||||
|
500) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_amduat_non_retryable_status() {
|
||||||
|
local status="$1"
|
||||||
|
case "${status}" in
|
||||||
|
400|404|410|422) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sets global AMDUAT_LAST_STATUS and AMDUAT_LAST_BODY.
|
||||||
|
amduat_api_call() {
|
||||||
|
local method="$1"
|
||||||
|
local path="$2"
|
||||||
|
local body="${3:-}"
|
||||||
|
|
||||||
|
local attempt=1
|
||||||
|
local delay_ms="${RETRY_INITIAL_MS}"
|
||||||
|
AMDUAT_LAST_STATUS="000"
|
||||||
|
AMDUAT_LAST_BODY=""
|
||||||
|
|
||||||
|
while (( attempt <= RETRY_MAX_ATTEMPTS )); do
|
||||||
|
local raw
|
||||||
|
local rc=0
|
||||||
|
raw="$(amduat_http_raw "${method}" "${path}" "${body}")" || rc=$?
|
||||||
|
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
if (( attempt < RETRY_MAX_ATTEMPTS )); then
|
||||||
|
_amduat_backoff_sleep "${delay_ms}"
|
||||||
|
delay_ms=$(( delay_ms * 2 ))
|
||||||
|
if (( delay_ms > RETRY_MAX_MS )); then
|
||||||
|
delay_ms="${RETRY_MAX_MS}"
|
||||||
|
fi
|
||||||
|
attempt=$(( attempt + 1 ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "request failed after retries: ${method} ${path}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
AMDUAT_LAST_STATUS="${raw##*$'\n'}"
|
||||||
|
AMDUAT_LAST_BODY="${raw%$'\n'*}"
|
||||||
|
|
||||||
|
if [[ "${AMDUAT_LAST_STATUS}" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${AMDUAT_LAST_STATUS}" in
|
||||||
|
400) echo "400 bad request for ${method} ${path}" >&2 ;;
|
||||||
|
404) echo "404 not found for ${method} ${path}" >&2 ;;
|
||||||
|
410) echo "410 cursor window expired for ${method} ${path}" >&2 ;;
|
||||||
|
422) echo "422 unprocessable request/result guard for ${method} ${path}" >&2 ;;
|
||||||
|
500) echo "500 internal error for ${method} ${path}" >&2 ;;
|
||||||
|
*) echo "unexpected status ${AMDUAT_LAST_STATUS} for ${method} ${path}" >&2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if _amduat_non_retryable_status "${AMDUAT_LAST_STATUS}"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _amduat_should_retry_status "${AMDUAT_LAST_STATUS}" && (( attempt < RETRY_MAX_ATTEMPTS )); then
|
||||||
|
_amduat_backoff_sleep "${delay_ms}"
|
||||||
|
delay_ms=$(( delay_ms * 2 ))
|
||||||
|
if (( delay_ms > RETRY_MAX_MS )); then
|
||||||
|
delay_ms="${RETRY_MAX_MS}"
|
||||||
|
fi
|
||||||
|
attempt=$(( attempt + 1 ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
103
src/app_v2.sh
Executable file
103
src/app_v2.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/amduat_v2_client.sh"
|
||||||
|
|
||||||
|
app_init() {
|
||||||
|
amduat_client_init
|
||||||
|
}
|
||||||
|
|
||||||
|
app_startup_checks() {
|
||||||
|
amduat_api_call GET "/v2/readyz" || return 1
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
|
||||||
|
amduat_api_call GET "/v2/graph/capabilities" || return 1
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
}
|
||||||
|
|
||||||
|
app_ingest_batch() {
|
||||||
|
local payload="$1"
|
||||||
|
amduat_api_call POST "/v2/graph/batch" "${payload}"
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
}
|
||||||
|
|
||||||
|
app_sync_once() {
|
||||||
|
local path="/v2/graph/changes?limit=${SYNC_LIMIT}&wait_ms=${SYNC_WAIT_MS}"
|
||||||
|
local cursor=""
|
||||||
|
|
||||||
|
if [[ -f "${CURSOR_FILE}" ]]; then
|
||||||
|
cursor="$(cat "${CURSOR_FILE}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${cursor}" ]]; then
|
||||||
|
path+="&since_cursor=${cursor}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! amduat_api_call GET "${path}"; then
|
||||||
|
if [[ "${AMDUAT_LAST_STATUS}" == "410" ]]; then
|
||||||
|
rm -f "${CURSOR_FILE}"
|
||||||
|
echo "changes cursor expired (410); cleared ${CURSOR_FILE}" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
|
||||||
|
local next
|
||||||
|
next="$(printf '%s\n' "${AMDUAT_LAST_BODY}" | sed -n 's/.*"next_cursor"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||||
|
if [[ -n "${next}" ]]; then
|
||||||
|
printf '%s' "${next}" > "${CURSOR_FILE}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
app_sync_loop() {
|
||||||
|
while true; do
|
||||||
|
app_sync_once
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
app_retrieve_with_fallback() {
|
||||||
|
local roots_csv="$1"
|
||||||
|
local goals_csv="${2:-}"
|
||||||
|
|
||||||
|
local roots_json
|
||||||
|
roots_json="$(printf '%s' "${roots_csv}" | awk -F',' 'BEGIN{printf "["} {for(i=1;i<=NF;i++){gsub(/^ +| +$/, "", $i); if (length($i)>0){if (printed) printf ","; printf "\"%s\"", $i; printed=1}}} END{printf "]"}')"
|
||||||
|
|
||||||
|
local goals_json="[]"
|
||||||
|
if [[ -n "${goals_csv}" ]]; then
|
||||||
|
goals_json="$(printf '%s' "${goals_csv}" | awk -F',' 'BEGIN{printf "["} {for(i=1;i<=NF;i++){gsub(/^ +| +$/, "", $i); if (length($i)>0){if (printed) printf ","; printf "\"%s\"", $i; printed=1}}} END{printf "]"}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload="{\"roots\":${roots_json},\"goal_predicates\":${goals_json},\"max_depth\":2,\"max_fanout\":256,\"limit_nodes\":200,\"limit_edges\":400,\"max_result_bytes\":1048576}"
|
||||||
|
|
||||||
|
if amduat_api_call POST "/v2/graph/retrieve" "${payload}"; then
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local first_root
|
||||||
|
first_root="$(printf '%s' "${roots_csv}" | awk -F',' '{gsub(/^ +| +$/, "", $1); printf "%s", $1}')"
|
||||||
|
local fallback_path="/v2/graph/subgraph?roots[]=${first_root}&max_depth=2&dir=outgoing&limit_nodes=200&limit_edges=400&max_result_bytes=1048576"
|
||||||
|
if [[ -n "${goals_csv}" ]]; then
|
||||||
|
local first_goal
|
||||||
|
first_goal="$(printf '%s' "${goals_csv}" | awk -F',' '{gsub(/^ +| +$/, "", $1); printf "%s", $1}')"
|
||||||
|
if [[ -n "${first_goal}" ]]; then
|
||||||
|
fallback_path+="&predicates[]=${first_goal}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
amduat_api_call GET "${fallback_path}"
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
}
|
||||||
|
|
||||||
|
app_tombstone_edge() {
|
||||||
|
local edge_ref="$1"
|
||||||
|
local payload
|
||||||
|
payload="{\"edge_ref\":\"${edge_ref}\"}"
|
||||||
|
amduat_api_call POST "/v2/graph/edges/tombstone" "${payload}"
|
||||||
|
printf '%s\n' "${AMDUAT_LAST_BODY}"
|
||||||
|
}
|
||||||
34
src/client.sh
Executable file
34
src/client.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="${ROOT_DIR}/config/env.local"
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
ENV_FILE="${ROOT_DIR}/config/env.example"
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ENV_FILE}"
|
||||||
|
|
||||||
|
SOCK="${SOCK:-../amduatd.sock}"
|
||||||
|
BASE="${BASE:-http://localhost}"
|
||||||
|
SPACE="${SPACE:-app1}"
|
||||||
|
|
||||||
|
api_get() {
|
||||||
|
local path="$1"
|
||||||
|
curl --silent --show-error --fail \
|
||||||
|
--unix-socket "${SOCK}" \
|
||||||
|
-H "X-Amduat-Space: ${SPACE}" \
|
||||||
|
"${BASE}${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_post_json() {
|
||||||
|
local path="$1"
|
||||||
|
local body="$2"
|
||||||
|
curl --silent --show-error --fail \
|
||||||
|
--unix-socket "${SOCK}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Amduat-Space: ${SPACE}" \
|
||||||
|
-X POST \
|
||||||
|
--data-binary "${body}" \
|
||||||
|
"${BASE}${path}"
|
||||||
|
}
|
||||||
55
src/config.sh
Executable file
55
src/config.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
amduat_config_load() {
|
||||||
|
local root_dir
|
||||||
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
local override_sock="${SOCK:-}"
|
||||||
|
local override_base="${BASE:-}"
|
||||||
|
local override_space="${SPACE:-}"
|
||||||
|
local override_sync_limit="${SYNC_LIMIT:-}"
|
||||||
|
local override_sync_wait_ms="${SYNC_WAIT_MS:-}"
|
||||||
|
local override_cursor_file="${CURSOR_FILE:-}"
|
||||||
|
local override_retry_max_attempts="${RETRY_MAX_ATTEMPTS:-}"
|
||||||
|
local override_retry_initial_ms="${RETRY_INITIAL_MS:-}"
|
||||||
|
local override_retry_max_ms="${RETRY_MAX_MS:-}"
|
||||||
|
local override_connect_timeout="${CURL_CONNECT_TIMEOUT_SECONDS:-}"
|
||||||
|
local override_max_time="${CURL_MAX_TIME_SECONDS:-}"
|
||||||
|
|
||||||
|
local env_file="${root_dir}/config/env.local"
|
||||||
|
if [[ ! -f "${env_file}" ]]; then
|
||||||
|
env_file="${root_dir}/config/env.example"
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${env_file}"
|
||||||
|
|
||||||
|
if [[ -n "${override_sock}" ]]; then SOCK="${override_sock}"; fi
|
||||||
|
if [[ -n "${override_base}" ]]; then BASE="${override_base}"; fi
|
||||||
|
if [[ -n "${override_space}" ]]; then SPACE="${override_space}"; fi
|
||||||
|
if [[ -n "${override_sync_limit}" ]]; then SYNC_LIMIT="${override_sync_limit}"; fi
|
||||||
|
if [[ -n "${override_sync_wait_ms}" ]]; then SYNC_WAIT_MS="${override_sync_wait_ms}"; fi
|
||||||
|
if [[ -n "${override_cursor_file}" ]]; then CURSOR_FILE="${override_cursor_file}"; fi
|
||||||
|
if [[ -n "${override_retry_max_attempts}" ]]; then RETRY_MAX_ATTEMPTS="${override_retry_max_attempts}"; fi
|
||||||
|
if [[ -n "${override_retry_initial_ms}" ]]; then RETRY_INITIAL_MS="${override_retry_initial_ms}"; fi
|
||||||
|
if [[ -n "${override_retry_max_ms}" ]]; then RETRY_MAX_MS="${override_retry_max_ms}"; fi
|
||||||
|
if [[ -n "${override_connect_timeout}" ]]; then CURL_CONNECT_TIMEOUT_SECONDS="${override_connect_timeout}"; fi
|
||||||
|
if [[ -n "${override_max_time}" ]]; then CURL_MAX_TIME_SECONDS="${override_max_time}"; fi
|
||||||
|
|
||||||
|
SOCK="${SOCK:-../amduatd.sock}"
|
||||||
|
BASE="${BASE:-http://localhost}"
|
||||||
|
SPACE="${SPACE:-app1}"
|
||||||
|
|
||||||
|
SYNC_LIMIT="${SYNC_LIMIT:-200}"
|
||||||
|
SYNC_WAIT_MS="${SYNC_WAIT_MS:-15000}"
|
||||||
|
CURSOR_FILE="${CURSOR_FILE:-${root_dir}/.cursor}"
|
||||||
|
if [[ "${CURSOR_FILE}" != /* ]]; then
|
||||||
|
CURSOR_FILE="${root_dir}/${CURSOR_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RETRY_MAX_ATTEMPTS="${RETRY_MAX_ATTEMPTS:-4}"
|
||||||
|
RETRY_INITIAL_MS="${RETRY_INITIAL_MS:-200}"
|
||||||
|
RETRY_MAX_MS="${RETRY_MAX_MS:-2000}"
|
||||||
|
|
||||||
|
CURL_CONNECT_TIMEOUT_SECONDS="${CURL_CONNECT_TIMEOUT_SECONDS:-2}"
|
||||||
|
CURL_MAX_TIME_SECONDS="${CURL_MAX_TIME_SECONDS:-30}"
|
||||||
|
}
|
||||||
100
tests/integration_v2.sh
Executable file
100
tests/integration_v2.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${ROOT_DIR}/src/app_v2.sh"
|
||||||
|
|
||||||
|
require_jq() {
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "jq is required for integration_v2.sh" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local needle="$2"
|
||||||
|
if [[ "${haystack}" != *"${needle}"* ]]; then
|
||||||
|
echo "assertion failed: expected to find ${needle}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
app_init
|
||||||
|
require_jq
|
||||||
|
if [[ ! -S "${SOCK}" ]]; then
|
||||||
|
echo "integration_v2.sh: SKIP (socket not found at ${SOCK})"
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1) startup checks
|
||||||
|
startup_out="$(app_startup_checks)"
|
||||||
|
assert_contains "${startup_out}" '"ok"'
|
||||||
|
|
||||||
|
# 2) idempotent ingest (batch + continue_on_error)
|
||||||
|
run_id="$(date +%s)"
|
||||||
|
trace_id="trace-it-${run_id}"
|
||||||
|
idempotency_key="it-seed-${run_id}"
|
||||||
|
doc_name="doc:it${run_id}"
|
||||||
|
topic_name="topic:italpha${run_id}"
|
||||||
|
payload="$(cat <<JSON
|
||||||
|
{
|
||||||
|
"idempotency_key":"${idempotency_key}",
|
||||||
|
"mode":"continue_on_error",
|
||||||
|
"nodes":[{"name":"${doc_name}"},{"name":"${topic_name}"}],
|
||||||
|
"edges":[
|
||||||
|
{
|
||||||
|
"subject":"${doc_name}",
|
||||||
|
"predicate":"ms.within_domain",
|
||||||
|
"object":"${topic_name}",
|
||||||
|
"provenance":{
|
||||||
|
"source_uri":"urn:test:seed",
|
||||||
|
"extractor":"integration-test",
|
||||||
|
"observed_at":1,
|
||||||
|
"ingested_at":2,
|
||||||
|
"trace_id":"${trace_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
)"
|
||||||
|
ingest_out="$(app_ingest_batch "${payload}")"
|
||||||
|
assert_contains "${ingest_out}" '"ok":true'
|
||||||
|
|
||||||
|
# Re-submit same idempotency key + identical payload.
|
||||||
|
ingest_out_2="$(app_ingest_batch "${payload}")"
|
||||||
|
assert_contains "${ingest_out_2}" '"idempotency_key"'
|
||||||
|
|
||||||
|
# 3) incremental sync with durable opaque cursor
|
||||||
|
rm -f "${CURSOR_FILE}"
|
||||||
|
sync_out="$(app_sync_once)"
|
||||||
|
assert_contains "${sync_out}" '"events"'
|
||||||
|
[[ -s "${CURSOR_FILE}" ]] || { echo "cursor file not persisted" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 4) retrieval endpoint + fallback path available
|
||||||
|
retrieve_out="$(app_retrieve_with_fallback "${doc_name}" "ms.within_domain")"
|
||||||
|
assert_contains "${retrieve_out}" '"edges"'
|
||||||
|
|
||||||
|
# Capture edge_ref using subgraph surface to avoid format differences.
|
||||||
|
subgraph_out="$(amduat_api_call GET "/v2/graph/subgraph?roots[]=${doc_name}&max_depth=2&dir=outgoing&limit_nodes=200&limit_edges=400&include_stats=true&max_result_bytes=1048576" && printf '%s' "${AMDUAT_LAST_BODY}")"
|
||||||
|
edge_ref="$(printf '%s' "${subgraph_out}" | jq -r '.edges[0].edge_ref // empty')"
|
||||||
|
if [[ -z "${edge_ref}" ]]; then
|
||||||
|
echo "failed to resolve edge_ref" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5) correction path and tombstone visibility semantics
|
||||||
|
app_tombstone_edge "${edge_ref}" >/dev/null
|
||||||
|
post_tombstone_retrieve="$(app_retrieve_with_fallback "${doc_name}" "ms.within_domain")"
|
||||||
|
post_edges_count="$(printf '%s' "${post_tombstone_retrieve}" | jq '.edges | length')"
|
||||||
|
if [[ "${post_edges_count}" != "0" ]]; then
|
||||||
|
echo "expected retrieval default to hide tombstoned edges" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
visible_tombstone="$(amduat_api_call GET "/v2/graph/subgraph?roots[]=${doc_name}&max_depth=2&dir=outgoing&limit_nodes=200&limit_edges=400&include_tombstoned=true&max_result_bytes=1048576" && printf '%s' "${AMDUAT_LAST_BODY}")"
|
||||||
|
assert_contains "${visible_tombstone}" '"edges"'
|
||||||
|
|
||||||
|
echo "integration_v2.sh: PASS"
|
||||||
Loading…
Reference in a new issue