This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user