@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.
Files changed (91) hide show
  1. package/dist/decision-gate/index.d.ts +3 -0
  2. package/dist/decision-gate/index.d.ts.map +1 -0
  3. package/dist/decision-gate/index.js +19 -0
  4. package/dist/decision-gate/index.js.map +1 -0
  5. package/dist/decision-gate/lib/decision-gate.d.ts +55 -0
  6. package/dist/decision-gate/lib/decision-gate.d.ts.map +1 -0
  7. package/dist/decision-gate/lib/decision-gate.js +56 -0
  8. package/dist/decision-gate/lib/decision-gate.js.map +1 -0
  9. package/dist/decision-gate/lib/pinned-subject-artifact.d.ts +9 -0
  10. package/dist/decision-gate/lib/pinned-subject-artifact.d.ts.map +1 -0
  11. package/dist/decision-gate/lib/pinned-subject-artifact.js +11 -0
  12. package/dist/decision-gate/lib/pinned-subject-artifact.js.map +1 -0
  13. package/dist/question/index.d.ts +5 -0
  14. package/dist/question/index.d.ts.map +1 -0
  15. package/dist/question/index.js +21 -0
  16. package/dist/question/index.js.map +1 -0
  17. package/dist/question/lib/question-action-context.d.ts +7 -0
  18. package/dist/question/lib/question-action-context.d.ts.map +1 -0
  19. package/dist/question/lib/question-action-context.js +3 -0
  20. package/dist/question/lib/question-action-context.js.map +1 -0
  21. package/dist/question/lib/question-dto.d.ts +54 -0
  22. package/dist/question/lib/question-dto.d.ts.map +1 -0
  23. package/dist/question/lib/question-dto.js +3 -0
  24. package/dist/question/lib/question-dto.js.map +1 -0
  25. package/dist/question/lib/question-enums.d.ts +20 -0
  26. package/dist/question/lib/question-enums.d.ts.map +1 -0
  27. package/dist/question/lib/question-enums.js +32 -0
  28. package/dist/question/lib/question-enums.js.map +1 -0
  29. package/dist/question/lib/question-escalation.d.ts +8 -0
  30. package/dist/question/lib/question-escalation.d.ts.map +1 -0
  31. package/dist/question/lib/question-escalation.js +3 -0
  32. package/dist/question/lib/question-escalation.js.map +1 -0
  33. package/dist/review-inquiry/index.d.ts +3 -0
  34. package/dist/review-inquiry/index.d.ts.map +1 -0
  35. package/dist/review-inquiry/index.js +19 -0
  36. package/dist/review-inquiry/index.js.map +1 -0
  37. package/dist/review-inquiry/lib/review-inquiry.d.ts +138 -0
  38. package/dist/review-inquiry/lib/review-inquiry.d.ts.map +1 -0
  39. package/dist/review-inquiry/lib/review-inquiry.js +20 -0
  40. package/dist/review-inquiry/lib/review-inquiry.js.map +1 -0
  41. package/dist/review-inquiry/lib/web-annotation.d.ts +195 -0
  42. package/dist/review-inquiry/lib/web-annotation.d.ts.map +1 -0
  43. package/dist/review-inquiry/lib/web-annotation.js +59 -0
  44. package/dist/review-inquiry/lib/web-annotation.js.map +1 -0
  45. package/dist/run-detail/index.d.ts +7 -0
  46. package/dist/run-detail/index.d.ts.map +1 -0
  47. package/dist/run-detail/index.js +23 -0
  48. package/dist/run-detail/index.js.map +1 -0
  49. package/dist/run-detail/lib/run-detail-dto.d.ts +34 -0
  50. package/dist/run-detail/lib/run-detail-dto.d.ts.map +1 -0
  51. package/dist/run-detail/lib/run-detail-dto.js +3 -0
  52. package/dist/run-detail/lib/run-detail-dto.js.map +1 -0
  53. package/dist/run-detail/lib/run-mode.d.ts +14 -0
  54. package/dist/run-detail/lib/run-mode.d.ts.map +1 -0
  55. package/dist/run-detail/lib/run-mode.js +25 -0
  56. package/dist/run-detail/lib/run-mode.js.map +1 -0
  57. package/dist/run-detail/lib/run-outcome.d.ts +28 -0
  58. package/dist/run-detail/lib/run-outcome.d.ts.map +1 -0
  59. package/dist/run-detail/lib/run-outcome.js +27 -0
  60. package/dist/run-detail/lib/run-outcome.js.map +1 -0
  61. package/dist/run-detail/lib/run-phase-rail.d.ts +21 -0
  62. package/dist/run-detail/lib/run-phase-rail.d.ts.map +1 -0
  63. package/dist/run-detail/lib/run-phase-rail.js +14 -0
  64. package/dist/run-detail/lib/run-phase-rail.js.map +1 -0
  65. package/dist/run-detail/lib/run-revision-state.d.ts +47 -0
  66. package/dist/run-detail/lib/run-revision-state.d.ts.map +1 -0
  67. package/dist/run-detail/lib/run-revision-state.js +11 -0
  68. package/dist/run-detail/lib/run-revision-state.js.map +1 -0
  69. package/dist/run-detail/lib/wait-windows.d.ts +15 -0
  70. package/dist/run-detail/lib/wait-windows.d.ts.map +1 -0
  71. package/dist/run-detail/lib/wait-windows.js +39 -0
  72. package/dist/run-detail/lib/wait-windows.js.map +1 -0
  73. package/package.json +1 -1
  74. package/src/decision-gate/index.ts +13 -0
  75. package/src/decision-gate/lib/decision-gate.ts +113 -0
  76. package/src/decision-gate/lib/pinned-subject-artifact.ts +29 -0
  77. package/src/question/index.ts +24 -0
  78. package/src/question/lib/question-action-context.ts +26 -0
  79. package/src/question/lib/question-dto.ts +143 -0
  80. package/src/question/lib/question-enums.ts +67 -0
  81. package/src/question/lib/question-escalation.ts +22 -0
  82. package/src/review-inquiry/index.ts +19 -0
  83. package/src/review-inquiry/lib/review-inquiry.ts +76 -0
  84. package/src/review-inquiry/lib/web-annotation.ts +136 -0
  85. package/src/run-detail/index.ts +22 -0
  86. package/src/run-detail/lib/run-detail-dto.ts +84 -0
  87. package/src/run-detail/lib/run-mode.ts +65 -0
  88. package/src/run-detail/lib/run-outcome.ts +66 -0
  89. package/src/run-detail/lib/run-phase-rail.ts +43 -0
  90. package/src/run-detail/lib/run-revision-state.ts +83 -0
  91. 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.30.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>;