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