@camox/api-contract 0.63.0 → 0.65.1
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/analytics.d.ts +1 -1
- package/dist/drafts.d.ts +50 -2
- package/dist/drafts.js +57 -3
- package/dist/layout-routes.d.ts +20 -0
- package/dist/layout-routes.js +43 -0
- package/dist/patrol.d.ts +116 -8
- package/dist/patrol.js +33 -1
- package/dist/router.d.ts +10000 -9206
- package/dist/threads.d.ts +19 -12
- package/dist/threads.js +21 -8
- package/package.json +8 -2
- package/src/drafts.ts +67 -2
- package/src/layout-routes.ts +51 -0
- package/src/patrol.ts +129 -9
- package/src/threads.ts +32 -4
package/dist/threads.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export declare const RECONCILE_ENTRY = "camox.reconcile";
|
|
|
35
35
|
/**
|
|
36
36
|
* The kind of the transcript entry opening a draft's thread a Live thread handed a request off
|
|
37
37
|
* to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
|
|
38
|
-
* messages.
|
|
38
|
+
* messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
|
|
39
|
+
* too, with their `operationId` as data, and start the agent's run, which acts for Camox.
|
|
39
40
|
*/
|
|
40
41
|
export declare const BRIEF_ENTRY = "camox.brief";
|
|
41
42
|
/**
|
|
@@ -53,8 +54,8 @@ export declare const PUSH_ENTRY = "camox.push";
|
|
|
53
54
|
/** How a pushed head's build ended: built, built with type errors, or built but not previewed. */
|
|
54
55
|
export declare const pushOutcomeSchema: z.ZodEnum<{
|
|
55
56
|
built: "built";
|
|
56
|
-
typeErrors: "typeErrors";
|
|
57
57
|
notPreviewed: "notPreviewed";
|
|
58
|
+
typeErrors: "typeErrors";
|
|
58
59
|
}>;
|
|
59
60
|
export type PushOutcome = z.infer<typeof pushOutcomeSchema>;
|
|
60
61
|
/**
|
|
@@ -64,6 +65,8 @@ export type PushOutcome = z.infer<typeof pushOutcomeSchema>;
|
|
|
64
65
|
export declare const START_DRAFT_TOOL = "startDraft";
|
|
65
66
|
/** The agent's tool building its code changes mid-turn, which the end of a turn also does. */
|
|
66
67
|
export declare const BUILD_TOOL = "build";
|
|
68
|
+
/** The agent's tool rendering the draft in a browser and returning screenshots to the model. */
|
|
69
|
+
export declare const LOOK_TOOL = "look";
|
|
67
70
|
/**
|
|
68
71
|
* The tools whose calls change a draft: the content tools that write, and the code tools that
|
|
69
72
|
* write files. The model calls the code tools, and code mode scripts call both. A test in the API
|
|
@@ -147,8 +150,8 @@ export declare const pushDataSchema: z.ZodObject<{
|
|
|
147
150
|
subjects: z.ZodArray<z.ZodString>;
|
|
148
151
|
outcome: z.ZodEnum<{
|
|
149
152
|
built: "built";
|
|
150
|
-
typeErrors: "typeErrors";
|
|
151
153
|
notPreviewed: "notPreviewed";
|
|
154
|
+
typeErrors: "typeErrors";
|
|
152
155
|
}>;
|
|
153
156
|
}, z.core.$strip>;
|
|
154
157
|
export type PushData = z.infer<typeof pushDataSchema>;
|
|
@@ -176,18 +179,18 @@ export type StartedDraft = z.infer<typeof startedDraftSchema>;
|
|
|
176
179
|
/** How a write touched what it names. */
|
|
177
180
|
export declare const touchKindSchema: z.ZodEnum<{
|
|
178
181
|
created: "created";
|
|
182
|
+
deleted: "deleted";
|
|
179
183
|
edited: "edited";
|
|
180
184
|
moved: "moved";
|
|
181
|
-
deleted: "deleted";
|
|
182
185
|
settings: "settings";
|
|
183
186
|
}>;
|
|
184
187
|
export type TouchKind = z.infer<typeof touchKindSchema>;
|
|
185
188
|
export declare const touchSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
186
189
|
kind: z.ZodEnum<{
|
|
187
190
|
created: "created";
|
|
191
|
+
deleted: "deleted";
|
|
188
192
|
edited: "edited";
|
|
189
193
|
moved: "moved";
|
|
190
|
-
deleted: "deleted";
|
|
191
194
|
settings: "settings";
|
|
192
195
|
}>;
|
|
193
196
|
locale: z.ZodOptional<z.ZodString>;
|
|
@@ -197,9 +200,9 @@ export declare const touchSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
197
200
|
}, z.core.$strip>, z.ZodObject<{
|
|
198
201
|
kind: z.ZodEnum<{
|
|
199
202
|
created: "created";
|
|
203
|
+
deleted: "deleted";
|
|
200
204
|
edited: "edited";
|
|
201
205
|
moved: "moved";
|
|
202
|
-
deleted: "deleted";
|
|
203
206
|
settings: "settings";
|
|
204
207
|
}>;
|
|
205
208
|
locale: z.ZodOptional<z.ZodString>;
|
|
@@ -208,9 +211,9 @@ export declare const touchSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
208
211
|
}, z.core.$strip>, z.ZodObject<{
|
|
209
212
|
kind: z.ZodEnum<{
|
|
210
213
|
created: "created";
|
|
214
|
+
deleted: "deleted";
|
|
211
215
|
edited: "edited";
|
|
212
216
|
moved: "moved";
|
|
213
|
-
deleted: "deleted";
|
|
214
217
|
settings: "settings";
|
|
215
218
|
}>;
|
|
216
219
|
locale: z.ZodOptional<z.ZodString>;
|
|
@@ -228,9 +231,9 @@ export declare const touchedBatchSchema: z.ZodObject<{
|
|
|
228
231
|
touches: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
229
232
|
kind: z.ZodEnum<{
|
|
230
233
|
created: "created";
|
|
234
|
+
deleted: "deleted";
|
|
231
235
|
edited: "edited";
|
|
232
236
|
moved: "moved";
|
|
233
|
-
deleted: "deleted";
|
|
234
237
|
settings: "settings";
|
|
235
238
|
}>;
|
|
236
239
|
locale: z.ZodOptional<z.ZodString>;
|
|
@@ -240,9 +243,9 @@ export declare const touchedBatchSchema: z.ZodObject<{
|
|
|
240
243
|
}, z.core.$strip>, z.ZodObject<{
|
|
241
244
|
kind: z.ZodEnum<{
|
|
242
245
|
created: "created";
|
|
246
|
+
deleted: "deleted";
|
|
243
247
|
edited: "edited";
|
|
244
248
|
moved: "moved";
|
|
245
|
-
deleted: "deleted";
|
|
246
249
|
settings: "settings";
|
|
247
250
|
}>;
|
|
248
251
|
locale: z.ZodOptional<z.ZodString>;
|
|
@@ -251,9 +254,9 @@ export declare const touchedBatchSchema: z.ZodObject<{
|
|
|
251
254
|
}, z.core.$strip>, z.ZodObject<{
|
|
252
255
|
kind: z.ZodEnum<{
|
|
253
256
|
created: "created";
|
|
257
|
+
deleted: "deleted";
|
|
254
258
|
edited: "edited";
|
|
255
259
|
moved: "moved";
|
|
256
|
-
deleted: "deleted";
|
|
257
260
|
settings: "settings";
|
|
258
261
|
}>;
|
|
259
262
|
locale: z.ZodOptional<z.ZodString>;
|
|
@@ -336,10 +339,12 @@ export type ThreadMessage = {
|
|
|
336
339
|
* - `changing`: a call of that round changes the draft (`CHANGING_TOOLS`), as soon as it's written
|
|
337
340
|
* or, in a code mode script, as soon as the script calls one.
|
|
338
341
|
* - `building`: its `build` call runs, or the end of the turn builds its code changes.
|
|
342
|
+
* - `checking`: it looks at what it designed, from writing a `look` call until the model is
|
|
343
|
+
* called again.
|
|
339
344
|
* - `fixing`: the end of the turn's build failed, and the agent fixes it.
|
|
340
345
|
* - `startingDraft`: on Live, its `startDraft` call runs.
|
|
341
346
|
*/
|
|
342
|
-
export type AgentActivity = "thinking" | "exploring" | "changing" | "building" | "fixing" | "startingDraft";
|
|
347
|
+
export type AgentActivity = "thinking" | "exploring" | "changing" | "building" | "checking" | "fixing" | "startingDraft";
|
|
343
348
|
/** Everything the studio shows for a thread. */
|
|
344
349
|
export type ThreadView = {
|
|
345
350
|
messages: readonly ThreadMessage[];
|
|
@@ -369,6 +374,8 @@ export type ThreadState = {
|
|
|
369
374
|
* or making it until the model is called again.
|
|
370
375
|
*/
|
|
371
376
|
changing?: boolean;
|
|
377
|
+
/** Whether the model's latest round calls `look`: from writing the call until it's called again. */
|
|
378
|
+
checking?: boolean;
|
|
372
379
|
/** The drafts the thread's cards name, as the thread last looked them up. */
|
|
373
380
|
drafts?: DraftCardStates;
|
|
374
381
|
};
|
|
@@ -559,7 +566,7 @@ export declare const editorTextOf: (content: Message["content"]) => string;
|
|
|
559
566
|
* run is over, or a later message started another run after its last model call came back
|
|
560
567
|
* unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
|
|
561
568
|
*/
|
|
562
|
-
export declare function threadViewOf({ steps, running, toolCalls, callingTools, changing, drafts }: ThreadState): ThreadView;
|
|
569
|
+
export declare function threadViewOf({ steps, running, toolCalls, callingTools, changing, checking, drafts }: ThreadState): ThreadView;
|
|
563
570
|
/** Folds one of Pi's agent events into the thread's state. */
|
|
564
571
|
export declare function reduceThreadState(state: ThreadState, event: AgentEvent): ThreadState;
|
|
565
572
|
/** Folds a batch of Pi's agent events into the thread's state. */
|
package/dist/threads.js
CHANGED
|
@@ -33,7 +33,8 @@ const RECONCILE_ENTRY = "camox.reconcile";
|
|
|
33
33
|
/**
|
|
34
34
|
* The kind of the transcript entry opening a draft's thread a Live thread handed a request off
|
|
35
35
|
* to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
|
|
36
|
-
* messages.
|
|
36
|
+
* messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
|
|
37
|
+
* too, with their `operationId` as data, and start the agent's run, which acts for Camox.
|
|
37
38
|
*/
|
|
38
39
|
const BRIEF_ENTRY = "camox.brief";
|
|
39
40
|
/**
|
|
@@ -61,6 +62,8 @@ const pushOutcomeSchema = z.enum([
|
|
|
61
62
|
const START_DRAFT_TOOL = "startDraft";
|
|
62
63
|
/** The agent's tool building its code changes mid-turn, which the end of a turn also does. */
|
|
63
64
|
const BUILD_TOOL = "build";
|
|
65
|
+
/** The agent's tool rendering the draft in a browser and returning screenshots to the model. */
|
|
66
|
+
const LOOK_TOOL = "look";
|
|
64
67
|
/**
|
|
65
68
|
* The tools whose calls change a draft: the content tools that write, and the code tools that
|
|
66
69
|
* write files. The model calls the code tools, and code mode scripts call both. A test in the API
|
|
@@ -69,6 +72,7 @@ const BUILD_TOOL = "build";
|
|
|
69
72
|
const CHANGING_TOOLS = /* @__PURE__ */ new Set([
|
|
70
73
|
"addBackToLocale",
|
|
71
74
|
"addLocale",
|
|
75
|
+
"applyFindingFix",
|
|
72
76
|
"createBlock",
|
|
73
77
|
"createCollectionRecord",
|
|
74
78
|
"createPage",
|
|
@@ -369,7 +373,7 @@ function threadStepOf(entry) {
|
|
|
369
373
|
* run is over, or a later message started another run after its last model call came back
|
|
370
374
|
* unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
|
|
371
375
|
*/
|
|
372
|
-
function threadViewOf({ steps, running, toolCalls = [], callingTools = false, changing = false, drafts = {} }) {
|
|
376
|
+
function threadViewOf({ steps, running, toolCalls = [], callingTools = false, changing = false, checking = false, drafts = {} }) {
|
|
373
377
|
const messages = [];
|
|
374
378
|
let waiting;
|
|
375
379
|
let unfinished = false;
|
|
@@ -507,14 +511,16 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ch
|
|
|
507
511
|
status,
|
|
508
512
|
toolCalls,
|
|
509
513
|
callingTools,
|
|
510
|
-
changing
|
|
514
|
+
changing,
|
|
515
|
+
checking
|
|
511
516
|
})
|
|
512
517
|
};
|
|
513
518
|
}
|
|
514
519
|
/** What the agent is doing during a run, from where the turn's build stands and its tool calls. */
|
|
515
|
-
function activityOf({ status, toolCalls, callingTools, changing }) {
|
|
520
|
+
function activityOf({ status, toolCalls, callingTools, changing, checking }) {
|
|
516
521
|
const calling = (name) => toolCalls.some((call) => call.name === name);
|
|
517
522
|
if (status === "building" || calling("build")) return "building";
|
|
523
|
+
if (checking) return "checking";
|
|
518
524
|
if (status === "fixing") return "fixing";
|
|
519
525
|
if (calling("startDraft")) return "startingDraft";
|
|
520
526
|
if (changing) return "changing";
|
|
@@ -525,6 +531,8 @@ function activityOf({ status, toolCalls, callingTools, changing }) {
|
|
|
525
531
|
const changes = (name) => CHANGING_TOOLS.has(name);
|
|
526
532
|
/** Whether a block of the model's message is a call of a tool that changes the draft. */
|
|
527
533
|
const callsChangingTool = (block) => block.type === "toolCall" && changes(block.name);
|
|
534
|
+
/** Whether a block of the model's message is a call of `look`. */
|
|
535
|
+
const callsLook = (block) => block.type === "toolCall" && block.name === "look";
|
|
528
536
|
const append = (state, entry) => {
|
|
529
537
|
const step = entry && threadStepOf(entry);
|
|
530
538
|
if (!step || state.steps.some((known) => known.id === step.id)) return state;
|
|
@@ -548,6 +556,7 @@ function reduceThreadState(state, event) {
|
|
|
548
556
|
running: event.run !== void 0,
|
|
549
557
|
toolCalls,
|
|
550
558
|
callingTools: generated.some((part) => part.type === "toolCall"),
|
|
559
|
+
checking: generated.some(callsLook) || toolCalls.some((call) => call.name === "look"),
|
|
551
560
|
changing: generated.some(callsChangingTool) || toolCalls.some((call) => changes(call.name)) || event.nestedTools.some((nested) => changes(nested.name) && toolCalls.some((call) => nested.parentCallId === call.callId || nested.parentCallId.startsWith(`${call.callId}/`)))
|
|
552
561
|
};
|
|
553
562
|
}
|
|
@@ -560,12 +569,14 @@ function reduceThreadState(state, event) {
|
|
|
560
569
|
running: false,
|
|
561
570
|
toolCalls: [],
|
|
562
571
|
callingTools: false,
|
|
563
|
-
changing: false
|
|
572
|
+
changing: false,
|
|
573
|
+
checking: false
|
|
564
574
|
};
|
|
565
575
|
case "message_start": return event.message.role === "assistant" ? {
|
|
566
576
|
...state,
|
|
567
577
|
callingTools: false,
|
|
568
|
-
changing: false
|
|
578
|
+
changing: false,
|
|
579
|
+
checking: false
|
|
569
580
|
} : state;
|
|
570
581
|
case "message_update": {
|
|
571
582
|
const calls = event.changes.flatMap((change) => change.type === "toolcall_start" || change.type === "block" ? [change.block] : change.type === "message" ? change.message.content : []);
|
|
@@ -573,7 +584,8 @@ function reduceThreadState(state, event) {
|
|
|
573
584
|
return {
|
|
574
585
|
...state,
|
|
575
586
|
callingTools: state.callingTools || calls.some((block) => block.type === "toolCall"),
|
|
576
|
-
changing: state.changing || calls.some(callsChangingTool)
|
|
587
|
+
changing: state.changing || calls.some(callsChangingTool),
|
|
588
|
+
checking: state.checking || calls.some(callsLook)
|
|
577
589
|
};
|
|
578
590
|
}
|
|
579
591
|
case "tool_execution_start": {
|
|
@@ -585,6 +597,7 @@ function reduceThreadState(state, event) {
|
|
|
585
597
|
return {
|
|
586
598
|
...state,
|
|
587
599
|
changing,
|
|
600
|
+
checking: state.checking || event.toolName === "look",
|
|
588
601
|
toolCalls: [...state.toolCalls ?? [], {
|
|
589
602
|
callId: event.toolCallId,
|
|
590
603
|
name: event.toolName
|
|
@@ -615,4 +628,4 @@ function reduceThreadFrame(state, frame) {
|
|
|
615
628
|
}
|
|
616
629
|
|
|
617
630
|
//#endregion
|
|
618
|
-
export { BRIEF_ENTRY, BUILD_STATUS_ENTRY, BUILD_TOOL, CHANGING_TOOLS, CONNECTED_AGENT_MESSAGE_ENTRY, DRAFT_ENDED_ENTRY, EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, PUSH_ENTRY, RECONCILE_ENTRY, RUN_STOPPED_ENTRY, START_DRAFT_TOOL, THREAD_ENDED_CLOSE_CODE, buildStatusDataSchema, buildStatusSchema, connectedAgentClientSchema, connectedAgentMessageDataSchema, editorMessageDataSchema, editorSelectionSchema, editorTextOf, editorWhereSchema, isSender, pushDataSchema, pushOutcomeSchema, reconcileDataSchema, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftCardSchema, startedDraftSchema, textOf, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
|
|
631
|
+
export { BRIEF_ENTRY, BUILD_STATUS_ENTRY, BUILD_TOOL, CHANGING_TOOLS, CONNECTED_AGENT_MESSAGE_ENTRY, DRAFT_ENDED_ENTRY, EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, LOOK_TOOL, PUSH_ENTRY, RECONCILE_ENTRY, RUN_STOPPED_ENTRY, START_DRAFT_TOOL, THREAD_ENDED_CLOSE_CODE, buildStatusDataSchema, buildStatusSchema, connectedAgentClientSchema, connectedAgentMessageDataSchema, editorMessageDataSchema, editorSelectionSchema, editorTextOf, editorWhereSchema, isSender, pushDataSchema, pushOutcomeSchema, reconcileDataSchema, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftCardSchema, startedDraftSchema, textOf, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camox/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"./router": {
|
|
52
52
|
"types": "./dist/router.d.ts",
|
|
53
53
|
"default": "./dist/router.js"
|
|
54
|
+
},
|
|
55
|
+
"./layout-routes": {
|
|
56
|
+
"types": "./dist/layout-routes.d.ts",
|
|
57
|
+
"default": "./dist/layout-routes.js"
|
|
54
58
|
}
|
|
55
59
|
},
|
|
56
60
|
"dependencies": {
|
|
@@ -60,7 +64,8 @@
|
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
66
|
"@earendil-works/pi-ai": "1.3.0",
|
|
63
|
-
"@earendil-works/pi-durable": "1.3.0"
|
|
67
|
+
"@earendil-works/pi-durable": "1.3.0",
|
|
68
|
+
"tsrs": "0.20.1-ts7.1.0-beta"
|
|
64
69
|
},
|
|
65
70
|
"peerDependencies": {
|
|
66
71
|
"@earendil-works/pi-ai": "1.3.0",
|
|
@@ -85,6 +90,7 @@
|
|
|
85
90
|
"^production",
|
|
86
91
|
"{workspaceRoot}/apps/api/src/**/*",
|
|
87
92
|
"{workspaceRoot}/packages/ai-tools/src/**/*",
|
|
93
|
+
"{workspaceRoot}/tsconfig.dts.api-contract.json",
|
|
88
94
|
"!{workspaceRoot}/**/*.test.ts"
|
|
89
95
|
]
|
|
90
96
|
}
|
package/src/drafts.ts
CHANGED
|
@@ -25,6 +25,46 @@ export const PUBLISHING_DRAFT_HEADER = "x-camox-publishing-draft";
|
|
|
25
25
|
*/
|
|
26
26
|
export const PUBLISHING_DRAFT_VAR = "CAMOX_PUBLISHING_DRAFT";
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Request header carrying a look token (ADR 0032): a short-lived, read-only credential the API
|
|
30
|
+
* signs for the agent's `look` tool, naming one project and one draft. A request carrying a valid
|
|
31
|
+
* one reads that draft's content, named with `DRAFT_HEADER`, as a member would, without a session.
|
|
32
|
+
* It writes nothing, reads no other draft, and reads none of Live's private data.
|
|
33
|
+
*/
|
|
34
|
+
export const LOOK_TOKEN_HEADER = "x-camox-look-token";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The cookie the browser `look` drives carries the look token in, set for the website's host
|
|
38
|
+
* only, so third-party requests never see it. The website forwards it as `LOOK_TOKEN_HEADER`.
|
|
39
|
+
*/
|
|
40
|
+
export const LOOK_TOKEN_COOKIE = "camox_look";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The website's lightweight document the browser `look` drives loads (the SDK runtime's
|
|
44
|
+
* `RUNTIME_ROUTE_PATHS.look`): one page of the draft `DRAFT_SEARCH_PARAM` names, at the path
|
|
45
|
+
* `LOOK_PATH_PARAM` gives in the locale `LOOK_LOCALE_PARAM` names, its Default Locale without.
|
|
46
|
+
*/
|
|
47
|
+
export const LOOK_DOCUMENT_PATH = "/_camox/look";
|
|
48
|
+
export const LOOK_PATH_PARAM = "path";
|
|
49
|
+
export const LOOK_LOCALE_PARAM = "locale";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The look document's Block Definition mode: instead of a page, the block of the Block Definition
|
|
53
|
+
* `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
|
|
54
|
+
* Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
|
|
55
|
+
* the shape the content tools take.
|
|
56
|
+
*/
|
|
57
|
+
export const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
|
|
58
|
+
export const LOOK_CONTENT_PARAM = "content";
|
|
59
|
+
export const LOOK_SETTINGS_PARAM = "settings";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The id of a block shown without being placed, as the look document's Block Definition mode
|
|
63
|
+
* shows it: never a stored block's, which are UUIDs. Its element is
|
|
64
|
+
* `[data-camox-viewport-block="unplaced"]`.
|
|
65
|
+
*/
|
|
66
|
+
export const UNPLACED_BLOCK_ID = "unplaced";
|
|
67
|
+
|
|
28
68
|
/**
|
|
29
69
|
* Request header asking the API to create the draft named by `DRAFT_HEADER` if it doesn't
|
|
30
70
|
* exist yet. Its value is `"1"`.
|
|
@@ -87,19 +127,34 @@ export const PUBLISH_BLOCKED_ERROR = "PUBLISH_BLOCKED";
|
|
|
87
127
|
|
|
88
128
|
/**
|
|
89
129
|
* oRPC error code for a publish refused because the draft still has pending translations: every
|
|
90
|
-
* locale goes live together,
|
|
130
|
+
* locale goes live together, and there is no publishing anyway (ADR 0033).
|
|
91
131
|
*/
|
|
92
132
|
export const TRANSLATIONS_PENDING_ERROR = "TRANSLATIONS_PENDING";
|
|
93
133
|
|
|
134
|
+
/**
|
|
135
|
+
* oRPC error code for a publish refused because the draft has open findings, each to fix or for a
|
|
136
|
+
* member to dismiss: there is no publishing past them (ADR 0033).
|
|
137
|
+
*/
|
|
138
|
+
export const FINDINGS_OPEN_ERROR = "FINDINGS_OPEN";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* oRPC error code for a publish refused because a patroller's check at publish hasn't read the
|
|
142
|
+
* draft's latest changes yet: publishing waits for it (ADR 0033).
|
|
143
|
+
*/
|
|
144
|
+
export const CHECKS_RUNNING_ERROR = "CHECKS_RUNNING";
|
|
145
|
+
|
|
94
146
|
/**
|
|
95
147
|
* Why the publish tool published nothing of a draft with changes, and returned a fresh preview to
|
|
96
148
|
* review again, by the error code of each: Live changed since the preview, the draft has blockers,
|
|
97
|
-
* translations are still running,
|
|
149
|
+
* translations are still running, the draft has open findings, a check at publish is still
|
|
150
|
+
* running, or the draft's code changed since the preview.
|
|
98
151
|
*/
|
|
99
152
|
export const NOT_PUBLISHED_CODES = {
|
|
100
153
|
conflictsChanged: CONFLICTS_CHANGED_ERROR,
|
|
101
154
|
blocked: PUBLISH_BLOCKED_ERROR,
|
|
102
155
|
translating: TRANSLATIONS_PENDING_ERROR,
|
|
156
|
+
findings: FINDINGS_OPEN_ERROR,
|
|
157
|
+
checking: CHECKS_RUNNING_ERROR,
|
|
103
158
|
revisionChanged: REVISION_CHANGED_ERROR,
|
|
104
159
|
} as const;
|
|
105
160
|
|
|
@@ -133,6 +188,7 @@ const BLOCK_WRITES: Record<keyof Router["blocks"], boolean> = {
|
|
|
133
188
|
getUsageCounts: false,
|
|
134
189
|
getPagesUsing: false,
|
|
135
190
|
getSyncedContent: false,
|
|
191
|
+
getUnplaced: false,
|
|
136
192
|
create: true,
|
|
137
193
|
updateContent: true,
|
|
138
194
|
updateSettings: true,
|
|
@@ -208,6 +264,14 @@ const LOCALE_WRITES: Record<keyof Router["locales"], boolean> = {
|
|
|
208
264
|
addBack: true,
|
|
209
265
|
};
|
|
210
266
|
|
|
267
|
+
// Applying a finding's fix edits content as the member would; dismissing one is the project's,
|
|
268
|
+
// never content (ADR 0034).
|
|
269
|
+
const PATROL_WRITES: Record<keyof Router["patrol"], boolean> = {
|
|
270
|
+
get: false,
|
|
271
|
+
applyFix: true,
|
|
272
|
+
dismissFinding: false,
|
|
273
|
+
};
|
|
274
|
+
|
|
211
275
|
// `definitions` is absent: its sync is a release, never a studio content write.
|
|
212
276
|
const CONTENT_WRITES: Partial<Record<keyof Router, Record<string, boolean>>> = {
|
|
213
277
|
blocks: BLOCK_WRITES,
|
|
@@ -215,6 +279,7 @@ const CONTENT_WRITES: Partial<Record<keyof Router, Record<string, boolean>>> = {
|
|
|
215
279
|
collectionDefinitions: COLLECTION_WRITES,
|
|
216
280
|
files: FILE_WRITES,
|
|
217
281
|
pages: PAGE_WRITES,
|
|
282
|
+
patrol: PATROL_WRITES,
|
|
218
283
|
repeatableItems: ITEM_WRITES,
|
|
219
284
|
};
|
|
220
285
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** How a layout's pages come about: authored by editors, derived from data, or one fixed page. */
|
|
2
|
+
export type LayoutKind = "curated" | "derived" | "singleton";
|
|
3
|
+
|
|
4
|
+
/** A layout as route matching reads it: its id, which is its file route, and its kind. */
|
|
5
|
+
export type LayoutRoute = { id: string; kind: LayoutKind };
|
|
6
|
+
|
|
7
|
+
/** The segments of a layout's file route, such as `blog.$slug`: `$` segments are params. */
|
|
8
|
+
export function routeSegments(id: string) {
|
|
9
|
+
return id.split(".");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The singleton or derived layout whose route matches `pathname`, a path in its Locale, with its
|
|
14
|
+
* params, or null. The route with the fewest params wins, so a singleton's fixed route wins over
|
|
15
|
+
* a derived route that would match it too. The SDK renders pages and the API checks them with it.
|
|
16
|
+
*/
|
|
17
|
+
export function matchLayoutRoute<L>(
|
|
18
|
+
layouts: readonly L[],
|
|
19
|
+
pathname: string,
|
|
20
|
+
routeOf: (layout: L) => LayoutRoute,
|
|
21
|
+
): { layout: L; params: Record<string, string> } | null {
|
|
22
|
+
const path = pathname.replace(/^\/|\/$/g, "").split("/");
|
|
23
|
+
const paramCount = (layout: L) =>
|
|
24
|
+
routeSegments(routeOf(layout).id).filter((s) => s.startsWith("$")).length;
|
|
25
|
+
const candidates = layouts
|
|
26
|
+
.filter((layout) => routeOf(layout).kind !== "curated")
|
|
27
|
+
.sort((a, b) => paramCount(a) - paramCount(b));
|
|
28
|
+
for (const layout of candidates) {
|
|
29
|
+
const segments = routeSegments(routeOf(layout).id);
|
|
30
|
+
if (segments.length !== path.length) continue;
|
|
31
|
+
const params: Record<string, string> = {};
|
|
32
|
+
let matches = true;
|
|
33
|
+
for (const [index, segment] of segments.entries()) {
|
|
34
|
+
let value: string;
|
|
35
|
+
try {
|
|
36
|
+
value = decodeURIComponent(path[index]!);
|
|
37
|
+
} catch {
|
|
38
|
+
matches = false;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (segment.startsWith("$") && value && !value.includes("/"))
|
|
42
|
+
params[segment.slice(1)] = value;
|
|
43
|
+
else if (segment !== value) {
|
|
44
|
+
matches = false;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (matches) return { layout, params };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
package/src/patrol.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* Live, and names each report's subject, so the studio needs no entity loaded to show it.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
7
|
+
/** Every patroller's stable id. The registry in the API lists them all. */
|
|
8
|
+
export const PATROLLER_IDS = ["translator", "seoManager", "linkChecker", "proofreader"] as const;
|
|
9
|
+
|
|
10
|
+
/** A patroller's stable id. */
|
|
11
|
+
export type PatrollerId = (typeof PATROLLER_IDS)[number];
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* What a structured report says was done. Each has a failed form, by the report's outcome. A
|
|
@@ -16,7 +19,10 @@ export type ReportVerb =
|
|
|
16
19
|
| "mergedWithLive"
|
|
17
20
|
| "wroteSeo"
|
|
18
21
|
| "describedFile"
|
|
19
|
-
| "publishedCatchUp"
|
|
22
|
+
| "publishedCatchUp"
|
|
23
|
+
| "removedLink"
|
|
24
|
+
| "fixedLink"
|
|
25
|
+
| "fixedMistake";
|
|
20
26
|
|
|
21
27
|
/** What a report can be about. */
|
|
22
28
|
export type ReportEntity =
|
|
@@ -51,8 +57,11 @@ export const reportLocale = (
|
|
|
51
57
|
/** Whether the work was done, or failed. A failure shows in its report only. */
|
|
52
58
|
export type ReportOutcome = "done" | "failed";
|
|
53
59
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Where a patroller is on duty, in drafts or on Live: it works as you go, checks when you
|
|
62
|
+
* publish, or is off there (ADR 0034).
|
|
63
|
+
*/
|
|
64
|
+
export type PatrollerDuty = "asYouGo" | "atPublish" | "off";
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Where the studio shows a report's subject: a page on the Canvas, with the block and repeatable
|
|
@@ -68,7 +77,6 @@ export type ReportView = {
|
|
|
68
77
|
id: string;
|
|
69
78
|
summary: ReportSummary;
|
|
70
79
|
outcome: ReportOutcome;
|
|
71
|
-
attention: AttentionLevel;
|
|
72
80
|
/** When the work last settled, in milliseconds since the epoch. */
|
|
73
81
|
at: number;
|
|
74
82
|
/** Whether the report is Live's, seen from a draft or from Live. */
|
|
@@ -80,6 +88,98 @@ export type ReportView = {
|
|
|
80
88
|
place: ReportPlace | null;
|
|
81
89
|
};
|
|
82
90
|
|
|
91
|
+
/**
|
|
92
|
+
* What a finding is about (ADR 0034), by its kind:
|
|
93
|
+
* - `missingPage`: a link to a page that doesn't exist, the page's name as it was last known.
|
|
94
|
+
* - `brokenLink`: an external link that failed, and how.
|
|
95
|
+
* - `misspelling`, `grammar`: the flagged text, in the sentence it's in.
|
|
96
|
+
* A link's `url` is as content stores it: a page link's is `camox:page:<id>`, and `text` is what
|
|
97
|
+
* the link reads.
|
|
98
|
+
*/
|
|
99
|
+
export type FindingAbout =
|
|
100
|
+
| { kind: "missingPage"; url: string; text: string; page: string | null }
|
|
101
|
+
| { kind: "brokenLink"; url: string; text: string; failure: LinkFailure }
|
|
102
|
+
| { kind: "misspelling" | "grammar"; text: string; sentence: string };
|
|
103
|
+
|
|
104
|
+
/** How a link failed: an HTTP error status, no connection (DNS included), or no answer in time. */
|
|
105
|
+
export type LinkFailure =
|
|
106
|
+
| { kind: "status"; status: number }
|
|
107
|
+
| { kind: "unreachable" }
|
|
108
|
+
| { kind: "timeout" };
|
|
109
|
+
|
|
110
|
+
export type FindingKind = FindingAbout["kind"];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The fix a finding suggests, which applying makes: removing the link, keeping its text, or
|
|
114
|
+
* replacing the flagged text with `replacement` in its sentence, keeping its formatting.
|
|
115
|
+
*/
|
|
116
|
+
export type FindingFix = { kind: "removeLink" } | { kind: "replaceText"; replacement: string };
|
|
117
|
+
|
|
118
|
+
/** A finding, open, in the scope viewed. */
|
|
119
|
+
export type FindingView = {
|
|
120
|
+
/** What applying or dismissing it names. */
|
|
121
|
+
id: number;
|
|
122
|
+
about: FindingAbout;
|
|
123
|
+
/** The fix it suggests, if any. */
|
|
124
|
+
fix: FindingFix | null;
|
|
125
|
+
/** The content it's in, named in its locale. */
|
|
126
|
+
subject: ReportSubject;
|
|
127
|
+
/** The locale of the content it's in, by its tag. */
|
|
128
|
+
locale: string;
|
|
129
|
+
/** The content field it's in, by name. */
|
|
130
|
+
field: string;
|
|
131
|
+
/** When it was first raised, in milliseconds since the epoch. */
|
|
132
|
+
raisedAt: number;
|
|
133
|
+
/** Whether it's Live's. Live's are fixed in the Patrol Draft. */
|
|
134
|
+
live: boolean;
|
|
135
|
+
/** Where its content shows, as for reports. */
|
|
136
|
+
place: ReportPlace | null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** What a finding is about, after where it is: "links to About, a page that no longer exists". */
|
|
140
|
+
function aboutSentence(about: FindingAbout): string {
|
|
141
|
+
switch (about.kind) {
|
|
142
|
+
case "missingPage":
|
|
143
|
+
return about.page === null
|
|
144
|
+
? "links to a page that no longer exists"
|
|
145
|
+
: `links to ${about.page}, a page that no longer exists`;
|
|
146
|
+
case "brokenLink": {
|
|
147
|
+
const { failure } = about;
|
|
148
|
+
const how =
|
|
149
|
+
failure.kind === "status"
|
|
150
|
+
? `answers with an error (${failure.status})`
|
|
151
|
+
: failure.kind === "timeout"
|
|
152
|
+
? "doesn't answer in time"
|
|
153
|
+
: "can't be reached";
|
|
154
|
+
return `links to ${about.url}, which ${how}`;
|
|
155
|
+
}
|
|
156
|
+
case "misspelling":
|
|
157
|
+
return "may be misspelled";
|
|
158
|
+
case "grammar":
|
|
159
|
+
return "may be a grammar mistake";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A finding as one sentence, as the studio, emails, Briefs and refusals word it: "“About us” in
|
|
165
|
+
* Footer links to About, a page that no longer exists".
|
|
166
|
+
*/
|
|
167
|
+
export function findingSentence(finding: Pick<FindingView, "about" | "subject">): string {
|
|
168
|
+
return `“${finding.about.text}” in ${finding.subject.name} ${aboutSentence(finding.about)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A member's dismissal of what a finding was about: no finding like it is raised again. */
|
|
172
|
+
export type DismissalView = {
|
|
173
|
+
id: number;
|
|
174
|
+
about: FindingAbout;
|
|
175
|
+
by: { id: string; name: string };
|
|
176
|
+
/** When it was dismissed, in milliseconds since the epoch. */
|
|
177
|
+
at: number;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** What applying a finding's fix did: applied it, or found its content changed and rechecked. */
|
|
181
|
+
export type ApplyResult = { applied: boolean };
|
|
182
|
+
|
|
83
183
|
/** How many subjects a patroller is on in one locale, a locale's tag. */
|
|
84
184
|
export type LocaleCount = { locale: string; count: number };
|
|
85
185
|
|
|
@@ -96,18 +196,38 @@ export type PatrollerView = {
|
|
|
96
196
|
id: PatrollerId;
|
|
97
197
|
/** Its role, as editors know it: "Translator", "SEO manager". */
|
|
98
198
|
role: string;
|
|
199
|
+
/** Its duty in the scope viewed. Off there, it still has its reports. */
|
|
200
|
+
duty: PatrollerDuty;
|
|
99
201
|
/** Null when it isn't working in the scope. */
|
|
100
202
|
working: PatrollerWorking | null;
|
|
101
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* When it last finished a check in the scope, whether it found anything or not, or null when it
|
|
205
|
+
* never checked there.
|
|
206
|
+
*/
|
|
207
|
+
lastCheckedAt: number | null;
|
|
208
|
+
/**
|
|
209
|
+
* Its latest activity in the scope: its last check, its newest report or when its pending work
|
|
210
|
+
* started, whichever is later. Null when it never worked there.
|
|
211
|
+
*/
|
|
102
212
|
lastActivityAt: number | null;
|
|
103
213
|
/** Its newest reports, newest first. */
|
|
104
214
|
reports: ReportView[];
|
|
105
215
|
/** How many more reports it has than `reports` shows, per locale, the most first. */
|
|
106
216
|
more: { locale: string | null; count: number }[];
|
|
217
|
+
/** Its open findings in the scope viewed, the oldest first. Their count is the row's badge. */
|
|
218
|
+
findings: FindingView[];
|
|
219
|
+
/** The project's dismissals of its findings, the newest first. */
|
|
220
|
+
dismissals: DismissalView[];
|
|
107
221
|
};
|
|
108
222
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Every patroller of the registry, the latest activity first; never-worked ones last. With the
|
|
225
|
+
* project's open Patrol Draft, where Live's findings are fixed, if any.
|
|
226
|
+
*/
|
|
227
|
+
export type PatrolView = {
|
|
228
|
+
patrollers: PatrollerView[];
|
|
229
|
+
patrolDraft: { id: string; name: string | null } | null;
|
|
230
|
+
};
|
|
111
231
|
|
|
112
232
|
/** How many reports the Patrol shows of each patroller; the rest are counted. */
|
|
113
233
|
export const REPORTS_SHOWN = 5;
|