Compare commits

...

12 commits

Author SHA1 Message Date
Carl Niklas Rydberg 0ae2c8d74a jhioåf 2026-02-08 09:54:50 +01:00
Carl Niklas Rydberg 4fa7c32117 Advance amduat to suppress snapshot warning flood 2026-02-08 09:01:20 +01:00
Carl Niklas Rydberg 05565f0c45 Advance amduat to index recovery and stale-head healing fixes 2026-02-08 08:55:46 +01:00
Carl Niklas Rydberg 2386449da7 Pin amduat lock-file and edge-append hardening commits 2026-02-08 08:48:43 +01:00
Carl Niklas Rydberg b6e2724c5a I dont know 2026-02-08 08:28:28 +01:00
Carl Niklas Rydberg ce7d852e71 Restore vendor/amduat pointer to available main commit 2026-02-08 08:14:14 +01:00
Carl Niklas Rydberg a54a42c0bf fixed index locck file error
(cherry picked from commit a8a2ab1efb)
2026-02-08 08:13:10 +01:00
Carl Niklas Rydberg 59be5aee7d Add index backend startup regression coverage and update test script paths
(cherry picked from commit 81c1115db5)
2026-02-08 08:13:10 +01:00
Carl Niklas Rydberg 7fab7d2e47 Merge branch 'apiv2' 2026-02-08 07:44:01 +01:00
Carl Niklas Rydberg cb91cc1569 some tests 2026-02-08 07:33:38 +01:00
Carl Niklas Rydberg cdc00dafc2 some fix 2026-02-08 07:32:20 +01:00
Carl Niklas Rydberg b8c0a6e6d0 Implement v2 graph API surface and contract/test coverage 2026-02-07 19:46:59 +01:00
18 changed files with 17562 additions and 26 deletions

View file

@ -327,6 +327,31 @@ add_test(NAME amduatd_fed_ingest
) )
set_tests_properties(amduatd_fed_ingest PROPERTIES SKIP_RETURN_CODE 77) set_tests_properties(amduatd_fed_ingest PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_queries
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_queries.sh
)
set_tests_properties(amduatd_graph_queries PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_contract
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_contract.sh
)
set_tests_properties(amduatd_graph_contract PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_index_append
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_index_append.sh
)
set_tests_properties(amduatd_graph_index_append PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_index_append_stress
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_index_append_stress.sh
)
set_tests_properties(amduatd_graph_index_append_stress PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_index_two_nodes
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_index_two_nodes.sh
)
set_tests_properties(amduatd_index_two_nodes PROPERTIES SKIP_RETURN_CODE 77)
add_executable(amduatd_test_space_doctor add_executable(amduatd_test_space_doctor
tests/test_amduatd_space_doctor.c tests/test_amduatd_space_doctor.c
src/amduatd_space_doctor.c src/amduatd_space_doctor.c

101
README.md
View file

@ -2,6 +2,13 @@
`amduat-api` builds `amduatd`, a minimal HTTP server over a Unix domain socket that exposes Amduat substrate operations for a **single ASL store root**. `amduat-api` builds `amduatd`, a minimal HTTP server over a Unix domain socket that exposes Amduat substrate operations for a **single ASL store root**.
## App Developer Handoff
For a compact, implementation-focused guide for external app teams, use:
- `docs/v2-app-developer-guide.md`
- `registry/amduatd-api-contract.v2.json` (machine-readable contract)
## Build ## Build
```sh ```sh
@ -444,6 +451,57 @@ curl --unix-socket amduatd.sock -X POST http://localhost/v1/pel/run \
-d '{"program_ref":"<program_ref>","input_refs":["<input_ref_0>"],"params_ref":"<params_ref>"}' -d '{"program_ref":"<program_ref>","input_refs":["<input_ref_0>"],"params_ref":"<params_ref>"}'
``` ```
Run a v2 PEL execute request with inline artifact ingest:
```sh
curl --unix-socket amduatd.sock -X POST http://localhost/v2/pel/execute \
-H 'Content-Type: application/json' \
-d '{
"scheme_ref":"dag",
"program_ref":"<program_ref>",
"inputs":{
"refs":["<input_ref_0>"],
"inline_artifacts":[{"body_hex":"48656c6c6f2c20763221","type_tag":"0x00000000"}]
},
"receipt":{
"input_manifest_ref":"<manifest_ref>",
"environment_ref":"<env_ref>",
"evaluator_id":"local-amduatd",
"executor_ref":"<executor_ref>",
"started_at":1731000000,
"completed_at":1731000001
}
}'
```
The v2 execute response returns `run_ref` (and `result_ref` alias),
`receipt_ref`, `stored_input_refs[]`, `output_refs[]`, and `status`.
Simplified async v2 operations (PEL-backed under the hood):
```sh
# put (returns job_id)
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/put \
-H 'Content-Type: application/json' \
-d '{"body_hex":"48656c6c6f","type_tag":"0x00000000"}'
# concat (returns job_id)
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/concat \
-H 'Content-Type: application/json' \
-d '{"left_ref":"<ref_a>","right_ref":"<ref_b>"}'
# slice (returns job_id)
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/slice \
-H 'Content-Type: application/json' \
-d '{"ref":"<ref>","offset":1,"length":3}'
# poll job
curl --unix-socket amduatd.sock http://localhost/v2/jobs/1
# get bytes
curl --unix-socket amduatd.sock http://localhost/v2/get/<ref>
```
When derivation indexing is enabled, successful PEL runs record derivations under When derivation indexing is enabled, successful PEL runs record derivations under
`<root>/index/derivations/by_artifact/` keyed by output refs (plus result/trace/receipt refs). `<root>/index/derivations/by_artifact/` keyed by output refs (plus result/trace/receipt refs).
@ -560,6 +618,49 @@ When the daemon uses the `fs` store backend, index-only checks are reported as
- request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (`program_ref`/`input_refs`/`params_ref` accept hex refs or concept names; omit `scheme_ref` to use `dag`) - request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (`program_ref`/`input_refs`/`params_ref` accept hex refs or concept names; omit `scheme_ref` to use `dag`)
- request receipt (optional): `{receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at, sbom_ref?, parity_digest_hex?, executor_fingerprint_ref?, run_id_hex?, limits?, logs?, determinism_level?, rng_seed_hex?, signature_hex?}}` - request receipt (optional): `{receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at, sbom_ref?, parity_digest_hex?, executor_fingerprint_ref?, run_id_hex?, limits?, logs?, determinism_level?, rng_seed_hex?, signature_hex?}}`
- response: `{result_ref, trace_ref?, receipt_ref?, output_refs[], status}` - response: `{result_ref, trace_ref?, receipt_ref?, output_refs[], status}`
- `POST /v2/pel/execute` (first v2 slice)
- request: `{program_ref, scheme_ref?, params_ref?, inputs:{refs[], inline_artifacts:[{body_hex, type_tag?}]}, receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at}}`
- response: `{run_ref, result_ref, trace_ref?, receipt_ref, stored_input_refs[], output_refs[], status}`
- `POST /v2/ops/put` → async enqueue `{job_id, status}`
- request: `{body_hex, type_tag?}`
- `POST /v2/ops/concat` → async enqueue `{job_id, status}`
- request: `{left_ref, right_ref}` (refs or concept names)
- `POST /v2/ops/slice` → async enqueue `{job_id, status}`
- request: `{ref, offset, length}` (`ref` accepts ref or concept name)
- `GET /v2/jobs/{id}``{job_id, kind, status, created_at_ms, started_at_ms, completed_at_ms, result_ref, error}`
- `GET /v2/get/{ref}` → artifact bytes (alias to `/v1/artifacts/{ref}`)
- `GET /v2/healthz` → liveness probe `{ok, status, time_ms}`
- `GET /v2/readyz` → readiness probe `{ok, status, components:{graph_index, federation}}` (`503` when not ready)
- `GET /v2/metrics` → Prometheus text metrics (`text/plain; version=0.0.4`)
- `POST /v2/graph/nodes` → create/seed graph node via concept `{name, ref?}`
- `POST /v2/graph/nodes/{name}/versions` → publish node version `{ref, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive)
- `POST /v2/graph/nodes/{name}/versions/tombstone` → tombstone a previously published node version `{ref, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive; append-only)
- `GET /v2/graph/nodes/{name}/versions?as_of=&include_tombstoned=` → get node versions (same shape as node read; defaults hide tombstoned versions from `latest_ref` and `versions[]`; set `include_tombstoned=true` to include them)
- `GET /v2/graph/nodes/{name}/neighbors?dir=&predicate=&limit=&cursor=&as_of=&provenance_ref=&include_tombstoned=&expand_names=&expand_artifacts=` → neighbor scan (paged, snapshot-bounded; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; add names/latest refs via expansion flags)
- `GET /v2/graph/search?name_prefix=&limit=&cursor=&as_of=` → search nodes by name prefix (paged, snapshot-bounded)
- `GET /v2/graph/paths?from=&to=&max_depth=&predicate=&as_of=&k=&expand_names=&expand_artifacts=&include_tombstoned=&provenance_ref=&max_fanout=&max_result_bytes=&include_stats=` → shortest directed path query (bounded BFS, snapshot-bounded, returns up to `k` paths with hop metadata and optional names/latest refs; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; supports fanout/response-size guards and optional stats)
- `GET /v2/graph/subgraph?roots[]=&max_depth=&max_fanout=&predicates[]=&dir=&as_of=&include_versions=&include_tombstoned=&limit_nodes=&limit_edges=&cursor=&provenance_ref=&max_result_bytes=&include_stats=` → bounded multi-hop subgraph retrieval from roots (snapshot-bounded, paged by opaque `next_cursor`; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; supports fanout/response-size guards and optional stats; returns `{nodes[], edges[], frontier?[], next_cursor, truncated}`)
- `POST /v2/graph/edges` → append graph edge `{subject, predicate, object, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive)
- `POST /v2/graph/edges/tombstone` → append tombstone for an existing edge `{edge_ref, metadata_ref?|provenance?}` (append-only correction/retraction; `metadata_ref` and `provenance` are mutually exclusive)
- `POST /v2/graph/batch` → apply mixed graph mutations `{idempotency_key?, mode?, nodes?, versions?, edges?}` (versions/edges items support `metadata_ref?|provenance?`; mode: `fail_fast|continue_on_error`; deterministic replay for repeated `idempotency_key` + identical payload; returns `{ok, applied, results[]}` with per-item status/error)
- `POST /v2/graph/query` → unified graph query `{where?, predicates?, direction?, include_versions?, include_tombstoned?, include_stats?, max_result_bytes?, as_of?, limit?, cursor?}` returning `{nodes[], edges[], paging, stats?}` (`nodes[].versions[]` included when `include_versions=true`; `where.provenance_ref` filters edges linked via `ms.has_provenance`; tombstoned edges are excluded as-of snapshot unless `include_tombstoned=true`)
- `POST /v2/graph/retrieve` → agent-oriented bounded retrieval `{roots[], goal_predicates?[], max_depth?, as_of?, provenance_min_confidence?, include_versions?, include_tombstoned?, max_fanout?, limit_nodes?, limit_edges?, max_result_bytes?}` returning `{nodes[], edges[], explanations[], truncated, stats}` (multi-hop traversal from roots with optional predicate targeting and provenance-confidence gating; each explanation includes edge inclusion reasons and traversal depth)
- `POST /v2/graph/export` → paged graph envelope export `{as_of?, cursor?, limit?, predicates?[], roots?[], include_tombstoned?, max_result_bytes?}` returning `{items[], next_cursor, has_more, snapshot_as_of, stats}` (`items[]` preserve edge refs/order plus predicate, tombstone flag, and attached `metadata_ref` when present)
- `POST /v2/graph/import` → ordered graph envelope replay `{mode?, items[]}` (`mode=fail_fast|continue_on_error`) returning `{ok, applied, results[]}` with per-item `index/status/code/error/edge_ref`
- `GET /v2/graph/schema/predicates` → current graph governance policy `{mode, provenance_mode, predicates[]}` where `mode` is `strict|warn|off` and `provenance_mode` is `optional|required`
- `POST /v2/graph/schema/predicates` → update graph governance `{mode?, provenance_mode?, predicates?[]}` (`predicates[]` entries accept `{predicate_ref|predicate, domain?, range?}`; predicate validation mode is enforced for edge writes in `POST /v2/graph/edges`, batch edge items, and graph import edge items; `provenance_mode=required` enforces provenance attachment (`metadata_ref` or `provenance`) for version/edge/tombstone writes; policy is persisted per space root)
- `GET /v2/graph/stats` → graph/index health summary `{edges_total, aliases_total, index{...}, tombstones{edges, ratio}}`
- `GET /v2/graph/capabilities` → graph feature/version negotiation payload `{contract, graph{version, features, limits, modes}, runtime{...}}`
- `GET /v2/graph/changes?since_cursor=&since_as_of=&limit=&wait_ms=&event_types[]=&predicates[]=&roots[]=` → incremental appended graph events with strict cursor replay window (returns `410` when cursor falls outside retained window), resumable cursor tokens, optional long-poll (`wait_ms`), and event/predicate/root filters; response `{events[], next_cursor, has_more}`
- `GET /v2/graph/edges?subject=&predicate=&object=&dir=&limit=&cursor=&as_of=&expand_names=&expand_artifacts=&provenance_ref=&include_tombstoned=&max_result_bytes=&include_stats=` → filtered edge scan with paging (`dir=any|outgoing|incoming`, snapshot-bounded; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; add names/latest refs via expansion flags; supports response-size guards and optional stats)
- graph paging cursors are opaque tokens (`g1_*`); legacy numeric cursors are still accepted
- `GET /v2/graph/nodes/{name}?as_of=&include_tombstoned=``{name, concept_ref, latest_ref, versions[], outgoing[], incoming[]}` (`latest_ref` is resolved from visible versions at `as_of`; tombstoned versions excluded by default)
- `GET /v2/graph/history/{name}?as_of=&include_tombstoned=` → event timeline for node versions/edges (snapshot-bounded; `latest_ref` and tombstoned fact events both respect same visibility rules)
Graph client helper examples for agent flows are in `scripts/graph_client_helpers.sh`:
- `batch-ingest` (idempotent batch write helper)
- `sync-once` (incremental changes cursor step)
- `subgraph` (bounded retrieval helper)
- `POST /v1/pel/programs` - `POST /v1/pel/programs`
- request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes) - request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes)
- response: `{program_ref}` - response: `{program_ref}`

View file

@ -0,0 +1,196 @@
# amduatd API v2 Design (PEL-only writes)
## Status
Draft proposal for discussion.
First server slice implemented: `POST /v2/pel/execute` with `inputs.refs`,
`inputs.inline_artifacts[{body_hex,type_tag?}]`, and required `receipt`.
## Goals
- Keep `amduatd` thin: transport, parsing, auth, and mapping to core calls.
- Make **PEL the only write primitive** in HTTP v2.
- Enforce artifact provenance for writes: adding new artifacts must go through:
1. store inputs,
2. run a PEL program,
3. persist and return receipt.
- Keep `/v1/*` behavior unchanged while v2 is introduced.
## Non-goals
- Re-implementing PEL semantics in `amduat-api`.
- Creating new substrate semantics in daemon code.
- Removing v1 immediately.
## High-level model
v2 separates read and write concerns:
- Read paths remain direct and deterministic (`GET /v2/meta`, `GET /v2/artifacts/{ref}`, federation/space read endpoints).
- All state-changing operations are executed as PEL runs.
In v2, `POST /v1/artifacts` equivalent is replaced by a mutation endpoint that always drives a PEL run.
For simplified clients, v2 also exposes async operation endpoints:
- `POST /v2/ops/put`
- `POST /v2/ops/concat`
- `POST /v2/ops/slice`
- `GET /v2/jobs/{id}`
These enqueue work and execute PEL in the daemon background loop.
Graph-oriented endpoints can be added as thin wrappers over concepts/relations:
- `POST /v2/graph/nodes` (maps to concept create/publish)
- `POST /v2/graph/edges` (maps to relation edge append)
- `GET /v2/graph/nodes/{name}` (node + versions + incoming/outgoing edges)
- `GET /v2/graph/history/{name}` (version + edge timeline)
## Versioning and contract id
- Base path: `/v2`
- Contract id: `AMDUATD/API/2`
- Keep the same store-seeded contract model used in v1.
## Endpoint shape
### Read-only endpoints (v2)
- `GET /v2/meta`
- `GET /v2/contract`
- `GET /v2/artifacts/{ref}`
- `HEAD /v2/artifacts/{ref}`
- `GET /v2/artifacts/{ref}?format=info`
- Space/federation read endpoints can be mirrored under `/v2/*` unchanged where possible.
### Write endpoints (v2)
- `POST /v2/pel/execute`
No direct artifact write endpoint in v2 (`POST /v2/artifacts` is intentionally absent).
## `POST /v2/pel/execute`
Single mutation primitive for v2.
### Request
```json
{
"program_ref": "<hex-ref-or-name>",
"scheme_ref": "dag",
"params_ref": "<optional-ref-or-name>",
"inputs": {
"refs": ["<hex-ref-or-name>"],
"inline_artifacts": [
{
"content_type": "application/octet-stream",
"type_tag": "0x00000000",
"body_hex": "48656c6c6f"
}
]
},
"receipt": {
"input_manifest_ref": "<ref-or-name>",
"environment_ref": "<ref-or-name>",
"evaluator_id": "local-amduatd",
"executor_ref": "<ref-or-name>",
"started_at": 1731000000,
"completed_at": 1731000005
},
"effects": {
"publish_outputs": true,
"append_fed_log": true
}
}
```
### Processing contract
Server behavior is deterministic and ordered:
1. Resolve `program_ref`, `scheme_ref`, `params_ref`, and `inputs.refs` to refs.
2. Decode and store `inputs.inline_artifacts` into ASL store.
3. Build effective `input_refs = refs + stored_inline_refs`.
4. Execute PEL using core `pel_surf_run_with_result`.
5. Build FER receipt from run result + receipt fields.
6. Store receipt artifact and return `receipt_ref`.
7. Apply requested side effects (`publish_outputs`, `append_fed_log`) after successful run.
If any step fails:
- return non-2xx,
- do not publish outputs,
- do not append federation publish records.
## Response
```json
{
"run_ref": "<pel1-result-ref>",
"trace_ref": "<optional-trace-ref>",
"receipt_ref": "<fer1-receipt-ref>",
"stored_input_refs": ["<ref>"],
"output_refs": ["<ref>"],
"status": "OK"
}
```
Notes:
- `stored_input_refs` are refs created from `inline_artifacts`.
- `receipt_ref` is required for successful writes in v2.
## Error model
- `400` invalid request or schema violation.
- `404` referenced input/program/params not found.
- `409` conflict in post-run side effects (e.g. cursor/update conflicts).
- `422` run completed but business policy failed (reserved for policy checks).
- `500` internal failure.
Error payload:
```json
{
"error": {
"code": "invalid_input_ref",
"message": "input ref not found",
"retryable": false
}
}
```
## Behavioral differences from v1
- Removed write path: direct `POST /artifacts`.
- New requirement: every write returns a receipt ref tied to a PEL run.
- `pel/run` semantics become the write gateway rather than an optional compute endpoint.
## Compatibility and rollout
1. Add `/v2/pel/execute` while keeping `/v1/*` unchanged.
2. Mirror core read endpoints under `/v2/*`.
3. Mark `POST /v1/artifacts` as deprecated in docs.
4. Migrate clients to v2 execute flow.
5. Remove v1 write endpoints in a later major release.
## Open questions
- Should v2 require exactly one output for write operations, or allow multiple outputs with one receipt?
- Should inline artifact body support both `body_hex` and `body_base64`, or move to multipart for large payloads?
- Should `publish_outputs` default to `true` or be explicit-only?
- Do we need async execution (`202 + run status endpoint`) for long-running programs in v2?
## Minimal implementation plan
1. Add v2 contract bytes (`registry/amduatd-api-contract.v2.json`).
2. Add `/v2/pel/execute` handler by adapting existing `POST /v1/pel/run` + artifact ingest path.
3. Factor shared receipt parsing/building from current v1 pel handler.
4. Add tests:
- inline artifact -> stored -> run -> receipt success,
- failure before run,
- failure during run,
- side effect gating (no publish on failure).
5. Document deprecation notice for `POST /v1/artifacts`.

View 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}\"}"
```

View file

@ -17,6 +17,7 @@ acts as the version identifier.
- `api-contract.schema.md` — JSONL manifest schema for API contracts. - `api-contract.schema.md` — JSONL manifest schema for API contracts.
- `api-contract.jsonl` — manifest of published contracts. - `api-contract.jsonl` — manifest of published contracts.
- `amduatd-api-contract.v1.json` — contract bytes (v1). - `amduatd-api-contract.v1.json` — contract bytes (v1).
- `amduatd-api-contract.v2.json` — draft contract bytes (v2, PEL-only writes).
Receipt note: Receipt note:
- `/v1/pel/run` accepts optional receipt v1.1 fields (executor fingerprint, run id, - `/v1/pel/run` accepts optional receipt v1.1 fields (executor fingerprint, run id,

View file

@ -0,0 +1,820 @@
{
"contract": "AMDUATD/API/2",
"base_path": "/v2",
"notes": "Draft v2: PEL-only write surface. Direct artifact write endpoint removed.",
"endpoints": [
{"method": "GET", "path": "/v2/meta"},
{"method": "HEAD", "path": "/v2/meta"},
{"method": "GET", "path": "/v2/contract"},
{"method": "GET", "path": "/v2/healthz"},
{"method": "GET", "path": "/v2/readyz"},
{"method": "GET", "path": "/v2/metrics"},
{"method": "GET", "path": "/v2/artifacts/{ref}"},
{"method": "HEAD", "path": "/v2/artifacts/{ref}"},
{"method": "GET", "path": "/v2/artifacts/{ref}?format=info"},
{"method": "POST", "path": "/v2/pel/execute"},
{"method": "POST", "path": "/v2/ops/put"},
{"method": "POST", "path": "/v2/ops/concat"},
{"method": "POST", "path": "/v2/ops/slice"},
{"method": "GET", "path": "/v2/jobs/{id}"},
{"method": "GET", "path": "/v2/get/{ref}"},
{"method": "POST", "path": "/v2/graph/nodes"},
{"method": "POST", "path": "/v2/graph/nodes/{name}/versions"},
{"method": "POST", "path": "/v2/graph/nodes/{name}/versions/tombstone"},
{"method": "GET", "path": "/v2/graph/nodes/{name}/versions"},
{"method": "GET", "path": "/v2/graph/nodes/{name}/neighbors"},
{"method": "GET", "path": "/v2/graph/search"},
{"method": "GET", "path": "/v2/graph/paths"},
{"method": "GET", "path": "/v2/graph/subgraph"},
{"method": "POST", "path": "/v2/graph/edges"},
{"method": "POST", "path": "/v2/graph/edges/tombstone"},
{"method": "POST", "path": "/v2/graph/batch"},
{"method": "POST", "path": "/v2/graph/query"},
{"method": "POST", "path": "/v2/graph/retrieve"},
{"method": "POST", "path": "/v2/graph/export"},
{"method": "POST", "path": "/v2/graph/import"},
{"method": "GET", "path": "/v2/graph/schema/predicates"},
{"method": "POST", "path": "/v2/graph/schema/predicates"},
{"method": "GET", "path": "/v2/graph/stats"},
{"method": "GET", "path": "/v2/graph/capabilities"},
{"method": "GET", "path": "/v2/graph/changes"},
{"method": "GET", "path": "/v2/graph/edges"},
{"method": "GET", "path": "/v2/graph/nodes/{name}"},
{"method": "GET", "path": "/v2/graph/history/{name}"}
],
"schemas": {
"job_enqueue_response": {
"type": "object",
"required": ["job_id", "status"],
"properties": {
"job_id": {"type": "integer"},
"status": {"type": "string"}
}
},
"job_status_response": {
"type": "object",
"required": ["job_id", "kind", "status", "created_at_ms"],
"properties": {
"job_id": {"type": "integer"},
"kind": {"type": "string"},
"status": {"type": "string"},
"created_at_ms": {"type": "integer"},
"started_at_ms": {"type": ["integer", "null"]},
"completed_at_ms": {"type": ["integer", "null"]},
"result_ref": {"type": ["string", "null"]},
"error": {"type": ["string", "null"]}
}
},
"healthz_response": {
"type": "object",
"required": ["ok", "status", "time_ms"],
"properties": {
"ok": {"type": "boolean"},
"status": {"type": "string"},
"time_ms": {"type": "integer"}
}
},
"readyz_response": {
"type": "object",
"required": ["ok", "status", "components"],
"properties": {
"ok": {"type": "boolean"},
"status": {"type": "string"},
"components": {
"type": "object",
"required": ["graph_index", "federation"],
"properties": {
"graph_index": {"type": "boolean"},
"federation": {"type": "boolean"}
}
}
}
},
"put_request": {
"type": "object",
"required": ["body_hex"],
"properties": {
"body_hex": {"type": "string"},
"type_tag": {"type": "string"}
}
},
"concat_request": {
"type": "object",
"required": ["left_ref", "right_ref"],
"properties": {
"left_ref": {"type": "string"},
"right_ref": {"type": "string"}
}
},
"slice_request": {
"type": "object",
"required": ["ref", "offset", "length"],
"properties": {
"ref": {"type": "string"},
"offset": {"type": "integer"},
"length": {"type": "integer"}
}
},
"graph_node_create_request": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"ref": {"type": "string", "description": "optional initial published ref"}
}
},
"graph_node_create_response": {
"type": "object",
"required": ["name", "concept_ref"],
"properties": {
"name": {"type": "string"},
"concept_ref": {"type": "string"}
}
},
"graph_provenance": {
"type": "object",
"required": ["source_uri", "extractor", "observed_at", "ingested_at", "trace_id"],
"properties": {
"source_uri": {"type": "string"},
"extractor": {"type": "string"},
"confidence": {"type": ["string", "number", "integer"]},
"observed_at": {"type": "integer"},
"ingested_at": {"type": "integer"},
"license": {"type": "string"},
"trace_id": {"type": "string"}
}
},
"graph_edge_create_request": {
"type": "object",
"required": ["subject", "predicate", "object"],
"properties": {
"subject": {"type": "string", "description": "concept name or hex ref"},
"predicate": {"type": "string", "description": "relation alias/name or hex ref"},
"object": {"type": "string", "description": "concept name or hex ref"},
"metadata_ref": {"type": "string", "description": "optional artifact ref"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
},
"graph_edge_create_response": {
"type": "object",
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
"properties": {
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"}
}
},
"graph_edge_tombstone_request": {
"type": "object",
"required": ["edge_ref"],
"properties": {
"edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
},
"graph_edge_tombstone_response": {
"type": "object",
"required": ["ok", "target_edge_ref", "tombstone_edge_ref"],
"properties": {
"ok": {"type": "boolean"},
"target_edge_ref": {"type": "string"},
"tombstone_edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"}
}
},
"graph_node_version_tombstone_request": {
"type": "object",
"required": ["ref"],
"properties": {
"ref": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
},
"graph_node_version_tombstone_response": {
"type": "object",
"required": ["ok", "name", "ref", "target_edge_ref", "tombstone_edge_ref"],
"properties": {
"ok": {"type": "boolean"},
"name": {"type": "string"},
"ref": {"type": "string"},
"target_edge_ref": {"type": "string"},
"tombstone_edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"}
}
},
"graph_batch_request": {
"type": "object",
"properties": {
"idempotency_key": {"type": "string"},
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"ref": {"type": "string"}
}
}
},
"versions": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "ref"],
"properties": {
"name": {"type": "string"},
"ref": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["subject", "predicate", "object"],
"properties": {
"subject": {"type": "string"},
"predicate": {"type": "string"},
"object": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
}
}
}
},
"graph_batch_response": {
"type": "object",
"required": ["ok", "applied", "results"],
"properties": {
"ok": {"type": "boolean"},
"idempotency_key": {"type": "string"},
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
"applied": {
"type": "object",
"required": ["nodes", "versions", "edges"],
"properties": {
"nodes": {"type": "integer"},
"versions": {"type": "integer"},
"edges": {"type": "integer"}
}
},
"results": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "index", "status", "code", "error"],
"properties": {
"kind": {"type": "string", "enum": ["node", "version", "edge"]},
"index": {"type": "integer"},
"status": {"type": "string", "enum": ["applied", "error"]},
"code": {"type": "integer"},
"error": {"type": ["string", "null"]}
}
}
}
}
},
"graph_query_request": {
"type": "object",
"properties": {
"where": {
"type": "object",
"properties": {
"subject": {"type": "string"},
"object": {"type": "string"},
"node": {"type": "string"},
"provenance_ref": {"type": "string"}
}
},
"predicates": {"type": "array", "items": {"type": "string"}},
"direction": {"type": "string", "enum": ["any", "outgoing", "incoming"]},
"include_versions": {"type": "boolean"},
"include_tombstoned": {"type": "boolean"},
"include_stats": {"type": "boolean"},
"max_result_bytes": {"type": "integer"},
"as_of": {"type": ["string", "integer"]},
"limit": {"type": "integer"},
"cursor": {"type": ["string", "integer"]}
}
},
"graph_query_response": {
"type": "object",
"required": ["nodes", "edges", "paging"],
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["concept_ref", "name", "latest_ref"],
"properties": {
"concept_ref": {"type": "string"},
"name": {"type": ["string", "null"]},
"latest_ref": {"type": ["string", "null"]},
"versions": {
"type": "array",
"items": {
"type": "object",
"required": ["edge_ref", "ref"],
"properties": {
"edge_ref": {"type": "string"},
"ref": {"type": "string"}
}
}
}
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
"properties": {
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"}
}
}
},
"paging": {
"type": "object",
"required": ["next_cursor", "has_more"],
"properties": {
"next_cursor": {"type": ["string", "null"]},
"has_more": {"type": "boolean"}
}
},
"stats": {
"type": "object",
"properties": {
"scanned_edges": {"type": "integer"},
"returned_edges": {"type": "integer"}
}
}
}
},
"graph_retrieve_request": {
"type": "object",
"required": ["roots"],
"properties": {
"roots": {"type": "array", "items": {"type": "string"}},
"goal_predicates": {"type": "array", "items": {"type": "string"}},
"max_depth": {"type": "integer"},
"max_fanout": {"type": "integer"},
"include_versions": {"type": "boolean"},
"include_tombstoned": {"type": "boolean"},
"as_of": {"type": ["string", "integer"]},
"provenance_min_confidence": {"type": ["string", "number", "integer"]},
"limit_nodes": {"type": "integer"},
"limit_edges": {"type": "integer"},
"max_result_bytes": {"type": "integer"}
}
},
"graph_retrieve_response": {
"type": "object",
"required": ["nodes", "edges", "explanations", "truncated", "stats"],
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["concept_ref", "name", "latest_ref"],
"properties": {
"concept_ref": {"type": "string"},
"name": {"type": ["string", "null"]},
"latest_ref": {"type": ["string", "null"]},
"versions": {
"type": "array",
"items": {
"type": "object",
"required": ["edge_ref", "ref"],
"properties": {
"edge_ref": {"type": "string"},
"ref": {"type": "string"}
}
}
}
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
"properties": {
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"}
}
}
},
"explanations": {
"type": "array",
"items": {
"type": "object",
"required": ["edge_ref", "depth", "reasons"],
"properties": {
"edge_ref": {"type": "string"},
"depth": {"type": "integer"},
"reasons": {"type": "array", "items": {"type": "string"}},
"confidence": {"type": ["number", "null"]}
}
}
},
"truncated": {"type": "boolean"},
"stats": {
"type": "object",
"properties": {
"scanned_edges": {"type": "integer"},
"traversed_edges": {"type": "integer"},
"returned_nodes": {"type": "integer"},
"returned_edges": {"type": "integer"}
}
}
}
},
"graph_export_request": {
"type": "object",
"properties": {
"as_of": {"type": ["string", "integer"]},
"cursor": {"type": ["string", "integer"]},
"limit": {"type": "integer"},
"predicates": {"type": "array", "items": {"type": "string"}},
"roots": {"type": "array", "items": {"type": "string"}},
"include_tombstoned": {"type": "boolean"},
"max_result_bytes": {"type": "integer"}
}
},
"graph_export_response": {
"type": "object",
"required": ["items", "next_cursor", "has_more", "snapshot_as_of", "stats"],
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["seq", "edge_ref", "subject_ref", "predicate_ref", "predicate", "object_ref", "tombstoned"],
"properties": {
"seq": {"type": "integer"},
"edge_ref": {"type": "string"},
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"predicate": {"type": "string"},
"object_ref": {"type": "string"},
"tombstoned": {"type": "boolean"},
"metadata_ref": {"type": ["string", "null"]}
}
}
},
"next_cursor": {"type": ["string", "null"]},
"has_more": {"type": "boolean"},
"snapshot_as_of": {"type": "string"},
"stats": {
"type": "object",
"properties": {
"scanned_edges": {"type": "integer"},
"exported_items": {"type": "integer"}
}
}
}
},
"graph_import_request": {
"type": "object",
"required": ["items"],
"properties": {
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subject_ref": {"type": "string"},
"subject": {"type": "string"},
"predicate_ref": {"type": "string"},
"predicate": {"type": "string"},
"object_ref": {"type": "string"},
"object": {"type": "string"},
"metadata_ref": {"type": "string"}
}
}
}
}
},
"graph_import_response": {
"type": "object",
"required": ["ok", "applied", "results"],
"properties": {
"ok": {"type": "boolean"},
"applied": {"type": "integer"},
"results": {
"type": "array",
"items": {
"type": "object",
"required": ["index", "status", "code", "error", "edge_ref"],
"properties": {
"index": {"type": "integer"},
"status": {"type": "string", "enum": ["applied", "error"]},
"code": {"type": "integer"},
"error": {"type": ["string", "null"]},
"edge_ref": {"type": ["string", "null"]}
}
}
}
}
},
"graph_schema_predicates_request": {
"type": "object",
"properties": {
"mode": {"type": "string", "enum": ["strict", "warn", "off"]},
"provenance_mode": {"type": "string", "enum": ["optional", "required"]},
"predicates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"predicate_ref": {"type": "string"},
"predicate": {"type": "string"},
"domain": {"type": "string"},
"range": {"type": "string"}
}
}
}
}
},
"graph_schema_predicates_response": {
"type": "object",
"required": ["mode", "provenance_mode", "predicates"],
"properties": {
"mode": {"type": "string", "enum": ["strict", "warn", "off"]},
"provenance_mode": {"type": "string", "enum": ["optional", "required"]},
"predicates": {
"type": "array",
"items": {
"type": "object",
"required": ["predicate_ref", "domain", "range"],
"properties": {
"predicate_ref": {"type": "string"},
"domain": {"type": ["string", "null"]},
"range": {"type": ["string", "null"]}
}
}
}
}
},
"graph_stats_response": {
"type": "object",
"required": ["edges_total", "aliases_total", "index", "tombstones"],
"properties": {
"edges_total": {"type": "integer"},
"aliases_total": {"type": "integer"},
"index": {
"type": "object",
"required": ["built_for_edges", "src_buckets", "dst_buckets", "predicate_buckets", "src_predicate_buckets", "dst_predicate_buckets", "healthy"],
"properties": {
"built_for_edges": {"type": "integer"},
"src_buckets": {"type": "integer"},
"dst_buckets": {"type": "integer"},
"predicate_buckets": {"type": "integer"},
"src_predicate_buckets": {"type": "integer"},
"dst_predicate_buckets": {"type": "integer"},
"healthy": {"type": "boolean"}
}
},
"tombstones": {
"type": "object",
"required": ["edges", "ratio"],
"properties": {
"edges": {"type": "integer"},
"ratio": {"type": "number"}
}
}
}
},
"graph_capabilities_response": {
"type": "object",
"required": ["contract", "graph", "runtime"],
"properties": {
"contract": {"type": "string"},
"graph": {
"type": "object",
"required": ["version", "features", "limits", "modes"],
"properties": {
"version": {"type": "string"},
"features": {"type": "array", "items": {"type": "string"}},
"limits": {"type": "object"},
"modes": {"type": "object"}
}
},
"runtime": {"type": "object"}
}
},
"graph_changes_response": {
"type": "object",
"required": ["events", "next_cursor", "has_more"],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"required": ["event", "cursor", "edge_ref", "subject_ref", "predicate_ref", "object_ref"],
"properties": {
"event": {"type": "string", "enum": ["edge_appended", "version_published", "tombstone_applied"]},
"cursor": {"type": "string"},
"edge_ref": {"type": "string"},
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"concept_ref": {"type": "string"},
"ref": {"type": "string"},
"tombstoned_edge_ref": {"type": "string"}
}
}
},
"next_cursor": {"type": ["string", "null"]},
"has_more": {"type": "boolean"}
}
},
"graph_node_response": {
"type": "object",
"required": ["name", "concept_ref", "latest_ref", "versions", "outgoing", "incoming"],
"properties": {
"name": {"type": "string"},
"concept_ref": {"type": "string"},
"latest_ref": {"type": ["string", "null"]},
"versions": {"type": "array", "items": {"type": "string"}},
"outgoing": {
"type": "array",
"items": {
"type": "object",
"required": ["predicate_ref", "object_ref"],
"properties": {
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"},
"metadata_ref": {"type": ["string", "null"]}
}
}
},
"incoming": {
"type": "array",
"items": {
"type": "object",
"required": ["predicate_ref", "subject_ref"],
"properties": {
"predicate_ref": {"type": "string"},
"subject_ref": {"type": "string"},
"edge_ref": {"type": "string"},
"metadata_ref": {"type": ["string", "null"]}
}
}
}
}
},
"graph_history_response": {
"type": "object",
"required": ["name", "events"],
"properties": {
"name": {"type": "string"},
"events": {
"type": "array",
"items": {
"type": "object",
"required": ["event", "at_ms"],
"properties": {
"event": {"type": "string"},
"at_ms": {"type": "integer"},
"ref": {"type": ["string", "null"]},
"edge_ref": {"type": ["string", "null"]},
"details": {"type": "object"}
}
}
}
}
},
"pel_execute_request": {
"type": "object",
"required": ["program_ref", "inputs", "receipt"],
"properties": {
"program_ref": {"type": "string", "description": "hex ref or concept name"},
"scheme_ref": {"type": "string", "description": "hex ref or 'dag'"},
"params_ref": {"type": "string", "description": "hex ref or concept name"},
"inputs": {
"type": "object",
"properties": {
"refs": {
"type": "array",
"items": {"type": "string", "description": "hex ref or concept name"}
},
"inline_artifacts": {
"type": "array",
"items": {
"type": "object",
"required": ["body_hex"],
"properties": {
"content_type": {"type": "string"},
"type_tag": {"type": "string", "description": "hex tag id, optional"},
"body_hex": {"type": "string"}
}
}
}
}
},
"receipt": {
"type": "object",
"required": [
"input_manifest_ref",
"environment_ref",
"evaluator_id",
"executor_ref",
"started_at",
"completed_at"
],
"properties": {
"input_manifest_ref": {"type": "string", "description": "hex ref or concept name"},
"environment_ref": {"type": "string", "description": "hex ref or concept name"},
"evaluator_id": {"type": "string"},
"executor_ref": {"type": "string", "description": "hex ref or concept name"},
"sbom_ref": {"type": "string", "description": "hex ref or concept name"},
"parity_digest_hex": {"type": "string"},
"executor_fingerprint_ref": {"type": "string", "description": "hex ref or concept name"},
"run_id_hex": {"type": "string"},
"limits": {
"type": "object",
"required": ["cpu_ms", "wall_ms", "max_rss_kib", "io_reads", "io_writes"],
"properties": {
"cpu_ms": {"type": "integer"},
"wall_ms": {"type": "integer"},
"max_rss_kib": {"type": "integer"},
"io_reads": {"type": "integer"},
"io_writes": {"type": "integer"}
}
},
"logs": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "log_ref", "sha256_hex"],
"properties": {
"kind": {"type": "integer"},
"log_ref": {"type": "string", "description": "hex ref or concept name"},
"sha256_hex": {"type": "string"}
}
}
},
"determinism_level": {"type": "integer", "description": "0-255"},
"rng_seed_hex": {"type": "string"},
"signature_hex": {"type": "string"},
"started_at": {"type": "integer"},
"completed_at": {"type": "integer"}
}
},
"effects": {
"type": "object",
"properties": {
"publish_outputs": {"type": "boolean"},
"append_fed_log": {"type": "boolean"}
}
}
}
},
"pel_execute_response": {
"type": "object",
"required": ["run_ref", "receipt_ref", "stored_input_refs", "output_refs", "status"],
"properties": {
"run_ref": {"type": "string", "description": "hex ref"},
"trace_ref": {"type": "string", "description": "hex ref"},
"receipt_ref": {"type": "string", "description": "hex ref"},
"stored_input_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
"output_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
"status": {"type": "string"}
}
},
"error_response": {
"type": "object",
"required": ["error"],
"properties": {
"error": {
"type": "object",
"required": ["code", "message", "retryable"],
"properties": {
"code": {"type": "string"},
"message": {"type": "string"},
"retryable": {"type": "boolean"}
}
}
}
}
}
}

186
scripts/graph_client_helpers.sh Executable file
View 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

View file

@ -29,8 +29,29 @@ if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
exit 77 exit 77
fi fi
AMDUATD_BIN="${ROOT_DIR}/build/amduatd" AMDUATD_BIN="${AMDUATD_BIN:-}"
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl" if [[ -z "${AMDUATD_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/amduatd" \
"${ROOT_DIR}/build-asan/amduatd"; do
if [[ -x "${cand}" ]]; then
AMDUATD_BIN="${cand}"
break
fi
done
fi
ASL_BIN="${ASL_BIN:-}"
if [[ -z "${ASL_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/vendor/amduat/amduat-asl" \
"${ROOT_DIR}/vendor/amduat/build/amduat-asl"; do
if [[ -x "${cand}" ]]; then
ASL_BIN="${cand}"
break
fi
done
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2 echo "missing binaries; build amduatd and amduat-asl first" >&2

View file

@ -29,8 +29,29 @@ if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
exit 77 exit 77
fi fi
AMDUATD_BIN="${ROOT_DIR}/build/amduatd" AMDUATD_BIN="${AMDUATD_BIN:-}"
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl" if [[ -z "${AMDUATD_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/amduatd" \
"${ROOT_DIR}/build-asan/amduatd"; do
if [[ -x "${cand}" ]]; then
AMDUATD_BIN="${cand}"
break
fi
done
fi
ASL_BIN="${ASL_BIN:-}"
if [[ -z "${ASL_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/vendor/amduat/amduat-asl" \
"${ROOT_DIR}/vendor/amduat/build/amduat-asl"; do
if [[ -x "${cand}" ]]; then
ASL_BIN="${cand}"
break
fi
done
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2 echo "missing binaries; build amduatd and amduat-asl first" >&2

382
scripts/test_graph_contract.sh Executable file
View file

@ -0,0 +1,382 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
graph_helpers_init "${ROOT_DIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/build/vendor/amduat/amduat-asl"
if [[ ! -x "${ASL_BIN}" ]]; then
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-contract-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphcontract"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
extract_json_string() {
local key="$1"
awk -v k="\"${key}\":\"" '
{
pos = index($0, k);
if (pos > 0) {
rest = substr($0, pos + length(k));
end = index(rest, "\"");
if (end > 0) {
print substr(rest, 1, end - 1);
exit 0;
}
}
}
'
}
cursor_to_num() {
local token="$1"
if [[ -z "${token}" ]]; then
echo 0
return 0
fi
if [[ "${token}" == g1_* ]]; then
echo "${token#g1_}"
return 0
fi
echo "${token}"
}
cursor_plus_one() {
local token="$1"
local n
if [[ -z "${token}" ]]; then
return 1
fi
if [[ "${token}" == g1_* ]]; then
n="${token#g1_}"
printf 'g1_%s' "$((n + 1))"
return 0
fi
printf '%s' "$((token + 1))"
}
create_artifact() {
local payload="$1"
local resp
local ref
resp="$({
graph_http_post "${sock}" "/v1/artifacts" "${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_id}"
})"
ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")"
if [[ -z "${ref}" ]]; then
echo "failed to parse artifact ref: ${resp}" >&2
exit 1
fi
printf '%s' "${ref}"
}
create_node() {
local name="$1"
local ref="$2"
graph_http_post "${sock}" "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
create_edge() {
local s="$1"
local p="$2"
local o="$3"
graph_http_post "${sock}" "/v2/graph/edges" "{\"subject\":\"${s}\",\"predicate\":\"${p}\",\"object\":\"${o}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
mkdir -p "${root}"
"${ASL_BIN}" init --root "${root}" >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
ready_rc=0
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
if [[ ${ready_rc} -eq 77 ]]; then
exit 77
fi
if [[ ${ready_rc} -ne 0 ]]; then
echo "daemon not ready" >&2
exit 1
fi
ref_a="$(create_artifact "contract-a")"
ref_b="$(create_artifact "contract-b")"
ref_c="$(create_artifact "contract-c")"
ref_v1="$(create_artifact "contract-v1")"
ref_v2="$(create_artifact "contract-v2")"
create_node "gc-a" "${ref_a}"
create_node "gc-b" "${ref_b}"
create_node "gc-c" "${ref_c}"
create_node "gc-v" "${ref_v1}"
create_edge "gc-a" "ms.computed_by" "gc-b"
create_edge "gc-b" "ms.computed_by" "gc-c"
graph_http_post "${sock}" "/v2/graph/nodes/gc-v/versions" "{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
versions_cutoff_raw="$(
graph_http_get "${sock}" "/v2/graph/changes?event_types[]=version_published&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
versions_cutoff="$(printf '%s\n' "${versions_cutoff_raw}" | extract_json_string "next_cursor")"
if [[ -z "${versions_cutoff}" ]]; then
echo "missing version cutoff cursor: ${versions_cutoff_raw}" >&2
exit 1
fi
versions_cutoff="$(cursor_plus_one "${versions_cutoff}")"
graph_http_post "${sock}" "/v2/graph/nodes/gc-v/versions/tombstone" "{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
node_default="$(
graph_http_get "${sock}" "/v2/graph/nodes/gc-v" \
--header "X-Amduat-Space: ${space_id}"
)"
node_default_latest="$(printf '%s\n' "${node_default}" | extract_json_string "latest_ref")"
if [[ "${node_default_latest}" != "${ref_v1}" ]]; then
echo "node default latest_ref mismatch (want ${ref_v1}): ${node_default}" >&2
exit 1
fi
if echo "${node_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "node default should hide tombstoned ${ref_v2}: ${node_default}" >&2
exit 1
fi
node_all="$(
graph_http_get "${sock}" "/v2/graph/nodes/gc-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
node_all_latest="$(printf '%s\n' "${node_all}" | extract_json_string "latest_ref")"
if [[ "${node_all_latest}" != "${ref_v2}" ]]; then
echo "node include_tombstoned latest_ref mismatch (want ${ref_v2}): ${node_all}" >&2
exit 1
fi
echo "${node_all}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "node include_tombstoned should include ${ref_v2}: ${node_all}" >&2
exit 1
}
node_asof="$(
graph_http_get "${sock}" "/v2/graph/nodes/gc-v?as_of=${versions_cutoff}" \
--header "X-Amduat-Space: ${space_id}"
)"
node_asof_latest="$(printf '%s\n' "${node_asof}" | extract_json_string "latest_ref")"
if [[ "${node_asof_latest}" != "${ref_v2}" ]]; then
echo "node as_of before tombstone latest_ref mismatch (want ${ref_v2}): ${node_asof}" >&2
exit 1
fi
echo "${node_asof}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "node as_of before tombstone should include ${ref_v2}: ${node_asof}" >&2
exit 1
}
history_default="$(
graph_http_get "${sock}" "/v2/graph/history/gc-v" \
--header "X-Amduat-Space: ${space_id}"
)"
history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")"
if [[ "${history_default_latest}" != "${ref_v1}" ]]; then
echo "history default latest_ref mismatch (want ${ref_v1}): ${history_default}" >&2
exit 1
fi
if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "history default should hide tombstoned ${ref_v2}: ${history_default}" >&2
exit 1
fi
history_all="$(
graph_http_get "${sock}" "/v2/graph/history/gc-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
history_all_latest="$(printf '%s\n' "${history_all}" | extract_json_string "latest_ref")"
if [[ "${history_all_latest}" != "${ref_v2}" ]]; then
echo "history include_tombstoned latest_ref mismatch (want ${ref_v2}): ${history_all}" >&2
exit 1
fi
echo "${history_all}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "history include_tombstoned should include ${ref_v2}: ${history_all}" >&2
exit 1
}
# schema strict: block predicate not in allowed list.
strict_policy='{"mode":"strict","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${strict_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
strict_reject="$({
graph_http_post_allow "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.within_domain","object":"gc-c"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${strict_reject}" | grep -q 'predicate rejected by schema policy' || {
echo "strict mode should reject disallowed predicate: ${strict_reject}" >&2
exit 1
}
# schema warn: same write should pass.
warn_policy='{"mode":"warn","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${warn_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
warn_accept="$({
graph_http_post "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.within_domain","object":"gc-c"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${warn_accept}" | grep -q '"edge_ref":"' || {
echo "warn mode should allow disallowed predicate: ${warn_accept}" >&2
exit 1
}
# provenance required: writes without metadata/provenance must fail with 422.
required_policy='{"mode":"warn","provenance_mode":"required","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${required_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
schema_required="$(
graph_http_get "${sock}" "/v2/graph/schema/predicates" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${schema_required}" | grep -q '"provenance_mode":"required"' || {
echo "schema provenance_mode did not persist: ${schema_required}" >&2
exit 1
}
required_reject="$({
graph_http_post_allow "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${required_reject}" | grep -q 'provenance required by schema policy' || {
echo "required provenance should reject missing attachment: ${required_reject}" >&2
exit 1
}
required_version_reject="$({
graph_http_post_allow "${sock}" "/v2/graph/nodes/gc-a/versions" "{\"ref\":\"${ref_v1}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${required_version_reject}" | grep -q 'provenance required by schema policy' || {
echo "required provenance should reject version write without attachment: ${required_version_reject}" >&2
exit 1
}
required_accept="$({
graph_http_post "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b","provenance":{"source_uri":"urn:test","extractor":"contract-test","observed_at":1,"ingested_at":2,"trace_id":"trace-required-1"}}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${required_accept}" | grep -q '"edge_ref":"' || {
echo "required provenance should allow explicit provenance payload: ${required_accept}" >&2
exit 1
}
# reset to optional so remaining tests can use minimal payloads.
optional_policy='{"mode":"warn","provenance_mode":"optional","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${optional_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
# batch idempotency replay must be deterministic.
idem_payload='{"idempotency_key":"gc-idem-1","mode":"continue_on_error","edges":[{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-c"},{"subject":"gc-missing","predicate":"ms.computed_by","object":"gc-c"}]}'
idem_first="$(graph_batch_ingest "${sock}" "${space_id}" "${idem_payload}")"
idem_second="$(graph_batch_ingest "${sock}" "${space_id}" "${idem_payload}")"
if [[ "${idem_first}" != "${idem_second}" ]]; then
echo "idempotent replay mismatch" >&2
echo "first=${idem_first}" >&2
echo "second=${idem_second}" >&2
exit 1
fi
# payload mismatch on same idempotency key must fail.
idem_conflict="$({
graph_http_post_allow "${sock}" "/v2/graph/batch" '{"idempotency_key":"gc-idem-1","mode":"continue_on_error","edges":[{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b"}]}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${idem_conflict}" | grep -q 'idempotency_key reuse with different payload' || {
echo "idempotency conflict missing expected error: ${idem_conflict}" >&2
exit 1
}
# changes sync helper: cursor monotonic + resumable loop.
changes_1="$(graph_changes_sync_once "${sock}" "${space_id}" "" 2)"
cursor_1="$(printf '%s\n' "${changes_1}" | extract_json_string "next_cursor")"
if [[ -z "${cursor_1}" ]]; then
echo "changes first page missing next_cursor: ${changes_1}" >&2
exit 1
fi
changes_2="$(graph_changes_sync_once "${sock}" "${space_id}" "${cursor_1}" 2)"
cursor_2="$(printf '%s\n' "${changes_2}" | extract_json_string "next_cursor")"
if [[ -z "${cursor_2}" ]]; then
echo "changes second page missing next_cursor: ${changes_2}" >&2
exit 1
fi
num_1="$(cursor_to_num "${cursor_1}")"
num_2="$(cursor_to_num "${cursor_2}")"
if [[ "${num_2}" -lt "${num_1}" ]]; then
echo "changes cursor regressed: ${cursor_1} -> ${cursor_2}" >&2
exit 1
fi
# subgraph helper should return connected nodes.
subgraph_resp="$(graph_subgraph_fetch "${sock}" "${space_id}" "gc-a" 2 "ms.computed_by")"
echo "${subgraph_resp}" | grep -q '"name":"gc-a"' || {
echo "subgraph missing gc-a: ${subgraph_resp}" >&2
exit 1
}
echo "${subgraph_resp}" | grep -q '"name":"gc-b"' || {
echo "subgraph missing gc-b: ${subgraph_resp}" >&2
exit 1
}
echo "ok: v2 graph contract tests passed"

View file

@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
graph_helpers_init "${ROOT_DIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-index-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphindex"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
mkdir -p "${root}"
"${ASL_BIN}" index init --root "${root}" >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
ready_rc=0
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
if [[ ${ready_rc} -eq 77 ]]; then
exit 77
fi
if [[ ${ready_rc} -ne 0 ]]; then
echo "daemon not ready" >&2
exit 1
fi
node_a="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-a"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_a}" | grep -q '"name":"idx-a"' || {
echo "first node create failed: ${node_a}" >&2
exit 1
}
node_b="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_b}" | grep -q '"name":"idx-b"' || {
echo "second node create failed: ${node_b}" >&2
exit 1
}
edge="$(
graph_http_post "${sock}" "/v2/graph/edges" \
'{"subject":"idx-a","predicate":"ms.within_domain","object":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edge}" | grep -q '"edge_ref":"' || {
echo "edge create failed: ${edge}" >&2
exit 1
}
echo "ok: index append sequence passed"

View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
graph_helpers_init "${ROOT_DIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
iters="${AMDUATD_INDEX_STRESS_ITERS:-20}"
case "${iters}" in
''|*[!0-9]*)
echo "invalid AMDUATD_INDEX_STRESS_ITERS: ${iters}" >&2
exit 2
;;
esac
if [[ "${iters}" -le 0 ]]; then
echo "AMDUATD_INDEX_STRESS_ITERS must be > 0" >&2
exit 2
fi
run_one() {
local i="$1"
local tmp_root root sock space_id log_file pid ready_rc
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-index-stress-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphindex${i}"
log_file="${tmp_root}/amduatd.log"
pid=""
cleanup_one() {
if [[ -n "${pid}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup_one RETURN
mkdir -p "${root}"
"${ASL_BIN}" index init --root "${root}" >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
ready_rc=0
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
if [[ ${ready_rc} -eq 77 ]]; then
return 77
fi
if [[ ${ready_rc} -ne 0 ]]; then
echo "iteration ${i}: daemon not ready" >&2
return 1
fi
node_a="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-a"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_a}" | grep -q '"name":"idx-a"' || {
echo "iteration ${i}: first node create failed: ${node_a}" >&2
return 1
}
node_b="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_b}" | grep -q '"name":"idx-b"' || {
echo "iteration ${i}: second node create failed: ${node_b}" >&2
return 1
}
edge="$(
graph_http_post "${sock}" "/v2/graph/edges" \
'{"subject":"idx-a","predicate":"ms.within_domain","object":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edge}" | grep -q '"edge_ref":"' || {
echo "iteration ${i}: edge create failed: ${edge}" >&2
return 1
}
echo "iteration ${i}/${iters}: ok"
return 0
}
for i in $(seq 1 "${iters}"); do
rc=0
run_one "${i}" || rc=$?
if [[ ${rc} -eq 77 ]]; then
exit 77
fi
if [[ ${rc} -ne 0 ]]; then
exit "${rc}"
fi
done
echo "ok: index append stress passed (${iters} iterations)"

781
scripts/test_graph_queries.sh Executable file
View file

@ -0,0 +1,781 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
if command -v curl >/dev/null 2>&1; then
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket support and helper missing" >&2
exit 77
fi
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/build/vendor/amduat/amduat-asl"
if [[ ! -x "${ASL_BIN}" ]]; then
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-queries-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphq"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
http_get() {
local path="$1"
shift
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method GET --path "${path}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
"http://localhost${path}"
fi
}
http_post() {
local path="$1"
local data="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "${path}" \
--data "${data}" \
"$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
extract_json_string() {
local key="$1"
awk -v k="\"${key}\":\"" '
{
pos = index($0, k);
if (pos > 0) {
rest = substr($0, pos + length(k));
end = index(rest, "\"");
if (end > 0) {
print substr(rest, 1, end - 1);
exit 0;
}
}
}
'
}
cursor_plus_one() {
local token="$1"
local n
if [[ -z "${token}" ]]; then
return 1
fi
if [[ "${token}" == g1_* ]]; then
n="${token#g1_}"
printf 'g1_%s' "$((n + 1))"
return 0
fi
printf '%s' "$((token + 1))"
}
wait_for_ready() {
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
if [[ -f "${log_file}" ]] && grep -q "bind: Operation not permitted" "${log_file}"; then
echo "skip: bind not permitted for unix socket" >&2
exit 77
fi
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
return 1
fi
if [[ -S "${sock}" ]] && http_get "/v1/meta" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
create_artifact() {
local payload="$1"
local resp
local ref
resp="$(
http_post "/v1/artifacts" "${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_id}"
)"
ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")"
if [[ -z "${ref}" ]]; then
echo "failed to parse artifact ref: ${resp}" >&2
exit 1
fi
printf '%s' "${ref}"
}
create_node() {
local name="$1"
local ref="$2"
http_post "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
create_edge() {
local subject="$1"
local predicate="$2"
local object="$3"
http_post "/v2/graph/edges" \
"{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
create_edge_with_metadata() {
local subject="$1"
local predicate="$2"
local object="$3"
local metadata_ref="$4"
http_post "/v2/graph/edges" \
"{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\",\"metadata_ref\":\"${metadata_ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
start_daemon() {
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
if ! wait_for_ready; then
echo "daemon not ready" >&2
exit 1
fi
}
restart_daemon() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
start_daemon
}
mkdir -p "${root}"
"${ASL_BIN}" init --root "${root}"
start_daemon
ref_a="$(create_artifact "payload-a")"
ref_b="$(create_artifact "payload-b")"
ref_c="$(create_artifact "payload-c")"
ref_d="$(create_artifact "payload-d")"
ref_e="$(create_artifact "payload-e")"
ref_e2="$(create_artifact "payload-e2")"
ref_v1="$(create_artifact "payload-v1")"
ref_v2="$(create_artifact "payload-v2")"
ref_p1="$(create_artifact "payload-provenance-1")"
ref_p2="$(create_artifact "payload-provenance-2")"
create_node "gq-a" "${ref_a}"
create_node "gq-b" "${ref_b}"
create_node "gq-c" "${ref_c}"
create_node "gq-d" "${ref_d}"
create_node "gq-e" "${ref_e}"
create_node "gq-v" "${ref_v1}"
create_node "gq-prov1" "${ref_p1}"
create_node "gq-prov2" "${ref_p2}"
# Seed path and neighbor data in a controlled edge order.
create_edge "gq-a" "ms.within_domain" "gq-b"
create_edge "gq-b" "ms.within_domain" "gq-c"
create_edge "gq-a" "ms.computed_by" "gq-b"
cutoff_resp="$(
http_get "/v2/graph/changes?event_types[]=edge_appended&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
cutoff_cursor="$(printf '%s\n' "${cutoff_resp}" | extract_json_string "next_cursor")"
if [[ -z "${cutoff_cursor}" ]]; then
echo "failed to parse cutoff cursor: ${cutoff_resp}" >&2
exit 1
fi
cutoff_cursor="$(cursor_plus_one "${cutoff_cursor}")"
create_edge "gq-a" "ms.computed_by" "gq-d"
create_edge "gq-a" "ms.within_domain" "gq-c"
search_page1="$(
http_get "/v2/graph/search?name_prefix=gq-&limit=2" \
--header "X-Amduat-Space: ${space_id}"
)"
search_cursor="$(printf '%s\n' "${search_page1}" | extract_json_string "next_cursor")"
if [[ -z "${search_cursor}" ]]; then
echo "missing search cursor in page1: ${search_page1}" >&2
exit 1
fi
search_page2="$(
http_get "/v2/graph/search?name_prefix=gq-&limit=10&cursor=${search_cursor}" \
--header "X-Amduat-Space: ${space_id}"
)"
search_joined="${search_page1} ${search_page2}"
echo "${search_joined}" | grep -q '"name":"gq-a"' || { echo "search missing gq-a" >&2; exit 1; }
echo "${search_joined}" | grep -q '"name":"gq-b"' || { echo "search missing gq-b" >&2; exit 1; }
echo "${search_joined}" | grep -q '"name":"gq-c"' || { echo "search missing gq-c" >&2; exit 1; }
echo "${search_joined}" | grep -q '"name":"gq-d"' || { echo "search missing gq-d" >&2; exit 1; }
neighbors_page1="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=1&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
neighbors_cursor="$(printf '%s\n' "${neighbors_page1}" | extract_json_string "next_cursor")"
if [[ -z "${neighbors_cursor}" ]]; then
echo "missing neighbors cursor in page1: ${neighbors_page1}" >&2
exit 1
fi
neighbors_page2="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&cursor=${neighbors_cursor}&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
neighbors_joined="${neighbors_page1} ${neighbors_page2}"
echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors missing gq-b" >&2; exit 1; }
echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-d"' || { echo "neighbors missing gq-d" >&2; exit 1; }
neighbors_asof="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&as_of=${cutoff_cursor}&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors as_of missing gq-b" >&2; exit 1; }
if echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-d"'; then
echo "neighbors as_of unexpectedly includes gq-d" >&2
exit 1
fi
paths_latest="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_latest}" | grep -q '"depth":1' || {
echo "paths latest expected direct depth 1: ${paths_latest}" >&2
exit 1
}
paths_asof="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&as_of=${cutoff_cursor}&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_asof}" | grep -q '"depth":2' || {
echo "paths as_of expected historical depth 2: ${paths_asof}" >&2
exit 1
}
subgraph_page1="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=1&limit_nodes=10&include_versions=true" \
--header "X-Amduat-Space: ${space_id}"
)"
subgraph_cursor="$(printf '%s\n' "${subgraph_page1}" | extract_json_string "next_cursor")"
if [[ -z "${subgraph_cursor}" ]]; then
echo "missing subgraph cursor in page1: ${subgraph_page1}" >&2
exit 1
fi
subgraph_page2="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10&cursor=${subgraph_cursor}" \
--header "X-Amduat-Space: ${space_id}"
)"
subgraph_joined="${subgraph_page1} ${subgraph_page2}"
echo "${subgraph_joined}" | grep -q '"name":"gq-a"' || { echo "subgraph missing root node gq-a" >&2; exit 1; }
echo "${subgraph_joined}" | grep -q '"name":"gq-b"' || { echo "subgraph missing gq-b" >&2; exit 1; }
echo "${subgraph_joined}" | grep -q '"name":"gq-d"' || { echo "subgraph missing gq-d" >&2; exit 1; }
echo "${subgraph_joined}" | grep -q '"versions":\[' || { echo "subgraph include_versions missing versions" >&2; exit 1; }
subgraph_asof="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&as_of=${cutoff_cursor}&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_asof}" | grep -q '"name":"gq-b"' || { echo "subgraph as_of missing gq-b" >&2; exit 1; }
if echo "${subgraph_asof}" | grep -q '"name":"gq-d"'; then
echo "subgraph as_of unexpectedly includes gq-d" >&2
exit 1
fi
gqd_edge_resp="$(
http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&object=gq-d&dir=outgoing&limit=1" \
--header "X-Amduat-Space: ${space_id}"
)"
gqd_edge_ref="$(printf '%s\n' "${gqd_edge_resp}" | extract_json_string "edge_ref")"
if [[ -z "${gqd_edge_ref}" ]]; then
echo "failed to parse gq-a->gq-d edge ref: ${gqd_edge_resp}" >&2
exit 1
fi
http_post "/v2/graph/edges/tombstone" \
"{\"edge_ref\":\"${gqd_edge_ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
subgraph_after_tombstone="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
if echo "${subgraph_after_tombstone}" | grep -q '"name":"gq-d"'; then
echo "subgraph after tombstone unexpectedly includes gq-d: ${subgraph_after_tombstone}" >&2
exit 1
fi
subgraph_include_tombstoned="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&include_tombstoned=true&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_include_tombstoned}" | grep -q '"name":"gq-d"' || {
echo "subgraph include_tombstoned missing gq-d: ${subgraph_include_tombstoned}" >&2
exit 1
}
edges_include_tombstoned="$(
http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&dir=outgoing&include_tombstoned=true&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edges_include_tombstoned}" | grep -q '"object_name":"gq-d"' || {
echo "edges include_tombstoned missing gq-d: ${edges_include_tombstoned}" >&2
exit 1
}
neighbors_after_tombstone="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
if echo "${neighbors_after_tombstone}" | grep -q '"neighbor_name":"gq-d"'; then
echo "neighbors default should exclude tombstoned gq-d edge: ${neighbors_after_tombstone}" >&2
exit 1
fi
neighbors_include_tombstoned="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&include_tombstoned=true&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_include_tombstoned}" | grep -q '"neighbor_name":"gq-d"' || {
echo "neighbors include_tombstoned missing gq-d: ${neighbors_include_tombstoned}" >&2
exit 1
}
paths_excluding_tombstoned="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_excluding_tombstoned}" | grep -q '"paths":\[\]' || {
echo "paths default should exclude tombstoned edge: ${paths_excluding_tombstoned}" >&2
exit 1
}
paths_include_tombstoned="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2&include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_include_tombstoned}" | grep -q '"depth":1' || {
echo "paths include_tombstoned expected depth 1 path: ${paths_include_tombstoned}" >&2
exit 1
}
create_edge_with_metadata "gq-b" "ms.computed_by" "gq-d" "gq-prov1"
create_edge_with_metadata "gq-b" "ms.computed_by" "gq-a" "gq-prov2"
neighbors_provenance="$(
http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov1&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_provenance}" | grep -q '"neighbor_name":"gq-d"' || {
echo "neighbors provenance filter missing gq-d: ${neighbors_provenance}" >&2
exit 1
}
neighbors_provenance_missing="$(
http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov-missing&limit=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_provenance_missing}" | grep -q '"neighbors":\[\]' || {
echo "neighbors unresolved provenance expected empty result: ${neighbors_provenance_missing}" >&2
exit 1
}
paths_provenance_match="$(
http_get "/v2/graph/paths?from=gq-b&to=gq-d&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_provenance_match}" | grep -q '"depth":1' || {
echo "paths provenance filter expected depth 1 path: ${paths_provenance_match}" >&2
exit 1
}
echo "${paths_provenance_match}" | grep -q '"object_name":"gq-d"' || {
echo "paths provenance filter missing gq-d: ${paths_provenance_match}" >&2
exit 1
}
paths_provenance_excluded="$(
http_get "/v2/graph/paths?from=gq-b&to=gq-a&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_provenance_excluded}" | grep -q '"paths":\[\]' || {
echo "paths provenance filter unexpectedly includes gq-b->gq-a path: ${paths_provenance_excluded}" >&2
exit 1
}
batch_resp="$(
http_post "/v2/graph/batch" \
"{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-d\",\"metadata_ref\":\"gq-prov1\"}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_resp}" | grep -q '"ok":true' || {
echo "batch edge with metadata_ref failed: ${batch_resp}" >&2
exit 1
}
echo "${batch_resp}" | grep -q '"results":\[' || {
echo "batch response missing results array: ${batch_resp}" >&2
exit 1
}
echo "${batch_resp}" | grep -q '"status":"applied"' || {
echo "batch response missing applied item status: ${batch_resp}" >&2
exit 1
}
batch_version_provenance="$(
http_post "/v2/graph/batch" \
"{\"versions\":[{\"name\":\"gq-e\",\"ref\":\"${ref_e2}\",\"provenance\":{\"source_uri\":\"urn:test:gq-e-v2\",\"extractor\":\"graph-test\",\"confidence\":\"0.91\",\"observed_at\":1730000000000,\"ingested_at\":1730000000100,\"license\":\"test-only\",\"trace_id\":\"trace-gq-e-v2\"}}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_version_provenance}" | grep -q '"ok":true' || {
echo "batch version with provenance failed: ${batch_version_provenance}" >&2
exit 1
}
echo "${batch_version_provenance}" | grep -q '"status":"applied"' || {
echo "batch version with provenance missing applied status: ${batch_version_provenance}" >&2
exit 1
}
batch_edge_provenance="$(
http_post "/v2/graph/batch" \
"{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-a\",\"provenance\":{\"source_uri\":\"urn:test:gq-c-a\",\"extractor\":\"graph-test\",\"confidence\":\"0.87\",\"observed_at\":1730000000200,\"ingested_at\":1730000000300,\"trace_id\":\"trace-gq-c-a\"}}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_edge_provenance}" | grep -q '"ok":true' || {
echo "batch edge with provenance failed: ${batch_edge_provenance}" >&2
exit 1
}
echo "${batch_edge_provenance}" | grep -q '"status":"applied"' || {
echo "batch edge with provenance missing applied status: ${batch_edge_provenance}" >&2
exit 1
}
http_post "/v2/graph/nodes/gq-v/versions" \
"{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
http_post "/v2/graph/nodes/gq-v/versions/tombstone" \
"{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
gqv_after_tombstone="$(
http_get "/v2/graph/nodes/gq-v" \
--header "X-Amduat-Space: ${space_id}"
)"
gqv_latest="$(printf '%s\n' "${gqv_after_tombstone}" | extract_json_string "latest_ref")"
if [[ "${gqv_latest}" != "${ref_v1}" ]]; then
echo "version tombstone expected latest_ref=${ref_v1}, got ${gqv_latest}: ${gqv_after_tombstone}" >&2
exit 1
fi
if echo "${gqv_after_tombstone}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "version tombstone expected ${ref_v2} hidden by default: ${gqv_after_tombstone}" >&2
exit 1
fi
gqv_include_tombstoned="$(
http_get "/v2/graph/nodes/gq-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
gqv_latest_all="$(printf '%s\n' "${gqv_include_tombstoned}" | extract_json_string "latest_ref")"
if [[ "${gqv_latest_all}" != "${ref_v2}" ]]; then
echo "include_tombstoned expected latest_ref=${ref_v2}, got ${gqv_latest_all}: ${gqv_include_tombstoned}" >&2
exit 1
fi
echo "${gqv_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "include_tombstoned expected historical version ${ref_v2}: ${gqv_include_tombstoned}" >&2
exit 1
}
history_default="$(
http_get "/v2/graph/history/gq-v" \
--header "X-Amduat-Space: ${space_id}"
)"
history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")"
if [[ "${history_default_latest}" != "${ref_v1}" ]]; then
echo "history default expected latest_ref=${ref_v1}, got ${history_default_latest}: ${history_default}" >&2
exit 1
fi
if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "history default expected tombstoned version ${ref_v2} hidden: ${history_default}" >&2
exit 1
fi
history_include_tombstoned="$(
http_get "/v2/graph/history/gq-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
history_all_latest="$(printf '%s\n' "${history_include_tombstoned}" | extract_json_string "latest_ref")"
if [[ "${history_all_latest}" != "${ref_v2}" ]]; then
echo "history include_tombstoned expected latest_ref=${ref_v2}, got ${history_all_latest}: ${history_include_tombstoned}" >&2
exit 1
fi
echo "${history_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "history include_tombstoned expected tombstoned version ${ref_v2}: ${history_include_tombstoned}" >&2
exit 1
}
batch_continue="$(
http_post "/v2/graph/batch" \
"{\"mode\":\"continue_on_error\",\"edges\":[{\"subject\":\"gq-a\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"},{\"subject\":\"gq-missing\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_continue}" | grep -q '"ok":false' || {
echo "batch continue_on_error expected ok=false: ${batch_continue}" >&2
exit 1
}
echo "${batch_continue}" | grep -q '"mode":"continue_on_error"' || {
echo "batch continue_on_error mode echo missing: ${batch_continue}" >&2
exit 1
}
echo "${batch_continue}" | grep -q '"status":"applied"' || {
echo "batch continue_on_error missing applied result: ${batch_continue}" >&2
exit 1
}
echo "${batch_continue}" | grep -q '"status":"error"' || {
echo "batch continue_on_error missing error result: ${batch_continue}" >&2
exit 1
}
idem_payload='{"idempotency_key":"gq-idem-1","mode":"continue_on_error","edges":[{"subject":"gq-b","predicate":"ms.computed_by","object":"gq-c"},{"subject":"gq-nope","predicate":"ms.computed_by","object":"gq-c"}]}'
idem_first="$(
http_post "/v2/graph/batch" \
"${idem_payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
idem_second="$(
http_post "/v2/graph/batch" \
"${idem_payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
if [[ "${idem_first}" != "${idem_second}" ]]; then
echo "idempotency replay response mismatch" >&2
echo "first=${idem_first}" >&2
echo "second=${idem_second}" >&2
exit 1
fi
restart_daemon
idem_third="$(
http_post "/v2/graph/batch" \
"${idem_payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
if [[ "${idem_first}" != "${idem_third}" ]]; then
echo "idempotency replay after restart mismatch" >&2
echo "first=${idem_first}" >&2
echo "third=${idem_third}" >&2
exit 1
fi
query_include_versions="$(
http_post "/v2/graph/query" \
"{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_versions\":true,\"limit\":10}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${query_include_versions}" | grep -q '"versions":\[' || {
echo "graph query include_versions missing versions: ${query_include_versions}" >&2
exit 1
}
query_with_stats="$(
http_post "/v2/graph/query" \
"{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_stats\":true,\"max_result_bytes\":1048576,\"limit\":10}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${query_with_stats}" | grep -q '"stats":{' || {
echo "graph query include_stats missing stats block: ${query_with_stats}" >&2
exit 1
}
echo "${query_with_stats}" | grep -q '"plan":"' || {
echo "graph query include_stats missing plan: ${query_with_stats}" >&2
exit 1
}
query_provenance="$(
http_post "/v2/graph/query" \
"{\"where\":{\"subject\":\"gq-b\",\"provenance_ref\":\"gq-prov1\"},\"predicates\":[\"ms.computed_by\"],\"direction\":\"outgoing\",\"limit\":10}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${query_provenance}" | grep -q '"name":"gq-d"' || {
echo "graph query provenance filter missing expected node gq-d: ${query_provenance}" >&2
exit 1
}
if echo "${query_provenance}" | grep -q '"name":"gq-a"'; then
echo "graph query provenance filter unexpectedly includes gq-a: ${query_provenance}" >&2
exit 1
fi
query_provenance_count="$(printf '%s\n' "${query_provenance}" | grep -o '"edge_ref":"' | wc -l | awk '{print $1}')"
if [[ "${query_provenance_count}" != "1" ]]; then
echo "graph query provenance expected exactly one edge, got ${query_provenance_count}: ${query_provenance}" >&2
exit 1
fi
edges_provenance="$(
http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edges_provenance}" | grep -q '"object_name":"gq-d"' || {
echo "graph edges provenance filter missing gq-d: ${edges_provenance}" >&2
exit 1
}
if echo "${edges_provenance}" | grep -q '"object_name":"gq-a"'; then
echo "graph edges provenance filter unexpectedly includes gq-a: ${edges_provenance}" >&2
exit 1
fi
edges_with_stats="$(
http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&include_stats=true&max_result_bytes=1048576&limit=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edges_with_stats}" | grep -q '"stats":{' || {
echo "graph edges include_stats missing stats block: ${edges_with_stats}" >&2
exit 1
}
echo "${edges_with_stats}" | grep -q '"plan":"' || {
echo "graph edges include_stats missing plan: ${edges_with_stats}" >&2
exit 1
}
subgraph_provenance="$(
http_get "/v2/graph/subgraph?roots[]=gq-b&max_depth=1&predicates[]=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_provenance}" | grep -q '"name":"gq-d"' || {
echo "subgraph provenance filter missing gq-d: ${subgraph_provenance}" >&2
exit 1
}
if echo "${subgraph_provenance}" | grep -q '"name":"gq-a"'; then
echo "subgraph provenance filter unexpectedly includes gq-a: ${subgraph_provenance}" >&2
exit 1
fi
subgraph_with_stats="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&max_fanout=4096&include_stats=true&max_result_bytes=1048576&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_with_stats}" | grep -q '"stats":{' || {
echo "subgraph include_stats missing stats block: ${subgraph_with_stats}" >&2
exit 1
}
echo "${subgraph_with_stats}" | grep -q '"plan":"' || {
echo "subgraph include_stats missing plan: ${subgraph_with_stats}" >&2
exit 1
}
paths_with_stats="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&include_stats=true&max_fanout=4096&max_result_bytes=1048576" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_with_stats}" | grep -q '"stats":{' || {
echo "paths include_stats missing stats block: ${paths_with_stats}" >&2
exit 1
}
echo "${paths_with_stats}" | grep -q '"plan":"' || {
echo "paths include_stats missing plan: ${paths_with_stats}" >&2
exit 1
}
gqb_node="$(
http_get "/v2/graph/nodes/gq-b" \
--header "X-Amduat-Space: ${space_id}"
)"
gqb_ref="$(printf '%s\n' "${gqb_node}" | extract_json_string "concept_ref")"
if [[ -z "${gqb_ref}" ]]; then
echo "failed to resolve gq-b concept ref: ${gqb_node}" >&2
exit 1
fi
changes_tombstone="$(
http_get "/v2/graph/changes?event_types[]=tombstone_applied&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${changes_tombstone}" | grep -q '"event":"tombstone_applied"' || {
echo "changes tombstone filter missing tombstone event: ${changes_tombstone}" >&2
exit 1
}
changes_filtered="$(
http_get "/v2/graph/changes?since_as_of=${cutoff_cursor}&predicates[]=ms.computed_by&roots[]=gq-b&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${changes_filtered}" | grep -q "\"${gqb_ref}\"" || {
echo "changes root/predicate filter missing gq-b involvement: ${changes_filtered}" >&2
exit 1
}
if echo "${changes_filtered}" | grep -q '"event":"version_published"'; then
echo "changes predicate filter unexpectedly includes version events: ${changes_filtered}" >&2
exit 1
fi
changes_wait_empty="$(
http_get "/v2/graph/changes?since_cursor=g1_999999&wait_ms=1&limit=1" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${changes_wait_empty}" | grep -q '"events":\[\]' || {
echo "changes wait_ms empty poll expected no events: ${changes_wait_empty}" >&2
exit 1
}
echo "ok: v2 graph query tests passed"

112
scripts/test_index_two_nodes.sh Executable file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/build/vendor/amduat/amduat-asl"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
if command -v curl >/dev/null 2>&1 && curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket and helper missing" >&2
exit 77
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-index-two-nodes-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="app1"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
wait_ready() {
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
return 1
fi
if [[ ! -S "${sock}" ]]; then
sleep 0.1
continue
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
if "${HTTP_HELPER}" --sock "${sock}" --method GET --path "/v2/readyz" >/dev/null 2>&1; then
return 0
fi
elif curl --silent --show-error --fail --unix-socket "${sock}" \
"http://localhost/v2/readyz" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
post_node() {
local name="$1"
local payload="{\"name\":\"${name}\"}"
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "/v2/graph/nodes" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" \
--data "${payload}"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
-H "Content-Type: application/json" \
-H "X-Amduat-Space: ${space_id}" \
-X POST --data-binary "${payload}" \
"http://localhost/v2/graph/nodes"
fi
}
mkdir -p "${root}"
"${ASL_BIN}" index init --root "${root}" --force >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
if ! wait_ready; then
echo "daemon not ready" >&2
exit 1
fi
resp1="$(post_node doca1)"
resp2="$(post_node topica1)"
echo "${resp1}" | grep -q '"name":"doca1"' || {
echo "first node write failed: ${resp1}" >&2
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
exit 1
}
echo "${resp2}" | grep -q '"name":"topica1"' || {
echo "second node write failed: ${resp2}" >&2
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
exit 1
}
echo "ok: two consecutive index-backed node writes succeeded"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,46 @@ typedef struct {
size_t cap; size_t cap;
} amduatd_edge_list_t; } amduatd_edge_list_t;
typedef struct {
amduat_reference_t ref;
size_t *edge_indices;
size_t len;
size_t cap;
} amduatd_ref_edge_bucket_t;
typedef struct {
amduat_reference_t left_ref;
amduat_reference_t right_ref;
size_t *edge_indices;
size_t len;
size_t cap;
} amduatd_ref_pair_edge_bucket_t;
typedef struct {
size_t built_for_edges_len;
size_t *alias_edge_indices;
size_t alias_len;
size_t alias_cap;
amduatd_ref_edge_bucket_t *src_buckets;
size_t src_len;
size_t src_cap;
amduatd_ref_edge_bucket_t *dst_buckets;
size_t dst_len;
size_t dst_cap;
amduatd_ref_edge_bucket_t *predicate_buckets;
size_t predicate_len;
size_t predicate_cap;
amduatd_ref_pair_edge_bucket_t *src_predicate_buckets;
size_t src_predicate_len;
size_t src_predicate_cap;
amduatd_ref_pair_edge_bucket_t *dst_predicate_buckets;
size_t dst_predicate_len;
size_t dst_predicate_cap;
amduatd_ref_edge_bucket_t *tombstoned_src_buckets;
size_t tombstoned_src_len;
size_t tombstoned_src_cap;
} amduatd_query_index_t;
typedef struct amduatd_concepts_t { typedef struct amduatd_concepts_t {
const char *root_path; const char *root_path;
char edges_path[1024]; char edges_path[1024];
@ -36,8 +76,10 @@ typedef struct amduatd_concepts_t {
amduat_reference_t rel_within_domain_ref; amduat_reference_t rel_within_domain_ref;
amduat_reference_t rel_computed_by_ref; amduat_reference_t rel_computed_by_ref;
amduat_reference_t rel_has_provenance_ref; amduat_reference_t rel_has_provenance_ref;
amduat_reference_t rel_tombstones_ref;
amduat_asl_collection_store_t edge_collection; amduat_asl_collection_store_t edge_collection;
amduatd_edge_list_t edges; amduatd_edge_list_t edges;
amduatd_query_index_t qindex;
} amduatd_concepts_t; } amduatd_concepts_t;
bool amduatd_concepts_init(amduatd_concepts_t *c, bool amduatd_concepts_init(amduatd_concepts_t *c,
@ -51,6 +93,8 @@ void amduatd_concepts_free(amduatd_concepts_t *c);
bool amduatd_concepts_refresh_edges(amduatd_ctx_t *ctx, bool amduatd_concepts_refresh_edges(amduatd_ctx_t *ctx,
size_t max_new_entries); size_t max_new_entries);
bool amduatd_concepts_ensure_query_index_ready(amduatd_concepts_t *c);
bool amduatd_concepts_can_handle(const amduatd_http_req_t *req); bool amduatd_concepts_can_handle(const amduatd_http_req_t *req);
bool amduatd_concepts_handle(amduatd_ctx_t *ctx, bool amduatd_concepts_handle(amduatd_ctx_t *ctx,

2
vendor/amduat vendored

@ -1 +1 @@
Subproject commit 9a2903072bd10b2bc09dd082681cc7c026bf9350 Subproject commit a433f92f13be5193642cd514272ae0faba4aed08