Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/hypercerts-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Read the relevant reference, then inspect the exact operation in the [bundled Op

## Bundled API contract

- [`references/openapi.json`](references/openapi.json) is a self-contained snapshot of all 55 query operations, including parameters, response schemas, declared XRPC errors, and their referenced schema definitions. The included contract makes the skill self-contained when installed separately.
- [`references/openapi.json`](references/openapi.json) is a self-contained snapshot of all 59 query operations, including parameters, response schemas, declared XRPC errors, and their referenced schema definitions. The included contract makes the skill self-contained when installed separately.
- To inspect an exact contract, find `/xrpc/<full NSID>` in the snapshot, read its operation, and follow any `#/components/schemas/...` references by schema name. Each operation and schema is stored on its own line for focused lookup.
- The snapshot describes declared behavior, not endpoint availability or data freshness on a particular host. Its OpenAPI `info.version` is not the deployed server's version.
- Use the API directly when a task needs live indexed results. Do not send extra diagnostic probes just to validate deployment; a deployment-specific availability check requires explicit approval for the target.
14 changes: 13 additions & 1 deletion .agents/skills/hypercerts-api/references/endpoint-map.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Endpoint map: choose by consumer task

This task index lists the 55 public query NSIDs in the bundled [OpenAPI contract](openapi.json). It helps choose an operation but does not replace its exact contract: inspect that operation and its referenced schemas for required parameters, types, defaults, limits, filter semantics, response fields, and declared errors. The static snapshot describes the contract, not availability or freshness on a selected deployment.
This task index lists the 59 public query NSIDs in the bundled [OpenAPI contract](openapi.json). It helps choose an operation but does not replace its exact contract: inspect that operation and its referenced schemas for required parameters, types, defaults, limits, filter semantics, response fields, and declared errors. The static snapshot describes the contract, not availability or freshness on a selected deployment.

## Fast selection rules

Expand All @@ -9,6 +9,7 @@ This task index lists the 55 public query NSIDs in the bundled [OpenAPI contract
- **Browse or filter without free text:** choose `list...` in the relevant domain.
- **Text discovery:** use `search...` only where listed below. A search query is not interchangeable with a list filter, and search semantics vary by endpoint.
- **Relationship checks:** use `get...Follow` for a single pair; list queries answer who follows whom or which entities an actor follows.
- **Feed reactions:** use `getLikes` or `getReposts` for distinct actors reacting to a subject URI, or `getActorLikes` / `getActorReposts` for an actor's own raw representative reactions.

Inspect the exact operation in the bundled OpenAPI contract before constructing a request: names such as `authors`, `actors`, `uri`, and `collection` have endpoint-specific meanings. Do not infer that a filter, search operation, output array name, or error exists just because a related endpoint has one.

Expand Down Expand Up @@ -82,6 +83,17 @@ An `unanswered` award is a successful indexed award result, not a not-found resp

Do not confuse actor follows with entity follows; their identifiers and list outputs differ. Recent raw follows preserve duplicates and are not a snapshot; consult that endpoint's description before using cursors.

## Feed reactions (4)

| User task | Query NSID | Practical choice |
|---|---|---|
| List actors who liked a record | `app.certified.feed.getLikes` | `subject` is a full record AT-URI; matching ignores CID. The response includes a total distinct-actor count, paginated actor sidecars, and each actor's raw representative like. |
| List actors who reposted a record | `app.certified.feed.getReposts` | `subject` is a full record AT-URI; matching ignores CID. The response includes a total distinct-actor count, paginated actor sidecars, and each actor's raw representative repost. |
| List an actor's likes | `app.certified.feed.getActorLikes` | `actor` is a repository-owner DID. Returns raw like records, one per subject URI; subjects need not be indexed. |
| List an actor's reposts | `app.certified.feed.getActorReposts` | `actor` is a repository-owner DID. Returns raw repost records, one per subject URI; subjects need not be indexed. |

Reaction queries use the earliest record per author/subject URI, with URI ascending as the tie-breaker. They sort pages by the representative record's `createdAt` and URI; check each operation for exact cursor and response details.

## Context and supporting records (8)

| User task | Query NSID | Practical choice |
Expand Down
22 changes: 18 additions & 4 deletions .agents/skills/hypercerts-api/references/openapi.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .changeset/feed-reactions-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hypercerts-org/hypercerts-api': minor
---

Add public queries for actors who liked or reposted a record and for an actor's own likes and reposts. Subject queries include distinct-actor totals; all four queries support bounded pagination.
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This updates `docs/sources/index.json`, the committed Lexicon snapshots, `docs/o

## HTTP runtime tests

`pnpm test:http` runs the suites in `api/tests/http` against the activity, badge-definition, badge-query, EVM-link, acknowledgement, collection, context-measurement, context attachment and evaluation, contributor-information, funding, location, profile, organization, feature, work-scope-tag, rights, vocabulary-tag, graph, and contribution query XRPC endpoints installed from this checkout. These tests exercise real HTTP behavior against PostgreSQL, not only Lua handlers with a fake database.
`pnpm test:http` runs the suites in `api/tests/http` against the activity, badge-definition, badge-query, EVM-link, acknowledgement, collection, context-measurement, context attachment and evaluation, contributor-information, funding, location, profile, organization, feature, work-scope-tag, rights, vocabulary-tag, graph, contribution, and feed-reaction query XRPC endpoints installed from this checkout. These tests exercise real HTTP behavior against PostgreSQL, not only Lua handlers with a fake database.

The local runner requires a local Docker Compose daemon, `psql`, and the pinned PostgreSQL and HappyView images already cached locally. Set `PSQL_PATH` to the absolute path of a trusted `psql` executable:

Expand Down Expand Up @@ -57,6 +57,7 @@ The HTTP gate fails if it discovers no suites, executes no `node:test` cases, or
- Activity retrieval with contributor-sidecar hydration, author/organization/contributor/URI filters, tied timestamp pagination, and literal wildcard search.
- Collection retrieval with CBOR-derived CIDs, location/tag projections, author, organization, item and tag filters, title/shortDescription search, `createdAt`/URI pagination ties, and source-order item pagination with exact-version resolution.
- Graph actor/entity lookups and lists, tied-key pagination, nullable profile/organization sidecars, entity target resolution, and the global recent-follows `before` filter.
- Feed-reaction subject and actor queries, earliest duplicate selection, URI-only subject matching across CIDs, distinct-actor totals, tied cursor pagination, page-only sidecar hydration, raw `via`/signature preservation, and actor queries without hydration.
- Contribution exact-record retrieval, repeated publisher filters, tied ascending/descending cursor pagination, nullable publisher sidecars, and named errors.
- Badge, badge-query, acknowledgement, contribution, and rights HTTP fixtures with CBOR-derived record CIDs; contribution DIDs are distinct from baseline fixture identities.
- Location retrieval with nullable sidecars, repeated author/URI/location-type filters, unsupported-search errors, tied pagination in both directions, malformed/absent `createdAt` handling, and named errors.
Expand Down
6 changes: 6 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ This package owns the shared API installer and build tooling, pinned upstream Le

Use the [`docs/` endpoint explorer](../docs/) and generated [OpenAPI reference](../docs/openapi.json) for endpoint names and contracts. The [coverage report](../docs/coverage.json) lists manifest-registered operations; inclusion does not imply runtime or deployment validation. See [docs/README.md](../docs/README.md) to run the explorer locally and refresh its committed reference.

## Feed reaction capability

`modules/feed-reactions/manifest.json` owns the like and repost record schemas and four public queries: `app.certified.feed.getLikes`, `getReposts`, `getActorLikes`, and `getActorReposts`. The root manifest includes this independently installable module in the aggregate bundle. Its record schemas enable backfill, and queries read the current indexed records directly; deleted reactions therefore stop appearing once removed from HappyView's index, without a separate stored count.

Subject queries match by AT-URI without requiring a subject CID match, return one earliest reaction per author, and include a distinct-actor total with nullable profile and organization sidecars. Actor queries return raw reaction records without subject hydration. All four use bounded `createdAt`/URI cursor pagination.

## Local validation

From the repository root:
Expand Down
33 changes: 33 additions & 0 deletions api/lexicons/app.certified.feed.getActorLikes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"lexicon": 1,
"id": "app.certified.feed.getActorLikes",
"defs": {
"main": {
"type": "query",
"description": "Lists an actor's indexed likes, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw like records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.",
"parameters": {
"type": "params",
"required": ["actor"],
"properties": {
"actor": { "type": "string", "format": "did", "description": "Repository-owner DID of the actor whose likes to list." },
"sortDirection": { "type": "string", "enum": ["asc", "desc"], "description": "Sort by representative like createdAt and URI; defaults to desc." },
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Maximum relationships per page; defaults to 25." },
"cursor": { "type": "string", "description": "Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages." }
}
},
"output": { "encoding": "application/json", "schema": { "type": "ref", "ref": "#output" } },
"errors": [
{ "name": "InvalidRequest", "description": "The actor DID, sort direction, limit, cursor, or another query parameter is invalid." },
{ "name": "LikeQueryFailed", "description": "The actor likes query failed; retry the request." }
]
},
"output": {
"type": "object",
"required": ["likes"],
"properties": {
"likes": { "type": "array", "items": { "type": "ref", "ref": "app.certified.feed.getLikes#likeRecordView" } },
"cursor": { "type": "string", "description": "Next-page cursor; omitted when pagination is complete." }
}
}
}
}
33 changes: 33 additions & 0 deletions api/lexicons/app.certified.feed.getActorReposts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"lexicon": 1,
"id": "app.certified.feed.getActorReposts",
"defs": {
"main": {
"type": "query",
"description": "Lists an actor's indexed reposts, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw repost records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.",
"parameters": {
"type": "params",
"required": ["actor"],
"properties": {
"actor": { "type": "string", "format": "did", "description": "Repository-owner DID of the actor whose reposts to list." },
"sortDirection": { "type": "string", "enum": ["asc", "desc"], "description": "Sort by representative repost createdAt and URI; defaults to desc." },
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Maximum relationships per page; defaults to 25." },
"cursor": { "type": "string", "description": "Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages." }
}
},
"output": { "encoding": "application/json", "schema": { "type": "ref", "ref": "#output" } },
"errors": [
{ "name": "InvalidRequest", "description": "The actor DID, sort direction, limit, cursor, or another query parameter is invalid." },
{ "name": "RepostQueryFailed", "description": "The actor reposts query failed; retry the request." }
]
},
"output": {
"type": "object",
"required": ["reposts"],
"properties": {
"reposts": { "type": "array", "items": { "type": "ref", "ref": "app.certified.feed.getReposts#repostRecordView" } },
"cursor": { "type": "string", "description": "Next-page cursor; omitted when pagination is complete." }
}
}
}
}
59 changes: 59 additions & 0 deletions api/lexicons/app.certified.feed.getLikes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"lexicon": 1,
"id": "app.certified.feed.getLikes",
"defs": {
"main": {
"type": "query",
"description": "Lists distinct actors liking a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.",
"parameters": {
"type": "params",
"required": ["subject"],
"properties": {
"subject": { "type": "string", "format": "at-uri", "description": "Full DID-authority record AT-URI; any target collection is accepted." },
"sortDirection": { "type": "string", "enum": ["asc", "desc"], "description": "Sort by representative like createdAt and URI; defaults to desc." },
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Maximum relationships per page; defaults to 25." },
"cursor": { "type": "string", "description": "Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages." }
}
},
"output": { "encoding": "application/json", "schema": { "type": "ref", "ref": "#output" } },
"errors": [
{ "name": "InvalidRequest", "description": "The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid." },
{ "name": "LikeQueryFailed", "description": "The like query, total count, or required actor hydration failed; retry the request." }
]
},
"likeRecordView": {
"type": "object",
"description": "Raw indexed like preserving the full record, including subject, via, and signatures when present. Duplicates use the earliest createdAt, with URI ascending on ties.",
"required": ["uri", "cid", "indexedAt", "did", "record"],
"nullable": ["indexedAt"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"indexedAt": { "type": "string", "format": "datetime" },
"did": { "type": "string", "format": "did", "description": "Repository-owner DID of the like publisher." },
"record": { "type": "ref", "ref": "app.certified.feed.like" }
}
},
"likeView": {
"type": "object",
"description": "Liker actor sidecars paired with the representative raw like record. Missing profile and organization records are null.",
"required": ["did", "profile", "organization", "like"],
"nullable": ["profile", "organization"],
"properties": {
"did": { "type": "string", "format": "did" },
"profile": { "type": "ref", "ref": "org.hypercerts.api.defs#profileView" },
"organization": { "type": "ref", "ref": "org.hypercerts.api.defs#organizationView" },
"like": { "type": "ref", "ref": "#likeRecordView" }
}
},
"output": {
"type": "object",
"required": ["likeCount", "likes"],
"properties": {
"likeCount": { "type": "integer", "minimum": 0, "description": "Total distinct indexed author DIDs liking this subject URI, independent of page size. Zero when no likes are indexed; totals may change between requests." },
"likes": { "type": "array", "items": { "type": "ref", "ref": "#likeView" } },
"cursor": { "type": "string", "description": "Next-page cursor; omitted when pagination is complete." }
}
}
}
}
Loading
Loading