runward 0.14.1 → 0.15.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 (56) hide show
  1. package/README.md +9 -3
  2. package/dist/cli.js +27 -2
  3. package/dist/commands/check.js +72 -21
  4. package/dist/commands/compliance.js +20 -8
  5. package/dist/commands/doctor.js +6 -1
  6. package/dist/commands/init.js +9 -2
  7. package/dist/commands/manifest.js +72 -0
  8. package/dist/commands/rules.js +76 -0
  9. package/dist/commands/update.js +5 -1
  10. package/dist/lib/compliance.js +40 -50
  11. package/dist/lib/conformance.js +61 -15
  12. package/dist/lib/constants.js +1 -1
  13. package/dist/lib/evidence.js +222 -0
  14. package/dist/lib/manifest-sync.js +111 -0
  15. package/dist/lib/paths.js +2 -0
  16. package/dist/lib/regimes.js +32 -0
  17. package/dist/lib/rules.js +44 -0
  18. package/dist/lib/tools.js +1 -1
  19. package/dist/lib/verify-findings.js +24 -0
  20. package/examples/request-triage/code/README.md +74 -0
  21. package/examples/request-triage/code/package.json +19 -0
  22. package/examples/request-triage/code/src/adapters/hardcoded-account-registry.adapter.ts +23 -0
  23. package/examples/request-triage/code/src/adapters/in-memory-routing.adapter.ts +79 -0
  24. package/examples/request-triage/code/src/adapters/in-memory-triage-log.adapter.ts +42 -0
  25. package/examples/request-triage/code/src/adapters/keyword-model.adapter.ts +57 -0
  26. package/examples/request-triage/code/src/core/application/triage-request.usecase.ts +195 -0
  27. package/examples/request-triage/code/src/core/domain/guard.ts +121 -0
  28. package/examples/request-triage/code/src/core/domain/triage.ts +88 -0
  29. package/examples/request-triage/code/src/core/ports/account-registry.port.ts +15 -0
  30. package/examples/request-triage/code/src/core/ports/model-provider.port.ts +26 -0
  31. package/examples/request-triage/code/src/core/ports/routing.port.ts +32 -0
  32. package/examples/request-triage/code/src/core/ports/triage-log.port.ts +33 -0
  33. package/examples/request-triage/code/src/demo.ts +80 -0
  34. package/examples/request-triage/code/test/triage.test.ts +243 -0
  35. package/examples/request-triage/code/tsconfig.json +14 -0
  36. package/examples/request-triage/runward/adr/ADR-0003-port-placement-and-sovereignty.md +44 -0
  37. package/examples/request-triage/runward/architecture.md +4 -3
  38. package/examples/request-triage/runward/decision-matrix.md +1 -1
  39. package/examples/request-triage/runward/execution-topology.md +3 -3
  40. package/examples/request-triage/runward/floor.md +4 -4
  41. package/package.json +20 -8
  42. package/regimes/eu-ai-act@2024-1689.json +31 -0
  43. package/regimes/iso-42001@2023.json +22 -0
  44. package/regimes/nist-ai-rmf@1.0.json +19 -0
  45. package/templates/adapters/README.md +4 -0
  46. package/templates/adapters/gitlab-ci.yml +18 -0
  47. package/templates/mission/architecture.md +2 -0
  48. package/templates/mission/execution-topology.md +2 -0
  49. package/templates/mission/floor.md +2 -0
  50. package/templates/mission/threat-model.md +2 -0
  51. package/templates/rules/frontier-deterministic-boundary.md +1 -0
  52. package/templates/targets/AGENTS.md +1 -1
  53. package/templates/workflows/floor.md +1 -1
  54. package/templates/workflows/verify.md +2 -2
  55. package/examples/request-triage/runward/compliance/eu-ai-act-readiness.md +0 -89
  56. package/examples/request-triage/runward/compliance/oscal-component-definition.json +0 -209
@@ -0,0 +1,195 @@
1
+ // Orchestrator use case: the floor of the request-triage mission
2
+ // (../../runward/floor.md). One short dependent sequence (ADR-0001):
3
+ // validate -> model proposes -> deterministic guard verifies/recomputes ->
4
+ // route. The orchestrator composes; the business rules live in the domain
5
+ // (guard) and at the boundaries (routing invariants).
6
+ //
7
+ // Compliance is fail-closed WITHOUT freezing the process: the validated
8
+ // record is serialized as a pending routing and the call returns
9
+ // "suspended"; resumeTriage() rehydrates it on the human decision. Approve
10
+ // routes with the approval recorded; reject means the routing never happens.
11
+
12
+ import {
13
+ InboundRequestSchema,
14
+ QUEUE_BY_CATEGORY,
15
+ REVIEW_QUEUE,
16
+ type InboundRequest,
17
+ type TriageRecord,
18
+ type TriageResult,
19
+ } from "../domain/triage.js";
20
+ import {
21
+ guardCategory,
22
+ guardFields,
23
+ buildApprovalSummary,
24
+ } from "../domain/guard.js";
25
+ import type { TriageModelPort } from "../ports/model-provider.port.js";
26
+ import type { AccountRegistryPort } from "../ports/account-registry.port.js";
27
+ import type { RoutingPort } from "../ports/routing.port.js";
28
+ import type { TriageLogPort } from "../ports/triage-log.port.js";
29
+
30
+ export interface TriageDeps {
31
+ model: TriageModelPort;
32
+ registry: AccountRegistryPort;
33
+ routing: RoutingPort;
34
+ log: TriageLogPort;
35
+ clock: { nowIso(): string };
36
+ }
37
+
38
+ export class TriageRequestUseCase {
39
+ constructor(private readonly deps: TriageDeps) {}
40
+
41
+ async triage(rawInput: InboundRequest): Promise<TriageResult> {
42
+ const { model, registry, routing, log, clock } = this.deps;
43
+
44
+ // 1. Validation at the boundary: never trust the input.
45
+ const parsed = InboundRequestSchema.safeParse(rawInput);
46
+ if (!parsed.success) {
47
+ throw new Error(
48
+ `Invalid request: ${parsed.error.issues.map((i) => i.message).join("; ")}`,
49
+ );
50
+ }
51
+ const input = parsed.data;
52
+ await log.append(input.requestId, {
53
+ at: clock.nowIso(),
54
+ event: "received",
55
+ detail: `from=${input.senderAddress}`,
56
+ });
57
+
58
+ // 2. The model proposes. Everything it returns is a hypothesis.
59
+ const proposal = await model.propose(input.body);
60
+
61
+ // 3. Deterministic guard (ADR-0002): closed vocabulary, registry
62
+ // resolution, deadline re-parsed from the source text.
63
+ const category = guardCategory(proposal.category);
64
+ const guarded = guardFields(proposal, input.body, registry);
65
+
66
+ const record: TriageRecord = {
67
+ requestId: input.requestId,
68
+ category,
69
+ confidence: proposal.confidence,
70
+ ...(guarded.accountRef ? { accountRef: guarded.accountRef } : {}),
71
+ ...(guarded.deadline ? { deadline: guarded.deadline } : {}),
72
+ };
73
+
74
+ // 4. Abstention is a first-class answer: an unknown category or a low
75
+ // confidence never routes — human review, not a plausible guess.
76
+ if (category === "unknown" || proposal.confidence === "low") {
77
+ const reason = "abstention: category unknown or confidence too low";
78
+ await log.append(input.requestId, {
79
+ at: clock.nowIso(),
80
+ event: "escalated",
81
+ detail: reason,
82
+ });
83
+ return { record, status: "needs_review", targetQueue: REVIEW_QUEUE, reason };
84
+ }
85
+
86
+ // 5. Guard escalation: a proposed value that failed verification never
87
+ // routes (fail-closed) — review is cheaper than misrouting.
88
+ if (guarded.escalations.length > 0) {
89
+ const reason = guarded.escalations.join("; ");
90
+ await log.append(input.requestId, {
91
+ at: clock.nowIso(),
92
+ event: "escalated",
93
+ detail: reason,
94
+ });
95
+ return { record, status: "needs_review", targetQueue: REVIEW_QUEUE, reason };
96
+ }
97
+
98
+ const targetQueue = QUEUE_BY_CATEGORY[category];
99
+
100
+ // 6. Compliance: approval required, always (the mission's attached
101
+ // condition: no silent miss). Suspend, do not block: serialize the
102
+ // validated record and free the process. The summary the approver
103
+ // sees is deterministic, built from the validated fields.
104
+ if (category === "compliance") {
105
+ const summary = buildApprovalSummary(
106
+ input.requestId,
107
+ category,
108
+ targetQueue,
109
+ record.accountRef,
110
+ record.deadline,
111
+ );
112
+ await log.setPending(input.requestId, {
113
+ record,
114
+ targetQueue,
115
+ summary,
116
+ suspendedAt: clock.nowIso(),
117
+ });
118
+ await log.append(input.requestId, {
119
+ at: clock.nowIso(),
120
+ event: "suspended",
121
+ detail: summary,
122
+ });
123
+ return { record, status: "suspended", targetQueue, reason: summary };
124
+ }
125
+
126
+ // 7. Support / sales: route directly through the guarded port.
127
+ const confirmation = await routing.assign(record, targetQueue);
128
+ await log.append(input.requestId, {
129
+ at: clock.nowIso(),
130
+ event: "routed",
131
+ detail: `queue=${confirmation.queue} ticket=${confirmation.ticketRef}`,
132
+ });
133
+ return {
134
+ record,
135
+ status: "routed",
136
+ targetQueue: confirmation.queue,
137
+ ticketRef: confirmation.ticketRef,
138
+ };
139
+ }
140
+
141
+ // Rehydrates a suspended compliance record on the human decision.
142
+ // "approve" routes it with the approval recorded; "reject" ends it — the
143
+ // routing never happens.
144
+ async resumeTriage(
145
+ requestId: string,
146
+ decision: "approve" | "reject",
147
+ decidedBy: string,
148
+ ): Promise<TriageResult> {
149
+ const { routing, log, clock } = this.deps;
150
+
151
+ const pending = await log.getPending(requestId);
152
+ if (!pending) {
153
+ throw new Error(`Request "${requestId}" is not suspended.`);
154
+ }
155
+
156
+ await log.append(requestId, {
157
+ at: clock.nowIso(),
158
+ event: "approval_decision",
159
+ detail: `${decision} by=${decidedBy}`,
160
+ });
161
+
162
+ if (decision === "reject") {
163
+ await log.clearPending(requestId);
164
+ await log.append(requestId, {
165
+ at: clock.nowIso(),
166
+ event: "rejected",
167
+ detail: "routing never executed",
168
+ });
169
+ return {
170
+ record: pending.record,
171
+ status: "rejected",
172
+ targetQueue: pending.targetQueue,
173
+ reason: `approval rejected by ${decidedBy}`,
174
+ };
175
+ }
176
+
177
+ // Approve: route the EXACT serialized record, approval recorded.
178
+ const confirmation = await routing.assign(pending.record, pending.targetQueue, {
179
+ approvedBy: decidedBy,
180
+ at: clock.nowIso(),
181
+ });
182
+ await log.clearPending(requestId);
183
+ await log.append(requestId, {
184
+ at: clock.nowIso(),
185
+ event: "routed",
186
+ detail: `queue=${confirmation.queue} ticket=${confirmation.ticketRef} approved_by=${decidedBy}`,
187
+ });
188
+ return {
189
+ record: pending.record,
190
+ status: "routed",
191
+ targetQueue: confirmation.queue,
192
+ ticketRef: confirmation.ticketRef,
193
+ };
194
+ }
195
+ }
@@ -0,0 +1,121 @@
1
+ // Deterministic guard on model-extracted fields (ADR-0002). Pure functions,
2
+ // unit-testable without a model in the loop.
3
+ //
4
+ // The rule: THE MODEL NEVER SUPPLIES A VALUE THE SYSTEM CAN COMPUTE OR
5
+ // VERIFY ITSELF. Concretely:
6
+ // - an account reference is resolved against the account registry; a
7
+ // reference that does not resolve is never routed on — the record
8
+ // escalates to human review with the reason attached;
9
+ // - a deadline is re-parsed from the source text by a deterministic
10
+ // parser; the model's proposal is DISCARDED in favor of the parsed
11
+ // value, whatever the model said;
12
+ // - the category must belong to the closed vocabulary; anything else is
13
+ // treated as "unknown" (abstention), never as a new category.
14
+
15
+ import {
16
+ CategorySchema,
17
+ type Category,
18
+ type TriageField,
19
+ } from "./triage.js";
20
+ import type { AccountRegistryPort } from "../ports/account-registry.port.js";
21
+ import type { TriageProposal } from "../ports/model-provider.port.js";
22
+
23
+ // ----------------------------------------------------------------------------
24
+ // Deterministic deadline parser. Reads ISO dates (YYYY-MM-DD) from the raw
25
+ // source text and validates them as real calendar dates. Returns the first
26
+ // valid one, normalized, or null. No model involved.
27
+ // ----------------------------------------------------------------------------
28
+ const ISO_DATE = /\b(\d{4})-(\d{2})-(\d{2})\b/g;
29
+
30
+ export function parseDeadline(sourceText: string): string | null {
31
+ for (const m of sourceText.matchAll(ISO_DATE)) {
32
+ const [raw, y, mo, d] = m;
33
+ const year = Number(y);
34
+ const month = Number(mo);
35
+ const day = Number(d);
36
+ // Real calendar date, not just a well-shaped string.
37
+ const date = new Date(Date.UTC(year, month - 1, day));
38
+ if (
39
+ date.getUTCFullYear() === year &&
40
+ date.getUTCMonth() === month - 1 &&
41
+ date.getUTCDate() === day
42
+ ) {
43
+ return raw;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // ----------------------------------------------------------------------------
50
+ // Category guard: closed vocabulary or abstention.
51
+ // ----------------------------------------------------------------------------
52
+ export function guardCategory(proposed: unknown): Category {
53
+ const parsed = CategorySchema.safeParse(proposed);
54
+ return parsed.success ? parsed.data : "unknown";
55
+ }
56
+
57
+ // ----------------------------------------------------------------------------
58
+ // Field guard: promote, recompute, or escalate.
59
+ // ----------------------------------------------------------------------------
60
+ export interface GuardedFields {
61
+ accountRef?: TriageField & { accountName?: string };
62
+ deadline?: TriageField;
63
+ // Non-empty when a proposed value failed verification: the record must
64
+ // escalate to human review instead of routing (fail-closed).
65
+ escalations: string[];
66
+ }
67
+
68
+ export function guardFields(
69
+ proposal: TriageProposal,
70
+ sourceText: string,
71
+ registry: AccountRegistryPort,
72
+ ): GuardedFields {
73
+ const out: GuardedFields = { escalations: [] };
74
+
75
+ // Account reference: verified against the registry or never routed on.
76
+ const proposedRef = proposal.fields.find((f) => f.name === "accountRef");
77
+ if (proposedRef) {
78
+ const account = registry.resolve(proposedRef.value);
79
+ if (account) {
80
+ out.accountRef = {
81
+ value: account.ref,
82
+ provenance: "verified",
83
+ accountName: account.name,
84
+ };
85
+ } else {
86
+ out.escalations.push(
87
+ `unverified account reference "${proposedRef.value}" (not in registry)`,
88
+ );
89
+ }
90
+ }
91
+
92
+ // Deadline: recomputed from the source text; the model's value is
93
+ // discarded even when present, even when it looks right.
94
+ const parsed = parseDeadline(sourceText);
95
+ if (parsed) {
96
+ out.deadline = { value: parsed, provenance: "computed" };
97
+ }
98
+
99
+ return out;
100
+ }
101
+
102
+ // ----------------------------------------------------------------------------
103
+ // Deterministic approval summary for a suspended compliance record: a pure
104
+ // function of the validated fields, never a model reformulation.
105
+ // ----------------------------------------------------------------------------
106
+ export function buildApprovalSummary(
107
+ requestId: string,
108
+ category: Category,
109
+ targetQueue: string,
110
+ accountRef?: TriageField,
111
+ deadline?: TriageField,
112
+ ): string {
113
+ const parts = [
114
+ `Approval required: route request "${requestId}"`,
115
+ `category=${category}`,
116
+ `queue=${targetQueue}`,
117
+ `accountRef=${accountRef ? `${accountRef.value} (${accountRef.provenance})` : "none"}`,
118
+ `deadline=${deadline ? `${deadline.value} (${deadline.provenance})` : "none"}`,
119
+ ];
120
+ return parts.join(" | ");
121
+ }
@@ -0,0 +1,88 @@
1
+ // Pure triage domain. Zero framework, zero I/O; zod is used as a schema
2
+ // library only. Mirrors the TriageRecord v1.0 contract of the mission
3
+ // documents (../../runward/architecture.md §3): closed category vocabulary,
4
+ // provenance-marked fields, target queue, confidence level.
5
+
6
+ import { z } from "zod";
7
+
8
+ // ----------------------------------------------------------------------------
9
+ // Closed category vocabulary. Anything else is a validation failure, not a
10
+ // new category (contracts/model-port.md). "unknown" is a first-class answer:
11
+ // abstention beats a plausible guess.
12
+ // ----------------------------------------------------------------------------
13
+ export const CATEGORIES = ["support", "sales", "compliance", "unknown"] as const;
14
+ export const CategorySchema = z.enum(CATEGORIES);
15
+ export type Category = z.infer<typeof CategorySchema>;
16
+
17
+ export const ConfidenceSchema = z.enum(["high", "medium", "low"]);
18
+ export type Confidence = z.infer<typeof ConfidenceSchema>;
19
+
20
+ // ----------------------------------------------------------------------------
21
+ // Provenance markers (ADR-0002). A model output is a hypothesis, never a
22
+ // fact: it enters the system marked "model-proposed" and nothing acts on it
23
+ // until the deterministic guard has promoted it to "verified" (checked
24
+ // against a source of truth) or replaced it with a "computed" value.
25
+ // ----------------------------------------------------------------------------
26
+ export const ProvenanceSchema = z.enum(["computed", "verified", "model-proposed"]);
27
+ export type Provenance = z.infer<typeof ProvenanceSchema>;
28
+
29
+ export interface TriageField {
30
+ value: string;
31
+ provenance: Provenance;
32
+ }
33
+
34
+ // ----------------------------------------------------------------------------
35
+ // Input entity: one raw inbound request (contracts/request-intake.md,
36
+ // reduced). The body is untrusted free text by definition. strict(): no
37
+ // smuggled keys cross the boundary.
38
+ // ----------------------------------------------------------------------------
39
+ export const InboundRequestSchema = z
40
+ .object({
41
+ requestId: z.string().min(1, "A request identifier cannot be empty."),
42
+ senderAddress: z.string().min(3),
43
+ body: z.string().min(1, "The body cannot be empty.").max(8000),
44
+ })
45
+ .strict();
46
+ export type InboundRequest = z.infer<typeof InboundRequestSchema>;
47
+
48
+ // ----------------------------------------------------------------------------
49
+ // Output entity: the validated triage record. Every field that can drive an
50
+ // action carries a provenance marker; the routing port refuses, fail-closed,
51
+ // any action-bearing field still "model-proposed".
52
+ // ----------------------------------------------------------------------------
53
+ export type TriageStatus =
54
+ | "routed" // assigned to a team queue through the routing port
55
+ | "suspended" // compliance: awaiting human approval (fail-closed, not frozen)
56
+ | "needs_review" // abstention or guard escalation: human review queue
57
+ | "rejected"; // approval denied on resume: never routed
58
+
59
+ export interface TriageRecord {
60
+ requestId: string;
61
+ category: Category;
62
+ confidence: Confidence;
63
+ // Resolved against the account registry, or absent. Never model-trusted.
64
+ accountRef?: TriageField & { accountName?: string };
65
+ // Re-parsed deterministically from the source text, or absent. The model's
66
+ // proposal for this field is always discarded (ADR-0002).
67
+ deadline?: TriageField;
68
+ }
69
+
70
+ export interface TriageResult {
71
+ record: TriageRecord;
72
+ status: TriageStatus;
73
+ targetQueue: string;
74
+ // Present when routed: the ticketing reference returned by the port.
75
+ ticketRef?: string;
76
+ // Present when suspended or escalated: why a human is in the loop.
77
+ reason?: string;
78
+ }
79
+
80
+ // Queue resolution is deterministic policy, never a model output.
81
+ export const QUEUE_BY_CATEGORY: Record<Category, string> = {
82
+ support: "support-queue",
83
+ sales: "sales-queue",
84
+ compliance: "compliance-queue",
85
+ unknown: "review-queue",
86
+ };
87
+
88
+ export const REVIEW_QUEUE = "review-queue";
@@ -0,0 +1,15 @@
1
+ // Secondary port: the account registry, a deterministic source of truth.
2
+ // The guard resolves every model-proposed account reference against it: a
3
+ // reference that does not resolve is never routed on (ADR-0002). The model
4
+ // cannot mint an account into existence.
5
+
6
+ export interface Account {
7
+ ref: string;
8
+ name: string;
9
+ }
10
+
11
+ export interface AccountRegistryPort {
12
+ // Returns the account when the reference exists, null otherwise. Pure
13
+ // lookup: no fuzzy matching, no repair of a fabricated reference.
14
+ resolve(ref: string): Account | null;
15
+ }
@@ -0,0 +1,26 @@
1
+ // Secondary port: the model proposes, it never decides
2
+ // (../../runward/contracts/model-port.md). The output is a hypothesis: a
3
+ // closed-vocabulary category, field extractions each carrying the span of
4
+ // source text they were read from, and a confidence level. Everything is
5
+ // provenance-marked "model-proposed" by construction; nothing downstream may
6
+ // act on it before the deterministic guard (ADR-0002).
7
+
8
+ import type { Category, Confidence } from "../domain/triage.js";
9
+
10
+ export interface ProposedField {
11
+ name: "accountRef" | "deadline";
12
+ value: string;
13
+ // Span of source text the value was read from. A value with no span is
14
+ // rejected at the schema boundary.
15
+ sourceSpan: string;
16
+ }
17
+
18
+ export interface TriageProposal {
19
+ category: Category;
20
+ fields: ProposedField[];
21
+ confidence: Confidence;
22
+ }
23
+
24
+ export interface TriageModelPort {
25
+ propose(requestText: string): Promise<TriageProposal>;
26
+ }
@@ -0,0 +1,32 @@
1
+ // Secondary port: the system's only action on the world
2
+ // (../../runward/contracts/routing-port.md). Assign one validated triage
3
+ // record to a queue. The most guarded boundary: it acts on verified facts
4
+ // only and fails closed.
5
+ //
6
+ // Invariants the adapter must hold:
7
+ // - refuse any record whose action-bearing fields are still
8
+ // "model-proposed" (fail-closed on provenance, ADR-0002);
9
+ // - never assign a compliance-flagged record without a recorded approval;
10
+ // - idempotent on requestId (no duplicate tickets).
11
+
12
+ import type { TriageRecord } from "../domain/triage.js";
13
+
14
+ export interface RoutingApproval {
15
+ approvedBy: string;
16
+ at: string; // ISO timestamp
17
+ }
18
+
19
+ export interface RoutingConfirmation {
20
+ ticketRef: string;
21
+ routedAt: string; // ISO timestamp, UTC
22
+ queue: string;
23
+ approval?: RoutingApproval;
24
+ }
25
+
26
+ export interface RoutingPort {
27
+ assign(
28
+ record: TriageRecord,
29
+ targetQueue: string,
30
+ approval?: RoutingApproval,
31
+ ): Promise<RoutingConfirmation>;
32
+ }
@@ -0,0 +1,33 @@
1
+ // Secondary port: persistence of triage decisions
2
+ // (../../runward/contracts/persistence-port.md, reduced). An append-only
3
+ // journal keyed by request id, plus the serialized suspension point of a
4
+ // compliance record awaiting human approval (suspend-and-rehydrate: the
5
+ // process is freed, the decision rehydrates the run).
6
+
7
+ import type { TriageRecord } from "../domain/triage.js";
8
+
9
+ export interface TriageLogEntry {
10
+ at: string; // ISO timestamp
11
+ event: string;
12
+ detail: string;
13
+ }
14
+
15
+ // Serialized suspension point: the exact validated record and target queue,
16
+ // plus a deterministic summary built by code from the real fields — never a
17
+ // model reformulation.
18
+ export interface PendingRouting {
19
+ record: TriageRecord;
20
+ targetQueue: string;
21
+ summary: string;
22
+ suspendedAt: string;
23
+ }
24
+
25
+ export interface TriageLogPort {
26
+ append(requestId: string, entry: TriageLogEntry): Promise<void>;
27
+ read(requestId: string): Promise<TriageLogEntry[]>;
28
+ // Suspension point management. setPending throws if one already exists
29
+ // (a record is suspended at most once at a time).
30
+ setPending(requestId: string, pending: PendingRouting): Promise<void>;
31
+ getPending(requestId: string): Promise<PendingRouting | null>;
32
+ clearPending(requestId: string): Promise<void>;
33
+ }
@@ -0,0 +1,80 @@
1
+ // End-to-end demo of the triage floor. Four inbound requests: a support
2
+ // issue, a sales inquiry, a compliance request (suspends, then approved),
3
+ // and one the system abstains on. No network, no key: the model adapter is
4
+ // the deterministic keyword classifier.
5
+ //
6
+ // Run: npm run demo
7
+
8
+ import { TriageRequestUseCase } from "./core/application/triage-request.usecase.js";
9
+ import { KeywordModelAdapter } from "./adapters/keyword-model.adapter.js";
10
+ import { HardcodedAccountRegistry } from "./adapters/hardcoded-account-registry.adapter.js";
11
+ import { InMemoryRoutingAdapter } from "./adapters/in-memory-routing.adapter.js";
12
+ import { InMemoryTriageLog } from "./adapters/in-memory-triage-log.adapter.js";
13
+
14
+ async function main(): Promise<void> {
15
+ const log = new InMemoryTriageLog();
16
+ const useCase = new TriageRequestUseCase({
17
+ model: new KeywordModelAdapter(),
18
+ registry: new HardcodedAccountRegistry(),
19
+ routing: new InMemoryRoutingAdapter(),
20
+ log,
21
+ clock: { nowIso: () => new Date().toISOString() },
22
+ });
23
+
24
+ const samples = [
25
+ {
26
+ requestId: "req-001",
27
+ senderAddress: "jane@acme.example",
28
+ body: "Our dashboard crashes with an error on login. Account ACC-1001. Please help.",
29
+ },
30
+ {
31
+ requestId: "req-002",
32
+ senderAddress: "buyer@globex.example",
33
+ body: "Could we get a pricing quote for the enterprise plan? Account ACC-2002.",
34
+ },
35
+ {
36
+ requestId: "req-003",
37
+ senderAddress: "dpo@initech.example",
38
+ body: "GDPR data deletion request for account ACC-3003, response required by 2026-08-01.",
39
+ },
40
+ {
41
+ requestId: "req-004",
42
+ senderAddress: "someone@example.org",
43
+ body: "Following up on the thing we discussed last month.",
44
+ },
45
+ {
46
+ // A plausible-looking account reference the model extracted, that the
47
+ // registry does not know: exactly what an LLM fabricates most fluently.
48
+ // The deterministic guard refuses to route on it (ADR-0002), fail-closed.
49
+ requestId: "req-005",
50
+ senderAddress: "mallory@acme.example",
51
+ body: "Our dashboard crashes with an error on every login. Account ACC-7777, please help urgently.",
52
+ },
53
+ ];
54
+
55
+ for (const sample of samples) {
56
+ const result = await useCase.triage(sample);
57
+ console.log(
58
+ `${sample.requestId}: category=${result.record.category} status=${result.status} queue=${result.targetQueue}` +
59
+ (result.ticketRef ? ` ticket=${result.ticketRef}` : "") +
60
+ (result.reason ? `\n reason: ${result.reason}` : ""),
61
+ );
62
+ }
63
+
64
+ // The compliance request suspended: a human approves, the run rehydrates.
65
+ console.log("\nApproving the suspended compliance request (req-003)...");
66
+ const resumed = await useCase.resumeTriage("req-003", "approve", "ops-coordinator");
67
+ console.log(
68
+ `req-003: status=${resumed.status} queue=${resumed.targetQueue} ticket=${resumed.ticketRef}`,
69
+ );
70
+
71
+ console.log("\nJournal of req-003:");
72
+ for (const entry of await log.read("req-003")) {
73
+ console.log(` [${entry.event}] ${entry.detail}`);
74
+ }
75
+ }
76
+
77
+ main().catch((err) => {
78
+ console.error("Demo failed:", err);
79
+ process.exit(1);
80
+ });