feat: list Creator-owned continuity items
CI / test (push) Failing after 14m51s

This commit is contained in:
m
2026-08-16 15:54:30 +03:00
parent 6a5550b7b0
commit 2877571bb3
6 changed files with 111 additions and 1 deletions
@@ -6,6 +6,7 @@ import (
"fmt"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -35,3 +36,23 @@ func (r *ContinuityRepository) Load(ctx context.Context, id continuity.Prospecti
}
return item, nil
}
func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.ProspectiveItem, error) {
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE owner_principal_id=$1 ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $2`, owner, limit)
if err != nil {
return nil, fmt.Errorf("list prospective items by owner: %w", err)
}
defer rows.Close()
items := make([]continuity.ProspectiveItem, 0)
for rows.Next() {
var item continuity.ProspectiveItem
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
return nil, fmt.Errorf("scan prospective item: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("list prospective items by owner: %w", err)
}
return items, nil
}
+22
View File
@@ -0,0 +1,22 @@
package main
import (
"fmt"
"strconv"
)
const (
defaultListLimit = 25
maximumListLimit = 100
)
func parseListLimit(raw string) (int, error) {
if raw == "" {
return defaultListLimit, nil
}
limit, err := strconv.Atoi(raw)
if err != nil || limit < 1 || limit > maximumListLimit {
return 0, fmt.Errorf("limit must be between 1 and %d", maximumListLimit)
}
return limit, nil
}
+17
View File
@@ -0,0 +1,17 @@
package main
import "testing"
func TestParseListLimitIsBounded(t *testing.T) {
if limit, err := parseListLimit(""); err != nil || limit != defaultListLimit {
t.Fatalf("default limit = %d, err = %v", limit, err)
}
if limit, err := parseListLimit("100"); err != nil || limit != 100 {
t.Fatalf("maximum limit = %d, err = %v", limit, err)
}
for _, value := range []string{"0", "101", "invalid", "-1"} {
if _, err := parseListLimit(value); err == nil {
t.Fatalf("invalid limit %q accepted", value)
}
}
}
+25
View File
@@ -73,6 +73,31 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(item)
})
mux.HandleFunc("GET /v1/creator/prospective-items", func(w http.ResponseWriter, r *http.Request) {
session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization"))
if errors.Is(err, errCreatorSessionDenied) {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
return
}
limit, err := parseListLimit(r.URL.Query().Get("limit"))
if err != nil {
http.Error(w, "invalid list limit", http.StatusBadRequest)
return
}
items, err := repository.ByOwner(r.Context(), session.PrincipalID, limit)
if err != nil {
http.Error(w, "prospective items unavailable", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Items []continuity.ProspectiveItem
}{Items: items})
})
log.Printf("continuity-runtime listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
@@ -0,0 +1,17 @@
{
"task_id": "DEV-000028",
"task_revision": 1,
"architecture_revision": "bootstrap-1",
"base_repo_revision": "6a5550b7b0cf5ae80ac31e042a800ca1f1796788",
"risk_tier": "C",
"intent": "List a bounded set of Continuity-owned ProspectiveItems for the authenticated Creator Principal so the console no longer requires prior knowledge of item identifiers.",
"allowed_change_scope": {"paths": ["adapters/postgres/continuity_repository.go", "cmd/continuity-runtime/**", "tests/creator_continuity/**", "dev/tasks/DEV-000028-creator-continuity-list.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/postgres/identity_repository.go", "cmd/core-runtime/**", "cmd/commit-runtime/**", "cmd/creator-web/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0},
"contracts": {"consume": ["contracts.identity@v1", "contracts.continuity@v1"], "may_change": []},
"semantic_owner": "continuity",
"assumptions": ["Trigger time followed by identifier is a stable deterministic V1 listing order", "A hard maximum of 100 items is sufficient for the current operator console"],
"acceptance_scenarios": ["list contains only ProspectiveItems owned by the authenticated Principal", "list size is bounded and deterministically ordered", "invalid limits are rejected", "list query does not mutate canonical Continuity state"],
"required_tests": ["unit", "repository integration", "owner-principal isolation", "runtime HTTP", "architecture", "remote Docker smoke"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
}
+9 -1
View File
@@ -55,4 +55,12 @@ $owned = docker --context $DockerContext exec --env "CREATOR_TEST_SESSION=$creat
if ($LASTEXITCODE -ne 0 -or ($owned | ConvertFrom-Json).ID -ne $ownedID) { throw "owned ProspectiveItem was not returned" }
Expect-GetStatus "$continuityURL/v1/creator/prospective-items/$foreignID" 403 $creatorSession
Write-Output "DEV-000026 authenticated Creator Continuity API: PASS"
$listedResponse = docker --context $DockerContext exec --env "CREATOR_TEST_SESSION=$creatorSession" $continuityContainer sh -c 'wget -qO- --header="Authorization: Bearer $CREATOR_TEST_SESSION" "$0"' "$continuityURL/v1/creator/prospective-items?limit=100"
if ($LASTEXITCODE -ne 0) { throw "Creator ProspectiveItem list failed" }
$listed = $listedResponse | ConvertFrom-Json
if (-not ($listed.Items | Where-Object ID -eq $ownedID)) { throw "owned ProspectiveItem missing from list" }
if ($listed.Items | Where-Object ID -eq $foreignID) { throw "foreign ProspectiveItem leaked into list" }
if ($listed.Items.Count -gt 100) { throw "ProspectiveItem list exceeded hard limit" }
Expect-GetStatus "$continuityURL/v1/creator/prospective-items?limit=101" 400 $creatorSession
Write-Output "DEV-000028 Creator-owned Continuity list: PASS"