@sudobility/sider_types 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +68 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/recipe-hash.d.ts +4 -0
- package/dist/recipe-hash.js +23 -0
- package/dist/templating.d.ts +13 -0
- package/dist/templating.js +67 -0
- package/package.json +1 -1
- package/src/api.ts +78 -1
- package/src/index.ts +2 -0
- package/src/recipe-hash.ts +25 -0
- package/src/templating.ts +70 -0
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { IsoTimestamp, JSONSchema } from "./common";
|
|
2
|
+
import type { EndpointGraphEdge, InvocationRecipe, SafetyClass, SecretSlot, Site, ToolSpec, ToolStatus, UXRecipe } from "./registry";
|
|
3
|
+
import type { CaptureBatch, Contributor, Observation, StepKind } from "./runtime";
|
|
3
4
|
/** A planner decision (mirror of the ShapeShyft plan-next-step output). */
|
|
4
5
|
export interface PlannerStep {
|
|
5
6
|
kind: StepKind;
|
|
@@ -52,3 +53,68 @@ export interface StatsResponse {
|
|
|
52
53
|
trustedToolCount: number;
|
|
53
54
|
contributorCount: number;
|
|
54
55
|
}
|
|
56
|
+
/** GET /api/v1/me — the signed-in contributor + aggregate counts. */
|
|
57
|
+
export interface MeResponse {
|
|
58
|
+
contributor: Contributor;
|
|
59
|
+
batchCount: number;
|
|
60
|
+
observationCount: number;
|
|
61
|
+
/** Tools this user has corroborated (cast a recipe vote on). */
|
|
62
|
+
toolsContributed: number;
|
|
63
|
+
/** Of those, how many are currently `trusted`. */
|
|
64
|
+
trustedToolsContributed: number;
|
|
65
|
+
}
|
|
66
|
+
/** One row of GET /api/v1/me/capture-batches. */
|
|
67
|
+
export interface MyCaptureBatchSummary extends CaptureBatch {
|
|
68
|
+
siteOrigin: string;
|
|
69
|
+
siteName: string;
|
|
70
|
+
}
|
|
71
|
+
/** GET /api/v1/me/capture-batches/:id — a batch plus its observations. */
|
|
72
|
+
export interface MyCaptureBatchDetail {
|
|
73
|
+
batch: MyCaptureBatchSummary;
|
|
74
|
+
observations: Observation[];
|
|
75
|
+
}
|
|
76
|
+
/** One row of GET /api/v1/me/tools — a tool this user corroborated. */
|
|
77
|
+
export interface MyToolEntry {
|
|
78
|
+
toolId: string;
|
|
79
|
+
siteId: string;
|
|
80
|
+
name: string;
|
|
81
|
+
status: ToolStatus;
|
|
82
|
+
safetyClass: SafetyClass;
|
|
83
|
+
version: number;
|
|
84
|
+
corroboratingUserCount: number;
|
|
85
|
+
/** Hash of MY independently-derived recipe. */
|
|
86
|
+
myRecipeHash: string;
|
|
87
|
+
/** Hash of the recipe currently stored on the tool. */
|
|
88
|
+
currentRecipeHash: string;
|
|
89
|
+
/** Whether my vote agrees with the stored recipe. */
|
|
90
|
+
agreesWithCurrent: boolean;
|
|
91
|
+
votedAt: IsoTimestamp;
|
|
92
|
+
}
|
|
93
|
+
/** GET /api/v1/tools/:id — full tool detail for the registry inspector. */
|
|
94
|
+
export interface ToolDetailResponse {
|
|
95
|
+
/** The tools table row (recipe, schema, safety, status, counts). */
|
|
96
|
+
tool: {
|
|
97
|
+
id: string;
|
|
98
|
+
siteId: string;
|
|
99
|
+
name: string;
|
|
100
|
+
description: string;
|
|
101
|
+
capabilityLabel: string;
|
|
102
|
+
inputSchema: JSONSchema;
|
|
103
|
+
safetyClass: SafetyClass;
|
|
104
|
+
directCallAvailable: boolean;
|
|
105
|
+
recipe: InvocationRecipe;
|
|
106
|
+
status: ToolStatus;
|
|
107
|
+
version: number;
|
|
108
|
+
observationCount: number;
|
|
109
|
+
corroboratingUserCount: number;
|
|
110
|
+
confidence: number;
|
|
111
|
+
createdAt: IsoTimestamp;
|
|
112
|
+
updatedAt: IsoTimestamp;
|
|
113
|
+
};
|
|
114
|
+
/** Anonymized corroboration breakdown: recipe hash → distinct-user count. */
|
|
115
|
+
corroboration: {
|
|
116
|
+
distinctUsers: number;
|
|
117
|
+
hashCounts: Record<string, number>;
|
|
118
|
+
trustThreshold: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { InvocationRecipe } from "./registry";
|
|
2
|
+
/** Stable stringify (sorted keys) so two users' equivalent recipes hash equal. */
|
|
3
|
+
export declare function canonicalizeRecipe(recipe: InvocationRecipe): string;
|
|
4
|
+
export declare function hashRecipe(recipe: InvocationRecipe): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Recipe canonicalization + corroboration hashing. Pure domain logic shared by
|
|
2
|
+
// the backend (trust engine: distinct-recipe-hash quorum) and the frontend. A
|
|
3
|
+
// stable sorted-key stringify makes two users' equivalent recipes hash equal.
|
|
4
|
+
/** Stable stringify (sorted keys) so two users' equivalent recipes hash equal. */
|
|
5
|
+
export function canonicalizeRecipe(recipe) {
|
|
6
|
+
return stableStringify(recipe);
|
|
7
|
+
}
|
|
8
|
+
export function hashRecipe(recipe) {
|
|
9
|
+
const s = canonicalizeRecipe(recipe);
|
|
10
|
+
let h = 0;
|
|
11
|
+
for (let i = 0; i < s.length; i++)
|
|
12
|
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
13
|
+
return (h >>> 0).toString(16);
|
|
14
|
+
}
|
|
15
|
+
function stableStringify(v) {
|
|
16
|
+
if (v === null || typeof v !== "object")
|
|
17
|
+
return JSON.stringify(v) ?? "null";
|
|
18
|
+
if (Array.isArray(v))
|
|
19
|
+
return `[${v.map(stableStringify).join(",")}]`;
|
|
20
|
+
const obj = v;
|
|
21
|
+
const keys = Object.keys(obj).sort();
|
|
22
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Observation } from "./runtime";
|
|
2
|
+
/** Replace id-like path segments with `{id}`: /sections/120/seats → /sections/{id}/seats */
|
|
3
|
+
export declare function templatePath(rawUrl: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* GraphQL keying: one URL serves many operations, so cluster on operation name,
|
|
6
|
+
* not path. Returns undefined for non-GraphQL requests.
|
|
7
|
+
*/
|
|
8
|
+
export declare function graphqlOperationOf(url: string, body: unknown): string | undefined;
|
|
9
|
+
type Clusterable = Pick<Observation, "method" | "url" | "requestBody">;
|
|
10
|
+
/** Stable key identifying the endpoint template an observation belongs to. */
|
|
11
|
+
export declare function endpointKey(o: Clusterable): string;
|
|
12
|
+
export declare function clusterObservations<T extends Clusterable>(observations: T[]): Map<string, T[]>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Deterministic clustering & path templating. Pure domain logic shared by the
|
|
2
|
+
// backend (distillation) and the frontend security lib — groups raw observations
|
|
3
|
+
// into endpoint templates so the brain infers semantics once per endpoint, not
|
|
4
|
+
// once per request. No runtime dependencies.
|
|
5
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
const LONGHEX_RE = /^[0-9a-f]{16,}$/i;
|
|
7
|
+
/** Replace id-like path segments with `{id}`: /sections/120/seats → /sections/{id}/seats */
|
|
8
|
+
export function templatePath(rawUrl) {
|
|
9
|
+
let pathname = rawUrl;
|
|
10
|
+
try {
|
|
11
|
+
pathname = new URL(rawUrl, "http://sider.local").pathname;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* keep raw */
|
|
15
|
+
}
|
|
16
|
+
return pathname
|
|
17
|
+
.split("/")
|
|
18
|
+
.map((seg) => {
|
|
19
|
+
if (seg === "")
|
|
20
|
+
return seg;
|
|
21
|
+
if (/^\d+$/.test(seg))
|
|
22
|
+
return "{id}";
|
|
23
|
+
if (UUID_RE.test(seg))
|
|
24
|
+
return "{id}";
|
|
25
|
+
if (LONGHEX_RE.test(seg))
|
|
26
|
+
return "{id}";
|
|
27
|
+
return seg;
|
|
28
|
+
})
|
|
29
|
+
.join("/");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* GraphQL keying: one URL serves many operations, so cluster on operation name,
|
|
33
|
+
* not path. Returns undefined for non-GraphQL requests.
|
|
34
|
+
*/
|
|
35
|
+
export function graphqlOperationOf(url, body) {
|
|
36
|
+
if (body && typeof body === "object") {
|
|
37
|
+
const b = body;
|
|
38
|
+
if (typeof b["operationName"] === "string" && b["operationName"]) {
|
|
39
|
+
return b["operationName"];
|
|
40
|
+
}
|
|
41
|
+
if (typeof b["query"] === "string") {
|
|
42
|
+
const m = /\b(query|mutation|subscription)\s+([A-Za-z0-9_]+)/.exec(b["query"]);
|
|
43
|
+
if (m && m[2])
|
|
44
|
+
return m[2];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (/\/graphql\b/i.test(url))
|
|
48
|
+
return "anonymous";
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Stable key identifying the endpoint template an observation belongs to. */
|
|
52
|
+
export function endpointKey(o) {
|
|
53
|
+
const op = graphqlOperationOf(o.url, o.requestBody);
|
|
54
|
+
return `${o.method} ${templatePath(o.url)}${op ? ` #${op}` : ""}`;
|
|
55
|
+
}
|
|
56
|
+
export function clusterObservations(observations) {
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
for (const o of observations) {
|
|
59
|
+
const key = endpointKey(o);
|
|
60
|
+
const g = groups.get(key);
|
|
61
|
+
if (g)
|
|
62
|
+
g.push(o);
|
|
63
|
+
else
|
|
64
|
+
groups.set(key, [o]);
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
// (to type its handlers) and sider_client (to type its calls). Keeping these
|
|
3
3
|
// here is the single source of truth that stops the two sides from drifting.
|
|
4
4
|
|
|
5
|
+
import type { IsoTimestamp, JSONSchema } from "./common";
|
|
5
6
|
import type {
|
|
6
7
|
EndpointGraphEdge,
|
|
8
|
+
InvocationRecipe,
|
|
7
9
|
SafetyClass,
|
|
8
10
|
SecretSlot,
|
|
9
11
|
Site,
|
|
10
12
|
ToolSpec,
|
|
13
|
+
ToolStatus,
|
|
11
14
|
UXRecipe,
|
|
12
15
|
} from "./registry";
|
|
13
|
-
import type { Observation, StepKind } from "./runtime";
|
|
16
|
+
import type { CaptureBatch, Contributor, Observation, StepKind } from "./runtime";
|
|
14
17
|
|
|
15
18
|
/** A planner decision (mirror of the ShapeShyft plan-next-step output). */
|
|
16
19
|
export interface PlannerStep {
|
|
@@ -66,3 +69,77 @@ export interface StatsResponse {
|
|
|
66
69
|
trustedToolCount: number;
|
|
67
70
|
contributorCount: number;
|
|
68
71
|
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Dashboard DTOs (per-user + registry inspection). Spec 2026-07-20 §3.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/** GET /api/v1/me — the signed-in contributor + aggregate counts. */
|
|
78
|
+
export interface MeResponse {
|
|
79
|
+
contributor: Contributor;
|
|
80
|
+
batchCount: number;
|
|
81
|
+
observationCount: number;
|
|
82
|
+
/** Tools this user has corroborated (cast a recipe vote on). */
|
|
83
|
+
toolsContributed: number;
|
|
84
|
+
/** Of those, how many are currently `trusted`. */
|
|
85
|
+
trustedToolsContributed: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One row of GET /api/v1/me/capture-batches. */
|
|
89
|
+
export interface MyCaptureBatchSummary extends CaptureBatch {
|
|
90
|
+
siteOrigin: string;
|
|
91
|
+
siteName: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** GET /api/v1/me/capture-batches/:id — a batch plus its observations. */
|
|
95
|
+
export interface MyCaptureBatchDetail {
|
|
96
|
+
batch: MyCaptureBatchSummary;
|
|
97
|
+
observations: Observation[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One row of GET /api/v1/me/tools — a tool this user corroborated. */
|
|
101
|
+
export interface MyToolEntry {
|
|
102
|
+
toolId: string;
|
|
103
|
+
siteId: string;
|
|
104
|
+
name: string;
|
|
105
|
+
status: ToolStatus;
|
|
106
|
+
safetyClass: SafetyClass;
|
|
107
|
+
version: number;
|
|
108
|
+
corroboratingUserCount: number;
|
|
109
|
+
/** Hash of MY independently-derived recipe. */
|
|
110
|
+
myRecipeHash: string;
|
|
111
|
+
/** Hash of the recipe currently stored on the tool. */
|
|
112
|
+
currentRecipeHash: string;
|
|
113
|
+
/** Whether my vote agrees with the stored recipe. */
|
|
114
|
+
agreesWithCurrent: boolean;
|
|
115
|
+
votedAt: IsoTimestamp;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** GET /api/v1/tools/:id — full tool detail for the registry inspector. */
|
|
119
|
+
export interface ToolDetailResponse {
|
|
120
|
+
/** The tools table row (recipe, schema, safety, status, counts). */
|
|
121
|
+
tool: {
|
|
122
|
+
id: string;
|
|
123
|
+
siteId: string;
|
|
124
|
+
name: string;
|
|
125
|
+
description: string;
|
|
126
|
+
capabilityLabel: string;
|
|
127
|
+
inputSchema: JSONSchema;
|
|
128
|
+
safetyClass: SafetyClass;
|
|
129
|
+
directCallAvailable: boolean;
|
|
130
|
+
recipe: InvocationRecipe;
|
|
131
|
+
status: ToolStatus;
|
|
132
|
+
version: number;
|
|
133
|
+
observationCount: number;
|
|
134
|
+
corroboratingUserCount: number;
|
|
135
|
+
confidence: number;
|
|
136
|
+
createdAt: IsoTimestamp;
|
|
137
|
+
updatedAt: IsoTimestamp;
|
|
138
|
+
};
|
|
139
|
+
/** Anonymized corroboration breakdown: recipe hash → distinct-user count. */
|
|
140
|
+
corroboration: {
|
|
141
|
+
distinctUsers: number;
|
|
142
|
+
hashCounts: Record<string, number>;
|
|
143
|
+
trustThreshold: number;
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Recipe canonicalization + corroboration hashing. Pure domain logic shared by
|
|
2
|
+
// the backend (trust engine: distinct-recipe-hash quorum) and the frontend. A
|
|
3
|
+
// stable sorted-key stringify makes two users' equivalent recipes hash equal.
|
|
4
|
+
|
|
5
|
+
import type { InvocationRecipe } from "./registry";
|
|
6
|
+
|
|
7
|
+
/** Stable stringify (sorted keys) so two users' equivalent recipes hash equal. */
|
|
8
|
+
export function canonicalizeRecipe(recipe: InvocationRecipe): string {
|
|
9
|
+
return stableStringify(recipe);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hashRecipe(recipe: InvocationRecipe): string {
|
|
13
|
+
const s = canonicalizeRecipe(recipe);
|
|
14
|
+
let h = 0;
|
|
15
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
16
|
+
return (h >>> 0).toString(16);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stableStringify(v: unknown): string {
|
|
20
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
|
|
21
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
|
|
22
|
+
const obj = v as Record<string, unknown>;
|
|
23
|
+
const keys = Object.keys(obj).sort();
|
|
24
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Deterministic clustering & path templating. Pure domain logic shared by the
|
|
2
|
+
// backend (distillation) and the frontend security lib — groups raw observations
|
|
3
|
+
// into endpoint templates so the brain infers semantics once per endpoint, not
|
|
4
|
+
// once per request. No runtime dependencies.
|
|
5
|
+
|
|
6
|
+
import type { Observation } from "./runtime";
|
|
7
|
+
|
|
8
|
+
const UUID_RE =
|
|
9
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const LONGHEX_RE = /^[0-9a-f]{16,}$/i;
|
|
11
|
+
|
|
12
|
+
/** Replace id-like path segments with `{id}`: /sections/120/seats → /sections/{id}/seats */
|
|
13
|
+
export function templatePath(rawUrl: string): string {
|
|
14
|
+
let pathname = rawUrl;
|
|
15
|
+
try {
|
|
16
|
+
pathname = new URL(rawUrl, "http://sider.local").pathname;
|
|
17
|
+
} catch {
|
|
18
|
+
/* keep raw */
|
|
19
|
+
}
|
|
20
|
+
return pathname
|
|
21
|
+
.split("/")
|
|
22
|
+
.map((seg) => {
|
|
23
|
+
if (seg === "") return seg;
|
|
24
|
+
if (/^\d+$/.test(seg)) return "{id}";
|
|
25
|
+
if (UUID_RE.test(seg)) return "{id}";
|
|
26
|
+
if (LONGHEX_RE.test(seg)) return "{id}";
|
|
27
|
+
return seg;
|
|
28
|
+
})
|
|
29
|
+
.join("/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GraphQL keying: one URL serves many operations, so cluster on operation name,
|
|
34
|
+
* not path. Returns undefined for non-GraphQL requests.
|
|
35
|
+
*/
|
|
36
|
+
export function graphqlOperationOf(url: string, body: unknown): string | undefined {
|
|
37
|
+
if (body && typeof body === "object") {
|
|
38
|
+
const b = body as Record<string, unknown>;
|
|
39
|
+
if (typeof b["operationName"] === "string" && b["operationName"]) {
|
|
40
|
+
return b["operationName"];
|
|
41
|
+
}
|
|
42
|
+
if (typeof b["query"] === "string") {
|
|
43
|
+
const m = /\b(query|mutation|subscription)\s+([A-Za-z0-9_]+)/.exec(b["query"]);
|
|
44
|
+
if (m && m[2]) return m[2];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (/\/graphql\b/i.test(url)) return "anonymous";
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Clusterable = Pick<Observation, "method" | "url" | "requestBody">;
|
|
52
|
+
|
|
53
|
+
/** Stable key identifying the endpoint template an observation belongs to. */
|
|
54
|
+
export function endpointKey(o: Clusterable): string {
|
|
55
|
+
const op = graphqlOperationOf(o.url, o.requestBody);
|
|
56
|
+
return `${o.method} ${templatePath(o.url)}${op ? ` #${op}` : ""}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function clusterObservations<T extends Clusterable>(
|
|
60
|
+
observations: T[],
|
|
61
|
+
): Map<string, T[]> {
|
|
62
|
+
const groups = new Map<string, T[]>();
|
|
63
|
+
for (const o of observations) {
|
|
64
|
+
const key = endpointKey(o);
|
|
65
|
+
const g = groups.get(key);
|
|
66
|
+
if (g) g.push(o);
|
|
67
|
+
else groups.set(key, [o]);
|
|
68
|
+
}
|
|
69
|
+
return groups;
|
|
70
|
+
}
|