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
4 changes: 2 additions & 2 deletions .agents/skills/hypercerts-api/references/endpoint-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ Do not confuse actor follows with entity follows; their identifiers and list out
| List acknowledgements | `org.hypercerts.context.listAcknowledgements` | Filter by publisher or subject as documented. |
| Load one attachment | `org.hypercerts.context.getAttachment` | Exact record AT-URI in `uri`. |
| List attachments | `org.hypercerts.context.listAttachments` | Filter by publisher, record URI, subject, or content type. |
| Load one evaluation | `org.hypercerts.context.getEvaluation` | Exact record AT-URI in `uri`; includes publisher/evaluator projections. |
| List evaluations | `org.hypercerts.context.listEvaluations` | Filter by publisher, evaluator, or subject. |
| Load one evaluation | `org.hypercerts.context.getEvaluation` | Exact record AT-URI in `uri`; includes publisher/evaluator projections and preserves raw records when malformed evaluator entries are omitted. |
| List evaluations | `org.hypercerts.context.listEvaluations` | Filter by publisher, evaluator, or subject; malformed evaluator entries do not hide otherwise valid records. |
| Load one measurement | `org.hypercerts.context.getMeasurement` | Exact record AT-URI in `uri`. |
| List measurements | `org.hypercerts.context.listMeasurements` | Filter by publisher or subject. |

Expand Down
10 changes: 5 additions & 5 deletions .agents/skills/hypercerts-api/references/openapi.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .agents/skills/hypercerts-api/references/reliability.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ For a potentially fresh record that is absent:

A nullable actor profile does not imply an invalid DID; a missing organization sidecar does not prove the actor is missing; an unavailable exact CID does not prove another version at that URI is absent.

For `org.hypercerts.context.getEvaluation` and `listEvaluations`, the top-level `record` remains the indexed value. The separate hydrated `evaluators` array is a dense projection of entries shaped as objects with valid DIDs; malformed entries are omitted, and a missing or non-array source yields an empty projection. Do not treat an empty projection as proof that the raw record had no evaluator data. More than 1,000 raw entries remains outside the supported limit: exact lookup fails and lists skip that record.

## Diagnose failures without overgeneralizing

1. Record HTTP status and parse the JSON XRPC error code/message when present; preserve a fallback for empty or non-JSON bodies. See [request-patterns.md](request-patterns.md#handle-http-and-xrpc-errors).
Expand Down
5 changes: 5 additions & 0 deletions .changeset/tolerate-malformed-evaluation-evaluators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hypercerts-org/hypercerts-api': patch
---

Evaluation lookups and lists now retain records with malformed evaluator entries. The original record is preserved, while invalid entries are omitted from the hydrated evaluator projection. Records with more than 1,000 raw evaluator entries remain outside the supported limit.
10 changes: 5 additions & 5 deletions api/lexicons/org.hypercerts.context.getEvaluation.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"defs": {
"main": {
"type": "query",
"description": "Returns the indexed evaluation at the given AT-URI with its publisher and evaluator views. Authentication is not required.",
"description": "Returns the indexed evaluation at the given AT-URI with its publisher and evaluator views. Invalid evaluator entries are omitted from the hydrated projection while the original record is preserved. Authentication is not required.",
"parameters": {
"type": "params",
"required": ["uri"],
Expand All @@ -27,7 +27,7 @@
},
"hydratedEvaluationEvaluatorView": {
"type": "object",
"description": "An original evaluator entry with its profile and organization sidecars looked up.",
"description": "A valid original evaluator entry with its profile and organization sidecars looked up.",
"required": ["did", "hydrationStatus", "profile", "organization"],
"nullable": ["profile", "organization"],
"properties": {
Expand All @@ -39,7 +39,7 @@
},
"omittedEvaluationEvaluatorView": {
"type": "object",
"description": "An original evaluator entry not hydrated.",
"description": "A valid original evaluator entry beyond the hydration position limit.",
"required": ["did", "hydrationStatus"],
"properties": {
"did": { "type": "string", "format": "did" },
Expand All @@ -48,7 +48,7 @@
},
"evaluationView": {
"type": "object",
"description": "Evaluation with the complete original record, hydrated publisher, and evaluator views. For hydrated actors, missing profile or organization sidecars are null; query or hydration failures fail the request.",
"description": "Evaluation with the original record, hydrated publisher, and evaluator projections. The record is not rewritten when evaluator data is malformed. A missing or non-array evaluator container yields an empty hydrated array; malformed entries are omitted. For hydrated actors, missing profile or organization sidecars are null; query or hydration failures fail the request. Evaluator arrays over 1000 entries still fail exact lookup and are skipped by lists.",
"required": ["uri", "cid", "indexedAt", "did", "author", "record", "evaluators"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
Expand All @@ -60,7 +60,7 @@
"evaluators": {
"type": "array",
"maxLength": 1000,
"description": "One-for-one projection of record.evaluators, preserving source order, duplicates, and entry fields. Entries after position 100 remain present with hydrationStatus \"omitted\" and no profile or organization.",
"description": "Dense projection of valid record.evaluators object entries with valid DIDs, preserving their source order and duplicates. Entries with malformed shapes or invalid DIDs are omitted without changing the original record; a missing or non-array source yields an empty array. Valid entries at source positions after 100 remain present with hydrationStatus \"omitted\" and no profile or organization. The raw evaluator array is limited to 1000 entries; over-limit records fail exact lookup and are skipped by lists.",
"items": {
"type": "union",
"refs": ["#hydratedEvaluationEvaluatorView", "#omittedEvaluationEvaluatorView"]
Expand Down
2 changes: 1 addition & 1 deletion api/lexicons/org.hypercerts.context.listEvaluations.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"defs": {
"main": {
"type": "query",
"description": "Lists indexed evaluations with optional publisher, evaluator, and subject filters. Values within each array filter use OR; distinct filters use AND. Authentication is not required.",
"description": "Lists indexed evaluations with optional publisher, evaluator, and subject filters. Values within each array filter use OR; distinct filters use AND. Records remain listed when evaluator entries cannot be projected; malformed entries are omitted from hydrated views while the original record is preserved. Authentication is not required.",
"parameters": {
"type": "params",
"properties": {
Expand Down
55 changes: 31 additions & 24 deletions api/lua/endpoints/getEvaluation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,28 @@ local function evaluation_query(sql, values)
return result
end

local function evaluation_evaluator_array(value)
if type(value) ~= "table" then return nil end
local count = 0
for key in pairs(value) do
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then return nil end
count = count + 1
end
if count ~= #value then return nil end
return value
end

local function evaluation_view(row, skip_invalid)
local view = record_view(row)
if type(view.record) ~= "table" or type(view.record.evaluators) ~= "table" then
if type(view.record) ~= "table" then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation has no evaluator array", 0)
end
if #view.record.evaluators > 1000 then
local evaluators = evaluation_evaluator_array(view.record.evaluators)
if evaluators and #evaluators > 1000 then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation exceeds the evaluator limit", 0)
end
for _, evaluator in ipairs(view.record.evaluators) do
if type(evaluator) ~= "table" or not valid_did(evaluator.did) then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation contains an invalid evaluator DID", 0)
end
end
view.author = { did = row.did }
return view
end
Expand All @@ -123,23 +129,24 @@ local function hydrate_evaluation_views(views)
for view_index, view in ipairs(views) do
actors[#actors + 1] = view.author
local projections, hydrated_actors = {}, {}
for position, source in ipairs(view.record.evaluators) do
if type(source) ~= "table" or not valid_did(source.did) then
error("EvaluationQueryFailed: indexed evaluation contains an invalid evaluator DID", 0)
end
local evaluator = {}
for key, value in pairs(source) do evaluator[key] = value end
if position <= EVALUATOR_HYDRATION_LIMIT then
evaluator.hydrationStatus = "hydrated"
local actor = { did = source.did }
actors[#actors + 1] = actor
hydrated_actors[position] = actor
else
evaluator.hydrationStatus = "omitted"
evaluator.profile = nil
evaluator.organization = nil
local sources = evaluation_evaluator_array(view.record.evaluators) or {}
for source_position, source in ipairs(sources) do
if type(source) == "table" and valid_did(source.did) then
local projection_position = #projections + 1
local evaluator = {}
for key, value in pairs(source) do evaluator[key] = value end
if source_position <= EVALUATOR_HYDRATION_LIMIT then
evaluator.hydrationStatus = "hydrated"
local actor = { did = source.did }
actors[#actors + 1] = actor
hydrated_actors[projection_position] = actor
else
evaluator.hydrationStatus = "omitted"
evaluator.profile = nil
evaluator.organization = nil
end
projections[projection_position] = evaluator
end
projections[position] = evaluator
end
evaluator_projections[view_index] = projections
evaluator_actors[view_index] = hydrated_actors
Expand Down
55 changes: 31 additions & 24 deletions api/lua/endpoints/listEvaluations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,28 @@ local function evaluation_query(sql, values)
return result
end

local function evaluation_evaluator_array(value)
if type(value) ~= "table" then return nil end
local count = 0
for key in pairs(value) do
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then return nil end
count = count + 1
end
if count ~= #value then return nil end
return value
end

local function evaluation_view(row, skip_invalid)
local view = record_view(row)
if type(view.record) ~= "table" or type(view.record.evaluators) ~= "table" then
if type(view.record) ~= "table" then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation has no evaluator array", 0)
end
if #view.record.evaluators > 1000 then
local evaluators = evaluation_evaluator_array(view.record.evaluators)
if evaluators and #evaluators > 1000 then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation exceeds the evaluator limit", 0)
end
for _, evaluator in ipairs(view.record.evaluators) do
if type(evaluator) ~= "table" or not valid_did(evaluator.did) then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation contains an invalid evaluator DID", 0)
end
end
view.author = { did = row.did }
return view
end
Expand All @@ -165,23 +171,24 @@ local function hydrate_evaluation_views(views)
for view_index, view in ipairs(views) do
actors[#actors + 1] = view.author
local projections, hydrated_actors = {}, {}
for position, source in ipairs(view.record.evaluators) do
if type(source) ~= "table" or not valid_did(source.did) then
error("EvaluationQueryFailed: indexed evaluation contains an invalid evaluator DID", 0)
end
local evaluator = {}
for key, value in pairs(source) do evaluator[key] = value end
if position <= EVALUATOR_HYDRATION_LIMIT then
evaluator.hydrationStatus = "hydrated"
local actor = { did = source.did }
actors[#actors + 1] = actor
hydrated_actors[position] = actor
else
evaluator.hydrationStatus = "omitted"
evaluator.profile = nil
evaluator.organization = nil
local sources = evaluation_evaluator_array(view.record.evaluators) or {}
for source_position, source in ipairs(sources) do
if type(source) == "table" and valid_did(source.did) then
local projection_position = #projections + 1
local evaluator = {}
for key, value in pairs(source) do evaluator[key] = value end
if source_position <= EVALUATOR_HYDRATION_LIMIT then
evaluator.hydrationStatus = "hydrated"
local actor = { did = source.did }
actors[#actors + 1] = actor
hydrated_actors[projection_position] = actor
else
evaluator.hydrationStatus = "omitted"
evaluator.profile = nil
evaluator.organization = nil
end
projections[projection_position] = evaluator
end
projections[position] = evaluator
end
evaluator_projections[view_index] = projections
evaluator_actors[view_index] = hydrated_actors
Expand Down
55 changes: 31 additions & 24 deletions api/lua/shared/evaluation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,28 @@ local function evaluation_query(sql, values)
return result
end

local function evaluation_evaluator_array(value)
if type(value) ~= "table" then return nil end
local count = 0
for key in pairs(value) do
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then return nil end
count = count + 1
end
if count ~= #value then return nil end
return value
end

local function evaluation_view(row, skip_invalid)
local view = record_view(row)
if type(view.record) ~= "table" or type(view.record.evaluators) ~= "table" then
if type(view.record) ~= "table" then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation has no evaluator array", 0)
end
if #view.record.evaluators > 1000 then
local evaluators = evaluation_evaluator_array(view.record.evaluators)
if evaluators and #evaluators > 1000 then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation exceeds the evaluator limit", 0)
end
for _, evaluator in ipairs(view.record.evaluators) do
if type(evaluator) ~= "table" or not valid_did(evaluator.did) then
if skip_invalid then return nil end
error("EvaluationQueryFailed: indexed evaluation contains an invalid evaluator DID", 0)
end
end
view.author = { did = row.did }
return view
end
Expand All @@ -40,23 +46,24 @@ local function hydrate_evaluation_views(views)
for view_index, view in ipairs(views) do
actors[#actors + 1] = view.author
local projections, hydrated_actors = {}, {}
for position, source in ipairs(view.record.evaluators) do
if type(source) ~= "table" or not valid_did(source.did) then
error("EvaluationQueryFailed: indexed evaluation contains an invalid evaluator DID", 0)
end
local evaluator = {}
for key, value in pairs(source) do evaluator[key] = value end
if position <= EVALUATOR_HYDRATION_LIMIT then
evaluator.hydrationStatus = "hydrated"
local actor = { did = source.did }
actors[#actors + 1] = actor
hydrated_actors[position] = actor
else
evaluator.hydrationStatus = "omitted"
evaluator.profile = nil
evaluator.organization = nil
local sources = evaluation_evaluator_array(view.record.evaluators) or {}
for source_position, source in ipairs(sources) do
if type(source) == "table" and valid_did(source.did) then
local projection_position = #projections + 1
local evaluator = {}
for key, value in pairs(source) do evaluator[key] = value end
if source_position <= EVALUATOR_HYDRATION_LIMIT then
evaluator.hydrationStatus = "hydrated"
local actor = { did = source.did }
actors[#actors + 1] = actor
hydrated_actors[projection_position] = actor
else
evaluator.hydrationStatus = "omitted"
evaluator.profile = nil
evaluator.organization = nil
end
projections[projection_position] = evaluator
end
projections[position] = evaluator
end
evaluator_projections[view_index] = projections
evaluator_actors[view_index] = hydrated_actors
Expand Down
Loading
Loading