@xemahq/kernel-contracts 0.30.0 → 0.31.0
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/decision-gate/index.d.ts +3 -0
- package/dist/decision-gate/index.d.ts.map +1 -0
- package/dist/decision-gate/index.js +19 -0
- package/dist/decision-gate/index.js.map +1 -0
- package/dist/decision-gate/lib/decision-gate.d.ts +55 -0
- package/dist/decision-gate/lib/decision-gate.d.ts.map +1 -0
- package/dist/decision-gate/lib/decision-gate.js +56 -0
- package/dist/decision-gate/lib/decision-gate.js.map +1 -0
- package/dist/decision-gate/lib/pinned-subject-artifact.d.ts +9 -0
- package/dist/decision-gate/lib/pinned-subject-artifact.d.ts.map +1 -0
- package/dist/decision-gate/lib/pinned-subject-artifact.js +11 -0
- package/dist/decision-gate/lib/pinned-subject-artifact.js.map +1 -0
- package/dist/question/index.d.ts +5 -0
- package/dist/question/index.d.ts.map +1 -0
- package/dist/question/index.js +21 -0
- package/dist/question/index.js.map +1 -0
- package/dist/question/lib/question-action-context.d.ts +7 -0
- package/dist/question/lib/question-action-context.d.ts.map +1 -0
- package/dist/question/lib/question-action-context.js +3 -0
- package/dist/question/lib/question-action-context.js.map +1 -0
- package/dist/question/lib/question-dto.d.ts +54 -0
- package/dist/question/lib/question-dto.d.ts.map +1 -0
- package/dist/question/lib/question-dto.js +3 -0
- package/dist/question/lib/question-dto.js.map +1 -0
- package/dist/question/lib/question-enums.d.ts +20 -0
- package/dist/question/lib/question-enums.d.ts.map +1 -0
- package/dist/question/lib/question-enums.js +32 -0
- package/dist/question/lib/question-enums.js.map +1 -0
- package/dist/question/lib/question-escalation.d.ts +8 -0
- package/dist/question/lib/question-escalation.d.ts.map +1 -0
- package/dist/question/lib/question-escalation.js +3 -0
- package/dist/question/lib/question-escalation.js.map +1 -0
- package/dist/review-inquiry/index.d.ts +3 -0
- package/dist/review-inquiry/index.d.ts.map +1 -0
- package/dist/review-inquiry/index.js +19 -0
- package/dist/review-inquiry/index.js.map +1 -0
- package/dist/review-inquiry/lib/review-inquiry.d.ts +138 -0
- package/dist/review-inquiry/lib/review-inquiry.d.ts.map +1 -0
- package/dist/review-inquiry/lib/review-inquiry.js +20 -0
- package/dist/review-inquiry/lib/review-inquiry.js.map +1 -0
- package/dist/review-inquiry/lib/web-annotation.d.ts +195 -0
- package/dist/review-inquiry/lib/web-annotation.d.ts.map +1 -0
- package/dist/review-inquiry/lib/web-annotation.js +59 -0
- package/dist/review-inquiry/lib/web-annotation.js.map +1 -0
- package/dist/run-detail/index.d.ts +7 -0
- package/dist/run-detail/index.d.ts.map +1 -0
- package/dist/run-detail/index.js +23 -0
- package/dist/run-detail/index.js.map +1 -0
- package/dist/run-detail/lib/run-detail-dto.d.ts +34 -0
- package/dist/run-detail/lib/run-detail-dto.d.ts.map +1 -0
- package/dist/run-detail/lib/run-detail-dto.js +3 -0
- package/dist/run-detail/lib/run-detail-dto.js.map +1 -0
- package/dist/run-detail/lib/run-mode.d.ts +14 -0
- package/dist/run-detail/lib/run-mode.d.ts.map +1 -0
- package/dist/run-detail/lib/run-mode.js +25 -0
- package/dist/run-detail/lib/run-mode.js.map +1 -0
- package/dist/run-detail/lib/run-outcome.d.ts +28 -0
- package/dist/run-detail/lib/run-outcome.d.ts.map +1 -0
- package/dist/run-detail/lib/run-outcome.js +27 -0
- package/dist/run-detail/lib/run-outcome.js.map +1 -0
- package/dist/run-detail/lib/run-phase-rail.d.ts +21 -0
- package/dist/run-detail/lib/run-phase-rail.d.ts.map +1 -0
- package/dist/run-detail/lib/run-phase-rail.js +14 -0
- package/dist/run-detail/lib/run-phase-rail.js.map +1 -0
- package/dist/run-detail/lib/run-revision-state.d.ts +47 -0
- package/dist/run-detail/lib/run-revision-state.d.ts.map +1 -0
- package/dist/run-detail/lib/run-revision-state.js +11 -0
- package/dist/run-detail/lib/run-revision-state.js.map +1 -0
- package/dist/run-detail/lib/wait-windows.d.ts +15 -0
- package/dist/run-detail/lib/wait-windows.d.ts.map +1 -0
- package/dist/run-detail/lib/wait-windows.js +39 -0
- package/dist/run-detail/lib/wait-windows.js.map +1 -0
- package/package.json +1 -1
- package/src/decision-gate/index.ts +13 -0
- package/src/decision-gate/lib/decision-gate.ts +113 -0
- package/src/decision-gate/lib/pinned-subject-artifact.ts +29 -0
- package/src/question/index.ts +24 -0
- package/src/question/lib/question-action-context.ts +26 -0
- package/src/question/lib/question-dto.ts +143 -0
- package/src/question/lib/question-enums.ts +67 -0
- package/src/question/lib/question-escalation.ts +22 -0
- package/src/review-inquiry/index.ts +19 -0
- package/src/review-inquiry/lib/review-inquiry.ts +76 -0
- package/src/review-inquiry/lib/web-annotation.ts +136 -0
- package/src/run-detail/index.ts +22 -0
- package/src/run-detail/lib/run-detail-dto.ts +84 -0
- package/src/run-detail/lib/run-mode.ts +65 -0
- package/src/run-detail/lib/run-outcome.ts +66 -0
- package/src/run-detail/lib/run-phase-rail.ts +43 -0
- package/src/run-detail/lib/run-revision-state.ts +83 -0
- package/src/run-detail/lib/wait-windows.ts +70 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WaitReason = void 0;
|
|
4
|
+
exports.totalWaitMs = totalWaitMs;
|
|
5
|
+
exports.currentOpenWait = currentOpenWait;
|
|
6
|
+
var WaitReason;
|
|
7
|
+
(function (WaitReason) {
|
|
8
|
+
WaitReason["WAITING_ON_HUMAN"] = "waiting_on_human";
|
|
9
|
+
WaitReason["WAITING_ON_AGENT"] = "waiting_on_agent";
|
|
10
|
+
WaitReason["WAITING_ON_EXTERNAL"] = "waiting_on_external";
|
|
11
|
+
WaitReason["WAITING_ON_DEPENDENCY"] = "waiting_on_dependency";
|
|
12
|
+
WaitReason["WAITING_ON_QUOTA"] = "waiting_on_quota";
|
|
13
|
+
})(WaitReason || (exports.WaitReason = WaitReason = {}));
|
|
14
|
+
function totalWaitMs(windows, nowMs) {
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (const w of windows) {
|
|
17
|
+
const start = Date.parse(w.startedAt);
|
|
18
|
+
const end = w.endedAt !== null ? Date.parse(w.endedAt) : nowMs;
|
|
19
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end > start) {
|
|
20
|
+
total += end - start;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return total;
|
|
24
|
+
}
|
|
25
|
+
function currentOpenWait(windows) {
|
|
26
|
+
let bestStart = -Infinity;
|
|
27
|
+
let bestWindow = null;
|
|
28
|
+
for (const w of windows) {
|
|
29
|
+
if (w.endedAt !== null)
|
|
30
|
+
continue;
|
|
31
|
+
const start = Date.parse(w.startedAt);
|
|
32
|
+
if (Number.isFinite(start) && start > bestStart) {
|
|
33
|
+
bestStart = start;
|
|
34
|
+
bestWindow = w;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return bestWindow;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=wait-windows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait-windows.js","sourceRoot":"","sources":["../../../src/run-detail/lib/wait-windows.ts"],"names":[],"mappings":";;;AAuCA,kCAUC;AAQD,0CAYC;AA3CD,IAAY,UAMX;AAND,WAAY,UAAU;IACpB,mDAAqC,CAAA;IACrC,mDAAqC,CAAA;IACrC,yDAA2C,CAAA;IAC3C,6DAA+C,CAAA;IAC/C,mDAAqC,CAAA;AACvC,CAAC,EANW,UAAU,0BAAV,UAAU,QAMrB;AAOD,SAAgB,WAAW,CAAC,OAA8B,EAAE,KAAa;IACvE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAC/D,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;YAClE,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAQD,SAAgB,eAAe,CAAC,OAA8B;IAC5D,IAAI,SAAS,GAAG,CAAC,QAAQ,CAAC;IAC1B,IAAI,UAAU,GAAsB,IAAI,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,OAAO,KAAK,IAAI;YAAE,SAAS;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YAChD,SAAS,GAAG,KAAK,CAAC;YAClB,UAAU,GAAG,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/kernel-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Consolidated Xema OS kernel wire contracts — pure types + zod schemas for the kernel protocol surfaces. One package, one npm scope, wildcard per-surface subpath exports. No framework/runtime deps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// @xemahq/decision-gate-contracts — Kernel layer
|
|
3
|
+
//
|
|
4
|
+
// Per-kind schemas for the DECISION_GATE InquiryKind: pin one or more
|
|
5
|
+
// artifact versions, ask deciders for a verdict, aggregate per the
|
|
6
|
+
// inquiry's policy, output the approved-version pinned set for downstream
|
|
7
|
+
// consumers (publish-kb etc.) to use unconditionally.
|
|
8
|
+
//
|
|
9
|
+
// Pure types + Zod. Workflow VM imports `import type` only.
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
export * from './lib/pinned-subject-artifact';
|
|
13
|
+
export * from './lib/decision-gate';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { PinnedSubjectArtifactSchema } from './pinned-subject-artifact';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verdict a decider returns on a Decision Gate inquiry.
|
|
7
|
+
*
|
|
8
|
+
* `ABSTAIN` is distinct from `REJECT`: a non-vote that the gate's policy
|
|
9
|
+
* may treat as "skip and wait for the rest" or "implicit reject" depending
|
|
10
|
+
* on the policy's `abstainTreatment`. Humans cannot ABSTAIN today (UI
|
|
11
|
+
* exposes only Approve / Reject); agents and endpoints may, e.g. when
|
|
12
|
+
* their structured output fails validation or the underlying activity
|
|
13
|
+
* exhausts retries.
|
|
14
|
+
*/
|
|
15
|
+
export enum DecisionVerdict {
|
|
16
|
+
APPROVE = 'approve',
|
|
17
|
+
REJECT = 'reject',
|
|
18
|
+
ABSTAIN = 'abstain',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Terminal outcome of the gate as a whole. Distinct from per-recipient
|
|
23
|
+
* verdict because it accounts for timeout + policy aggregation.
|
|
24
|
+
*/
|
|
25
|
+
export enum DecisionGateOutcome {
|
|
26
|
+
APPROVED = 'approved',
|
|
27
|
+
REJECTED = 'rejected',
|
|
28
|
+
EXPIRED_APPROVED = 'expired_approved',
|
|
29
|
+
EXPIRED_REJECTED = 'expired_rejected',
|
|
30
|
+
CANCELLED = 'cancelled',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prompt payload for a DECISION_GATE inquiry. The pinned set is what
|
|
35
|
+
* deciders are deciding on; `description` is human-readable context shown
|
|
36
|
+
* to humans + included in the agent decider's prompt.
|
|
37
|
+
*
|
|
38
|
+
* `passthrough()` keeps unknown keys on the parsed object — used by
|
|
39
|
+
* orchestrators (e.g. `xema/review@v3`) that inject extra context for
|
|
40
|
+
* the FE to render (`reviewIteration`, `reviewerFindings`,
|
|
41
|
+
* `pastIterations`). Decision-gate itself never reads them; the FE
|
|
42
|
+
* gates rendering on field presence and ignores when absent.
|
|
43
|
+
*/
|
|
44
|
+
export const DecisionGatePromptSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
subjectArtifacts: z.array(PinnedSubjectArtifactSchema),
|
|
47
|
+
description: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.passthrough();
|
|
50
|
+
export type DecisionGatePrompt = z.infer<typeof DecisionGatePromptSchema>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reply payload submitted by one decider (human / agent / endpoint). The
|
|
54
|
+
* shape is identical across recipient kinds — what differs is the
|
|
55
|
+
* `submittedBy` attribution and how the payload was produced (UI form,
|
|
56
|
+
* agent structured output, endpoint verdict mapper).
|
|
57
|
+
*
|
|
58
|
+
* `reason` is REQUIRED when the verdict is REJECT and must be a non-empty
|
|
59
|
+
* trimmed string. The contract enforces this at every entry point (engine
|
|
60
|
+
* controller, agent reply, endpoint verdict-mapper) so a reviewer cannot
|
|
61
|
+
* reject a deliverable without articulating why. APPROVE and ABSTAIN
|
|
62
|
+
* leave `reason` optional — the happy path stays frictionless and an
|
|
63
|
+
* ABSTAIN typically means "validation failed / cannot decide" which has
|
|
64
|
+
* no human-authored justification to capture.
|
|
65
|
+
*/
|
|
66
|
+
export const DecisionGateReplySchema = z
|
|
67
|
+
.object({
|
|
68
|
+
verdict: z.nativeEnum(DecisionVerdict),
|
|
69
|
+
reason: z.string().optional(),
|
|
70
|
+
attachment: z.record(z.string(), z.unknown()).optional(),
|
|
71
|
+
})
|
|
72
|
+
.superRefine((value, ctx) => {
|
|
73
|
+
if (value.verdict !== DecisionVerdict.REJECT) return;
|
|
74
|
+
const trimmed = value.reason?.trim() ?? '';
|
|
75
|
+
if (trimmed.length === 0) {
|
|
76
|
+
ctx.addIssue({
|
|
77
|
+
code: 'custom',
|
|
78
|
+
path: ['reason'],
|
|
79
|
+
message: 'reason is required and must be non-empty when verdict is reject',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
export type DecisionGateReply = z.infer<typeof DecisionGateReplySchema>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One row in the decision trail — used for audit and to render "alice
|
|
87
|
+
* approved 12:34:56, bob rejected 12:35:10, oracle-agent abstained" in
|
|
88
|
+
* the FE focused-page detail view.
|
|
89
|
+
*/
|
|
90
|
+
export const DecisionRecordSchema = z.object({
|
|
91
|
+
recipientId: z.string().min(1),
|
|
92
|
+
verdict: z.nativeEnum(DecisionVerdict),
|
|
93
|
+
reason: z.string().nullable(),
|
|
94
|
+
submittedBy: z.string().min(1),
|
|
95
|
+
submittedAtIso: z.string(),
|
|
96
|
+
});
|
|
97
|
+
export type DecisionRecord = z.infer<typeof DecisionRecordSchema>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Aggregated verdict the gate workflow returns to its parent. Downstream
|
|
101
|
+
* consumers (`publish-kb`, KB ingest, audit) read `approvedArtifacts` —
|
|
102
|
+
* NOT `Math.max(version)` — for the bytes to publish.
|
|
103
|
+
*
|
|
104
|
+
* `approvedArtifacts` is non-empty iff `outcome` is APPROVED /
|
|
105
|
+
* EXPIRED_APPROVED. On REJECTED / EXPIRED_REJECTED / CANCELLED, the array
|
|
106
|
+
* is empty by construction (consumers must not run).
|
|
107
|
+
*/
|
|
108
|
+
export const DecisionGateVerdictSchema = z.object({
|
|
109
|
+
outcome: z.nativeEnum(DecisionGateOutcome),
|
|
110
|
+
approvedArtifacts: z.array(PinnedSubjectArtifactSchema),
|
|
111
|
+
decisionTrail: z.array(DecisionRecordSchema),
|
|
112
|
+
});
|
|
113
|
+
export type DecisionGateVerdict = z.infer<typeof DecisionGateVerdictSchema>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pinned reference to a single artifact version. Stored on the inquiry's
|
|
5
|
+
* `prompt.subjectArtifacts` for `kind=DECISION_GATE` and propagated through
|
|
6
|
+
* the decision-gate verdict so downstream consumers (e.g. `publish-kb`,
|
|
7
|
+
* KB ingest, audit) resolve "the bytes the deciders approved" — not
|
|
8
|
+
* "whatever happens to be latest at publish time."
|
|
9
|
+
*
|
|
10
|
+
* The four-tuple `(artifactId, versionId, version, hash)` is the result
|
|
11
|
+
* of `flattenSubjectArtifacts` over upstream `deliverables[]` at the
|
|
12
|
+
* moment the gate workflow registers the inquiry. Once written, it is
|
|
13
|
+
* frozen for the lifetime of the inquiry.
|
|
14
|
+
*
|
|
15
|
+
* Canonical home for this type. Predecessor refactors (artifact-identity
|
|
16
|
+
* PR1-PR3) inlined it inside the human-approval workflow file because the
|
|
17
|
+
* workflow VM cannot run Zod; this package keeps the type importable as
|
|
18
|
+
* `import type` from the workflow file while the engine validates it via
|
|
19
|
+
* Zod at the controller boundary.
|
|
20
|
+
*/
|
|
21
|
+
export const PinnedSubjectArtifactSchema = z.object({
|
|
22
|
+
artifactId: z.string().min(1),
|
|
23
|
+
versionId: z.string().min(1),
|
|
24
|
+
version: z.number().int().positive(),
|
|
25
|
+
hash: z.string().min(1),
|
|
26
|
+
});
|
|
27
|
+
export type PinnedSubjectArtifact = z.infer<
|
|
28
|
+
typeof PinnedSubjectArtifactSchema
|
|
29
|
+
>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// @xemahq/question-contracts — Layer 1 / Kernel
|
|
3
|
+
//
|
|
4
|
+
// User-facing wire shapes for the unified "Question" surface. The platform
|
|
5
|
+
// internally has three overlapping primitives (decision_gate inquiries,
|
|
6
|
+
// agent_tool_inquiry inquiries, human-recipient review jobs); the FE
|
|
7
|
+
// collapses them into ONE concept the user sees: a "Question".
|
|
8
|
+
//
|
|
9
|
+
// Per the "one concept" model: never say
|
|
10
|
+
// "approval", "review", "decision gate", "inquiry", or "task" in the UI.
|
|
11
|
+
// Internal terms stay in code, schemas, logs, telemetry. The reply form
|
|
12
|
+
// renders by `kind`, not by which backend primitive backs the row.
|
|
13
|
+
//
|
|
14
|
+
// Endpoints powered by this package:
|
|
15
|
+
// GET /questions → QuestionDto[] (paginated envelope)
|
|
16
|
+
// GET /questions/badge → { count: number }
|
|
17
|
+
// GET /questions/:id → QuestionDto (single envelope)
|
|
18
|
+
// POST /questions/:id/reply (body shape lives in user-hub-api DTOs)
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
export * from './lib/question-enums';
|
|
22
|
+
export * from './lib/question-dto';
|
|
23
|
+
export * from './lib/question-action-context';
|
|
24
|
+
export * from './lib/question-escalation';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every QuestionDto carries a stable
|
|
3
|
+
* "action context" that the FE's reply form, the bell dropdown's
|
|
4
|
+
* deep-links, and the email channel adapter all read.
|
|
5
|
+
*
|
|
6
|
+
* The contract: `runId` + `taskAnchor` are ALWAYS present. Without them
|
|
7
|
+
* the bell can't deep-link to the run+Work mode and the email link
|
|
8
|
+
* can't restore context — so the engine refuses to materialize a
|
|
9
|
+
* question without both fields. Fail-fast at the boundary, not in the
|
|
10
|
+
* FE.
|
|
11
|
+
*/
|
|
12
|
+
export interface QuestionActionContext {
|
|
13
|
+
/** The workflow run this question belongs to. */
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
/** The workflow slug — used to construct the deep-link path. */
|
|
16
|
+
readonly workflowSlug: string;
|
|
17
|
+
/**
|
|
18
|
+
* A stable anchor for scrolling to the question's canvas section.
|
|
19
|
+
* The FE uses this as the URL fragment so refresh restores scroll
|
|
20
|
+
* position. Format: `q-<inquiryId>` or `q-<inquiryId>-r<recipientId>`
|
|
21
|
+
* for per-recipient framing.
|
|
22
|
+
*/
|
|
23
|
+
readonly taskAnchor: string;
|
|
24
|
+
/** Optional checkpoint key when the question gates a phase boundary. */
|
|
25
|
+
readonly stageKey: string | null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { QuestionActionContext } from './question-action-context';
|
|
2
|
+
import { QuestionEscalationLevel } from './question-escalation';
|
|
3
|
+
import { QuestionKind, QuestionSeverity, QuestionStatus } from './question-enums';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The wire shape powering the global Questions bell, the run-detail
|
|
7
|
+
* Work-mode question canvas, and the question-detail standalone route.
|
|
8
|
+
*
|
|
9
|
+
* One row per (recipient, inquiry) pair. The same inquiry may produce
|
|
10
|
+
* N QuestionDto rows when it has N recipients — each recipient sees
|
|
11
|
+
* THEIR question with their own status. The aggregation policy lives
|
|
12
|
+
* server-side; the FE renders one row at a time.
|
|
13
|
+
*
|
|
14
|
+
* `kind` discriminates which Reply form variant renders. `kindPayload`
|
|
15
|
+
* carries the kind-specific render hints (choice options, default
|
|
16
|
+
* approval reason templates, etc). `subjects` carries the artifacts
|
|
17
|
+
* the question is about so the canvas can render them inline via the
|
|
18
|
+
* existing OutputCard renderer registry — no separate fetch needed
|
|
19
|
+
* beyond the run's Outputs list. `extras` is a closed-set bag for
|
|
20
|
+
* orchestrator-injected hints (review iteration, reviewer findings,
|
|
21
|
+
* past-iteration history) that the canvas renders conditionally.
|
|
22
|
+
*/
|
|
23
|
+
export interface QuestionDto {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly kind: QuestionKind;
|
|
26
|
+
readonly status: QuestionStatus;
|
|
27
|
+
readonly severity: QuestionSeverity;
|
|
28
|
+
|
|
29
|
+
/** The user-facing question — one sentence, never JSON. */
|
|
30
|
+
readonly title: string;
|
|
31
|
+
/** Optional detail line shown under the title. */
|
|
32
|
+
readonly detail: string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Long-form context shown above the subjects in the canvas. Markdown.
|
|
35
|
+
* Distinct from `detail` (one-liner under the title) — `description`
|
|
36
|
+
* is the human-readable framing the engine pulled from the inquiry's
|
|
37
|
+
* prompt (e.g. DECISION_GATE.description, agent_tool_inquiry.context).
|
|
38
|
+
*/
|
|
39
|
+
readonly description: string | null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Engine-side `InquiryRecipient.id` PK for THIS recipient. Required by
|
|
43
|
+
* the FE reply-submit call; populated from the inquiry CloudEvent's
|
|
44
|
+
* `recipients[]` projection during materialization. Empty string for
|
|
45
|
+
* legacy rows that pre-date the wiring.
|
|
46
|
+
*/
|
|
47
|
+
readonly recipientId: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Artifact references the question is asking about (the documents
|
|
51
|
+
* being reviewed, the proposed change, etc.). The FE renders each via
|
|
52
|
+
* the existing OutputCard renderer registry by matching `artifactId`
|
|
53
|
+
* against the run's Outputs list. Empty for questions without subject
|
|
54
|
+
* artifacts (e.g. agent_tool_inquiry asking "what should I do next?").
|
|
55
|
+
*/
|
|
56
|
+
readonly subjects: readonly QuestionSubjectRef[];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Closed-set bag of orchestrator-injected render hints. Today the FE
|
|
60
|
+
* recognises:
|
|
61
|
+
* - `reviewIteration` (number)
|
|
62
|
+
* - `reviewerFindings` (array of {severity, message, location?})
|
|
63
|
+
* - `pastIterations` (array of {iteration, verdict, submittedAtIso})
|
|
64
|
+
* - `reviewSubjects` (array of `ReviewableSubject` entries from
|
|
65
|
+
* `narrowReviewableContent` — post-unification this is the
|
|
66
|
+
* closed set `{ kind: 'artifact-ref', … } | { kind: 'string',
|
|
67
|
+
* value }`. The kernel narrower throws fail-fast on any other
|
|
68
|
+
* shape, so consumers can `switch (subject.kind)` exhaustively.
|
|
69
|
+
* Distinct from `subjects[]` above, which carries the strict
|
|
70
|
+
* approval-time artifact pins consumed by `publish-kb`.)
|
|
71
|
+
* Unknown keys are ignored. The engine surfaces these from
|
|
72
|
+
* `prompt.passthrough()` extras authored by orchestrators like
|
|
73
|
+
* `xema/review@v3`.
|
|
74
|
+
*/
|
|
75
|
+
readonly extras: Readonly<Record<string, unknown>>;
|
|
76
|
+
|
|
77
|
+
readonly action: QuestionActionContext;
|
|
78
|
+
|
|
79
|
+
/** Empty array when no escalation has fired. */
|
|
80
|
+
readonly escalationHistory: readonly QuestionEscalationLevel[];
|
|
81
|
+
|
|
82
|
+
/** ISO-8601 when the question became visible to the recipient. */
|
|
83
|
+
readonly occurredAt: string;
|
|
84
|
+
/** ISO-8601 deadline; `null` for non-timing-bound questions. */
|
|
85
|
+
readonly expiresAt: string | null;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Kind-specific render hints. Variant-typed by `kind`. The FE narrows
|
|
89
|
+
* via discriminated union before reading.
|
|
90
|
+
*/
|
|
91
|
+
readonly kindPayload: QuestionKindPayload;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A pinned reference to an artifact this question is about. Mirrors
|
|
96
|
+
* `PinnedSubjectArtifact` from `@xemahq/decision-gate-contracts` —
|
|
97
|
+
* carried through verbatim from the inquiry prompt.
|
|
98
|
+
*/
|
|
99
|
+
export interface QuestionSubjectRef {
|
|
100
|
+
readonly artifactId: string;
|
|
101
|
+
readonly version: number;
|
|
102
|
+
/** Engine-resolved kind of the underlying artifact (markdown_doc,
|
|
103
|
+
* code_diff, decision_record, …). Optional — absent when the inquiry
|
|
104
|
+
* was created before the engine started pinning the kind. */
|
|
105
|
+
readonly kind?: string;
|
|
106
|
+
/** Engine-resolved title at pin time. Optional for the same reason. */
|
|
107
|
+
readonly title?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Discriminated union over `kind`. */
|
|
111
|
+
export type QuestionKindPayload =
|
|
112
|
+
| { readonly kind: QuestionKind.APPROVAL; readonly approval: ApprovalKindPayload }
|
|
113
|
+
| { readonly kind: QuestionKind.CHOICE; readonly choice: ChoiceKindPayload }
|
|
114
|
+
| { readonly kind: QuestionKind.FREEFORM; readonly freeform: FreeformKindPayload };
|
|
115
|
+
|
|
116
|
+
export interface ApprovalKindPayload {
|
|
117
|
+
/** Whether the reply form should expose a "Revise with note" CTA. */
|
|
118
|
+
readonly allowRevise: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Optional reasons the FE pre-populates in the reject dropdown.
|
|
121
|
+
* Empty array → reason is free-text only.
|
|
122
|
+
*/
|
|
123
|
+
readonly suggestedRejectReasons: readonly string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ChoiceKindPayload {
|
|
127
|
+
readonly options: readonly QuestionChoiceOptionDto[];
|
|
128
|
+
/** True if the recipient can pick more than one option. */
|
|
129
|
+
readonly multiSelect: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface QuestionChoiceOptionDto {
|
|
133
|
+
readonly value: string;
|
|
134
|
+
readonly label: string;
|
|
135
|
+
readonly description: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface FreeformKindPayload {
|
|
139
|
+
/** Hint text rendered as the textarea placeholder. */
|
|
140
|
+
readonly placeholder: string | null;
|
|
141
|
+
/** Optional cap; FE enforces, server re-validates. */
|
|
142
|
+
readonly maxLength: number | null;
|
|
143
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three reply-shape families a Question can take. Drives which
|
|
3
|
+
* Reply form the FE renders below the canvas.
|
|
4
|
+
*
|
|
5
|
+
* - `APPROVAL` — Approve / Reject (+ optional reason). Renders the
|
|
6
|
+
* 3-button row used by checkpoint reviews and decision gates.
|
|
7
|
+
* - `CHOICE` — pick one of N predefined options. Renders a radio list.
|
|
8
|
+
* - `FREEFORM` — open-ended textarea. Renders a single textarea +
|
|
9
|
+
* submit. Used for agent_tool_inquiry "what should I do next?".
|
|
10
|
+
*
|
|
11
|
+
* The reply shape is enforced server-side by the inquiry kind's Zod
|
|
12
|
+
* schema (per `@xemahq/inquiry-contracts` `KindSchemaRegistry`); this
|
|
13
|
+
* enum is the FE-facing projection.
|
|
14
|
+
*/
|
|
15
|
+
export enum QuestionKind {
|
|
16
|
+
APPROVAL = 'approval',
|
|
17
|
+
CHOICE = 'choice',
|
|
18
|
+
FREEFORM = 'freeform',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Lifecycle status of a question from the recipient's perspective.
|
|
23
|
+
*
|
|
24
|
+
* - `PENDING` — recipient must reply; counts toward badge.
|
|
25
|
+
* - `RESPONDED` — this recipient replied but the inquiry as a whole
|
|
26
|
+
* hasn't yet aggregated to a verdict. Stays in the list (to show
|
|
27
|
+
* what was answered) but does NOT count toward badge.
|
|
28
|
+
* - `RESOLVED` — terminal, verdict reached. Hidden from the bell.
|
|
29
|
+
* - `EXPIRED` — terminal via timeout (no verdict reached, escalation
|
|
30
|
+
* chain exhausted).
|
|
31
|
+
* - `CANCELLED` — terminal via run cancel or admin override.
|
|
32
|
+
*
|
|
33
|
+
* Maps server-side from inquiry-recipient status + inquiry status.
|
|
34
|
+
* The FE never derives terminality on its own.
|
|
35
|
+
*/
|
|
36
|
+
export enum QuestionStatus {
|
|
37
|
+
PENDING = 'pending',
|
|
38
|
+
RESPONDED = 'responded',
|
|
39
|
+
RESOLVED = 'resolved',
|
|
40
|
+
EXPIRED = 'expired',
|
|
41
|
+
CANCELLED = 'cancelled',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Statuses that are TERMINAL — no further transitions accepted. */
|
|
45
|
+
export const TERMINAL_QUESTION_STATUSES: readonly QuestionStatus[] = [
|
|
46
|
+
QuestionStatus.RESOLVED,
|
|
47
|
+
QuestionStatus.EXPIRED,
|
|
48
|
+
QuestionStatus.CANCELLED,
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/** Statuses that count toward the badge in the global Questions header button. */
|
|
52
|
+
export const BADGE_COUNTING_QUESTION_STATUSES: readonly QuestionStatus[] = [
|
|
53
|
+
QuestionStatus.PENDING,
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Severity of a question — used to sort the bell + decide whether to
|
|
58
|
+
* pop a toast. The engine maps from inquiry policy + escalation level.
|
|
59
|
+
*
|
|
60
|
+
* `URGENT` toasts immediately; `NORMAL` toasts on first arrival only;
|
|
61
|
+
* `INFO` never toasts (badge-only).
|
|
62
|
+
*/
|
|
63
|
+
export enum QuestionSeverity {
|
|
64
|
+
URGENT = 'urgent',
|
|
65
|
+
NORMAL = 'normal',
|
|
66
|
+
INFO = 'info',
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escalation breadcrumb shown under the question header in Work mode
|
|
3
|
+
* when escalation has fired at least once.
|
|
4
|
+
*
|
|
5
|
+
* Rendered as a level chain, for example:
|
|
6
|
+
* "Level 1 (Alice) · expired · → Level 2 (Bob)"
|
|
7
|
+
*
|
|
8
|
+
* The engine emits the full level history; the FE renders the chain
|
|
9
|
+
* left-to-right with a strikethrough on expired levels. The CURRENT
|
|
10
|
+
* level (where the recipient sits) is the last entry with `expired:
|
|
11
|
+
* false`.
|
|
12
|
+
*/
|
|
13
|
+
export interface QuestionEscalationLevel {
|
|
14
|
+
readonly level: number;
|
|
15
|
+
readonly recipientDisplayName: string;
|
|
16
|
+
/** True when this level timed out and advanced to the next. */
|
|
17
|
+
readonly expired: boolean;
|
|
18
|
+
/** ISO-8601 when the level entered. */
|
|
19
|
+
readonly enteredAt: string;
|
|
20
|
+
/** ISO-8601 when the level expired (only set if `expired === true`). */
|
|
21
|
+
readonly expiredAt: string | null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// @xemahq/review-inquiry-contracts — Kernel layer
|
|
3
|
+
//
|
|
4
|
+
// Abstract types for the REVIEW InquiryKind: a comment-only review surface
|
|
5
|
+
// that pins one or more artifact versions and asks recipients to leave
|
|
6
|
+
// free-form commentary. Distinct from DECISION_GATE — there is no
|
|
7
|
+
// approve/reject verdict and no policy aggregation. Reviewers are heard;
|
|
8
|
+
// the workflow continues regardless of what they say.
|
|
9
|
+
//
|
|
10
|
+
// Compose with DECISION_GATE when both review-and-gate semantics are
|
|
11
|
+
// needed: emit a REVIEW first (collect comments), then a DECISION_GATE
|
|
12
|
+
// over the same `subjectArtifacts` (collect verdicts). The two stay
|
|
13
|
+
// orthogonal at the contract level.
|
|
14
|
+
//
|
|
15
|
+
// Pure types + Zod. Zero runtime adapter knowledge.
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
export * from './lib/review-inquiry';
|
|
19
|
+
export * from './lib/web-annotation';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { PinnedSubjectArtifactSchema } from '../../decision-gate';
|
|
4
|
+
|
|
5
|
+
import { WebAnnotationTargetSchema } from './web-annotation';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prompt payload for a REVIEW inquiry.
|
|
9
|
+
*
|
|
10
|
+
* A REVIEW inquiry is a comment-only review surface: it pins one or more
|
|
11
|
+
* artifact versions and asks recipients to leave free-form comments. It is
|
|
12
|
+
* intentionally distinct from DECISION_GATE — there is NO approve/reject
|
|
13
|
+
* verdict, no policy aggregation, no terminal "approved/rejected" outcome.
|
|
14
|
+
* Reviewers are heard; the workflow continues regardless of what they say.
|
|
15
|
+
*
|
|
16
|
+
* `subjectArtifacts` mirrors the DECISION_GATE prompt and uses the same
|
|
17
|
+
* pinned-reference primitive so downstream consumers (KB ingest, audit)
|
|
18
|
+
* can resolve "what was reviewed at v3" via the same machinery they use
|
|
19
|
+
* for approval gates. `subjectArtifacts` may be empty: a "freeform review
|
|
20
|
+
* thread" on the run as a whole is a legitimate use case.
|
|
21
|
+
*
|
|
22
|
+
* `passthrough()` keeps unknown keys on the parsed object so orchestrators
|
|
23
|
+
* (e.g. `xema/review-thread@v1`) can inject extra render context (past
|
|
24
|
+
* iteration links, reviewer rubrics) that the FE picks up by field
|
|
25
|
+
* presence without contract drift.
|
|
26
|
+
*/
|
|
27
|
+
export const ReviewInquiryPromptSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
subjectArtifacts: z.array(PinnedSubjectArtifactSchema),
|
|
30
|
+
description: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.passthrough();
|
|
33
|
+
export type ReviewInquiryPrompt = z.infer<typeof ReviewInquiryPromptSchema>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One comment submitted as part of a REVIEW reply.
|
|
37
|
+
*
|
|
38
|
+
* - `body` is required and non-empty — a "review" with no content is a
|
|
39
|
+
* no-op and should be suppressed at the UI rather than persisted.
|
|
40
|
+
*
|
|
41
|
+
* - `anchor` is an optional W3C Web Annotation target
|
|
42
|
+
* (https://www.w3.org/TR/annotation-model/) pinning the comment to a
|
|
43
|
+
* specific artifact version and optionally a sub-region of it via a
|
|
44
|
+
* selector (text quote, text position, fragment, range, JSON pointer).
|
|
45
|
+
* We standardise on Web Annotation so downstream tools — including
|
|
46
|
+
* redraft agents and SCM adapters — receive a vocabulary they already
|
|
47
|
+
* know, instead of a Xema-bespoke string format. The engine
|
|
48
|
+
* round-trips the structured value opaquely; only the FE and the
|
|
49
|
+
* redraft agent need to interpret selector kinds.
|
|
50
|
+
*
|
|
51
|
+
* When the reviewer leaves a document-level remark (no specific span
|
|
52
|
+
* highlighted), `anchor` is omitted entirely — the LLM briefing
|
|
53
|
+
* translator groups those as "general notes" without needing a
|
|
54
|
+
* second sentinel field.
|
|
55
|
+
*/
|
|
56
|
+
export const ReviewInquiryCommentSchema = z.object({
|
|
57
|
+
body: z.string().trim().min(1),
|
|
58
|
+
anchor: WebAnnotationTargetSchema.optional(),
|
|
59
|
+
});
|
|
60
|
+
export type ReviewInquiryComment = z.infer<typeof ReviewInquiryCommentSchema>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reply payload for a REVIEW inquiry. At least one comment is required —
|
|
64
|
+
* an empty review is a no-op and should be modelled as "didn't reply"
|
|
65
|
+
* instead.
|
|
66
|
+
*
|
|
67
|
+
* NOTE: no `verdict` field. The whole point of the REVIEW kind is that
|
|
68
|
+
* reviewers leave commentary without being forced into an approve/reject
|
|
69
|
+
* binary. If a workflow needs both ("review then gate"), it composes a
|
|
70
|
+
* REVIEW inquiry followed by a DECISION_GATE that pins the same
|
|
71
|
+
* `subjectArtifacts`.
|
|
72
|
+
*/
|
|
73
|
+
export const ReviewInquiryReplySchema = z.object({
|
|
74
|
+
comments: z.array(ReviewInquiryCommentSchema).min(1),
|
|
75
|
+
});
|
|
76
|
+
export type ReviewInquiryReply = z.infer<typeof ReviewInquiryReplySchema>;
|