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.
- package/README.md +9 -3
- package/dist/cli.js +27 -2
- package/dist/commands/check.js +72 -21
- package/dist/commands/compliance.js +20 -8
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/init.js +9 -2
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/rules.js +76 -0
- package/dist/commands/update.js +5 -1
- package/dist/lib/compliance.js +40 -50
- package/dist/lib/conformance.js +61 -15
- package/dist/lib/constants.js +1 -1
- package/dist/lib/evidence.js +222 -0
- package/dist/lib/manifest-sync.js +111 -0
- package/dist/lib/paths.js +2 -0
- package/dist/lib/regimes.js +32 -0
- package/dist/lib/rules.js +44 -0
- package/dist/lib/tools.js +1 -1
- package/dist/lib/verify-findings.js +24 -0
- package/examples/request-triage/code/README.md +74 -0
- package/examples/request-triage/code/package.json +19 -0
- package/examples/request-triage/code/src/adapters/hardcoded-account-registry.adapter.ts +23 -0
- package/examples/request-triage/code/src/adapters/in-memory-routing.adapter.ts +79 -0
- package/examples/request-triage/code/src/adapters/in-memory-triage-log.adapter.ts +42 -0
- package/examples/request-triage/code/src/adapters/keyword-model.adapter.ts +57 -0
- package/examples/request-triage/code/src/core/application/triage-request.usecase.ts +195 -0
- package/examples/request-triage/code/src/core/domain/guard.ts +121 -0
- package/examples/request-triage/code/src/core/domain/triage.ts +88 -0
- package/examples/request-triage/code/src/core/ports/account-registry.port.ts +15 -0
- package/examples/request-triage/code/src/core/ports/model-provider.port.ts +26 -0
- package/examples/request-triage/code/src/core/ports/routing.port.ts +32 -0
- package/examples/request-triage/code/src/core/ports/triage-log.port.ts +33 -0
- package/examples/request-triage/code/src/demo.ts +80 -0
- package/examples/request-triage/code/test/triage.test.ts +243 -0
- package/examples/request-triage/code/tsconfig.json +14 -0
- package/examples/request-triage/runward/adr/ADR-0003-port-placement-and-sovereignty.md +44 -0
- package/examples/request-triage/runward/architecture.md +4 -3
- package/examples/request-triage/runward/decision-matrix.md +1 -1
- package/examples/request-triage/runward/execution-topology.md +3 -3
- package/examples/request-triage/runward/floor.md +4 -4
- package/package.json +20 -8
- package/regimes/eu-ai-act@2024-1689.json +31 -0
- package/regimes/iso-42001@2023.json +22 -0
- package/regimes/nist-ai-rmf@1.0.json +19 -0
- package/templates/adapters/README.md +4 -0
- package/templates/adapters/gitlab-ci.yml +18 -0
- package/templates/mission/architecture.md +2 -0
- package/templates/mission/execution-topology.md +2 -0
- package/templates/mission/floor.md +2 -0
- package/templates/mission/threat-model.md +2 -0
- package/templates/rules/frontier-deterministic-boundary.md +1 -0
- package/templates/targets/AGENTS.md +1 -1
- package/templates/workflows/floor.md +1 -1
- package/templates/workflows/verify.md +2 -2
- package/examples/request-triage/runward/compliance/eu-ai-act-readiness.md +0 -89
- package/examples/request-triage/runward/compliance/oscal-component-definition.json +0 -209
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Test floor of the triage example: classification, deterministic guard on
|
|
2
|
+
// extracted fields, routing, fail-closed compliance (suspend-and-rehydrate),
|
|
3
|
+
// abstention on unknown. Everything deterministic: no model key, no network.
|
|
4
|
+
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { TriageRequestUseCase } from "../src/core/application/triage-request.usecase.ts";
|
|
8
|
+
import { KeywordModelAdapter } from "../src/adapters/keyword-model.adapter.ts";
|
|
9
|
+
import { HardcodedAccountRegistry } from "../src/adapters/hardcoded-account-registry.adapter.ts";
|
|
10
|
+
import {
|
|
11
|
+
InMemoryRoutingAdapter,
|
|
12
|
+
ProvenanceRefusalError,
|
|
13
|
+
ApprovalMissingError,
|
|
14
|
+
} from "../src/adapters/in-memory-routing.adapter.ts";
|
|
15
|
+
import { InMemoryTriageLog } from "../src/adapters/in-memory-triage-log.adapter.ts";
|
|
16
|
+
import { parseDeadline } from "../src/core/domain/guard.ts";
|
|
17
|
+
import type { TriageModelPort } from "../src/core/ports/model-provider.port.ts";
|
|
18
|
+
import type { TriageRecord } from "../src/core/domain/triage.ts";
|
|
19
|
+
|
|
20
|
+
const fixedClock = { nowIso: () => "2026-06-01T00:00:00.000Z" };
|
|
21
|
+
|
|
22
|
+
function buildRig(model?: TriageModelPort) {
|
|
23
|
+
const routing = new InMemoryRoutingAdapter();
|
|
24
|
+
const log = new InMemoryTriageLog();
|
|
25
|
+
const useCase = new TriageRequestUseCase({
|
|
26
|
+
model: model ?? new KeywordModelAdapter(),
|
|
27
|
+
registry: new HardcodedAccountRegistry(),
|
|
28
|
+
routing,
|
|
29
|
+
log,
|
|
30
|
+
clock: fixedClock,
|
|
31
|
+
});
|
|
32
|
+
return { useCase, routing, log };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Classification and routing ---------------------------------------------
|
|
36
|
+
|
|
37
|
+
test("support request: classified and routed to the support queue", async () => {
|
|
38
|
+
const { useCase } = buildRig();
|
|
39
|
+
const res = await useCase.triage({
|
|
40
|
+
requestId: "r1",
|
|
41
|
+
senderAddress: "jane@acme.example",
|
|
42
|
+
body: "The export keeps failing with an error, please help. Account ACC-1001.",
|
|
43
|
+
});
|
|
44
|
+
assert.equal(res.record.category, "support");
|
|
45
|
+
assert.equal(res.status, "routed");
|
|
46
|
+
assert.equal(res.targetQueue, "support-queue");
|
|
47
|
+
assert.ok(res.ticketRef);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("sales request: classified and routed to the sales queue", async () => {
|
|
51
|
+
const { useCase } = buildRig();
|
|
52
|
+
const res = await useCase.triage({
|
|
53
|
+
requestId: "r2",
|
|
54
|
+
senderAddress: "buyer@globex.example",
|
|
55
|
+
body: "We would like a pricing quote for 50 seats.",
|
|
56
|
+
});
|
|
57
|
+
assert.equal(res.record.category, "sales");
|
|
58
|
+
assert.equal(res.status, "routed");
|
|
59
|
+
assert.equal(res.targetQueue, "sales-queue");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("compliance beats overlapping signals: a privacy request that also says 'help' is compliance", async () => {
|
|
63
|
+
const { useCase } = buildRig();
|
|
64
|
+
const res = await useCase.triage({
|
|
65
|
+
requestId: "r3",
|
|
66
|
+
senderAddress: "dpo@initech.example",
|
|
67
|
+
body: "Please help us process this GDPR data deletion request.",
|
|
68
|
+
});
|
|
69
|
+
assert.equal(res.record.category, "compliance");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// --- Abstention ---------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
test("unknown category: the system abstains, nothing is routed", async () => {
|
|
75
|
+
const { useCase, routing } = buildRig();
|
|
76
|
+
const res = await useCase.triage({
|
|
77
|
+
requestId: "r4",
|
|
78
|
+
senderAddress: "someone@example.org",
|
|
79
|
+
body: "Following up on the thing from last month.",
|
|
80
|
+
});
|
|
81
|
+
assert.equal(res.record.category, "unknown");
|
|
82
|
+
assert.equal(res.status, "needs_review");
|
|
83
|
+
assert.equal(res.targetQueue, "review-queue");
|
|
84
|
+
assert.equal(routing.assignments, 0); // abstention: no action taken
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// --- Deterministic guard on extracted fields (ADR-0002) -----------------------
|
|
88
|
+
|
|
89
|
+
test("guard: an account reference that resolves in the registry is verified", async () => {
|
|
90
|
+
const { useCase } = buildRig();
|
|
91
|
+
const res = await useCase.triage({
|
|
92
|
+
requestId: "r5",
|
|
93
|
+
senderAddress: "jane@acme.example",
|
|
94
|
+
body: "Login is broken for account ACC-1001.",
|
|
95
|
+
});
|
|
96
|
+
assert.equal(res.record.accountRef?.value, "ACC-1001");
|
|
97
|
+
assert.equal(res.record.accountRef?.provenance, "verified");
|
|
98
|
+
assert.equal(res.record.accountRef?.accountName, "Acme Corp");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("guard: a fabricated account reference never routes — escalated to review", async () => {
|
|
102
|
+
const { useCase, routing } = buildRig();
|
|
103
|
+
const res = await useCase.triage({
|
|
104
|
+
requestId: "r6",
|
|
105
|
+
senderAddress: "jane@acme.example",
|
|
106
|
+
body: "Our app crashes constantly. Account ACC-9999.", // not in the registry
|
|
107
|
+
});
|
|
108
|
+
assert.equal(res.status, "needs_review");
|
|
109
|
+
assert.ok(res.reason?.includes("ACC-9999"));
|
|
110
|
+
assert.equal(routing.assignments, 0); // fail-closed: no routing happened
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("guard: the deadline is re-parsed from the source text; the model's proposal is discarded", async () => {
|
|
114
|
+
// Adversarial model stub: proposes a WRONG deadline (and a valid category).
|
|
115
|
+
const lyingModel: TriageModelPort = {
|
|
116
|
+
async propose() {
|
|
117
|
+
return {
|
|
118
|
+
category: "support",
|
|
119
|
+
confidence: "high",
|
|
120
|
+
fields: [{ name: "deadline", value: "2099-12-31", sourceSpan: "made up" }],
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const { useCase } = buildRig(lyingModel);
|
|
125
|
+
const res = await useCase.triage({
|
|
126
|
+
requestId: "r7",
|
|
127
|
+
senderAddress: "jane@acme.example",
|
|
128
|
+
body: "The report is broken and we need it fixed by 2026-07-15.",
|
|
129
|
+
});
|
|
130
|
+
// The system's value comes from the deterministic parser, not the model.
|
|
131
|
+
assert.equal(res.record.deadline?.value, "2026-07-15");
|
|
132
|
+
assert.equal(res.record.deadline?.provenance, "computed");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("guard: the deadline parser rejects well-shaped non-dates", () => {
|
|
136
|
+
assert.equal(parseDeadline("due 2026-13-45, then 2026-02-30"), null);
|
|
137
|
+
assert.equal(parseDeadline("due 2026-02-28"), "2026-02-28");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// --- Fail-closed compliance: suspend and rehydrate ----------------------------
|
|
141
|
+
|
|
142
|
+
test("compliance: suspended awaiting approval, never routed silently", async () => {
|
|
143
|
+
const { useCase, routing, log } = buildRig();
|
|
144
|
+
const res = await useCase.triage({
|
|
145
|
+
requestId: "r8",
|
|
146
|
+
senderAddress: "dpo@initech.example",
|
|
147
|
+
body: "GDPR erasure request for ACC-3003, respond by 2026-08-01.",
|
|
148
|
+
});
|
|
149
|
+
assert.equal(res.status, "suspended");
|
|
150
|
+
assert.equal(routing.assignments, 0); // fail-closed: nothing routed yet
|
|
151
|
+
// The trajectory is serialized: the exact validated record awaits.
|
|
152
|
+
const pending = await log.getPending("r8");
|
|
153
|
+
assert.equal(pending?.record.accountRef?.value, "ACC-3003");
|
|
154
|
+
// The approval summary is deterministic, built from the validated fields.
|
|
155
|
+
assert.ok(pending?.summary.includes("ACC-3003 (verified)"));
|
|
156
|
+
assert.ok(pending?.summary.includes("2026-08-01 (computed)"));
|
|
157
|
+
assert.equal(res.reason, pending?.summary);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("compliance resume approve: routed to the compliance queue with the approval recorded", async () => {
|
|
161
|
+
const { useCase, log } = buildRig();
|
|
162
|
+
await useCase.triage({
|
|
163
|
+
requestId: "r9",
|
|
164
|
+
senderAddress: "dpo@initech.example",
|
|
165
|
+
body: "GDPR data access request for ACC-3003.",
|
|
166
|
+
});
|
|
167
|
+
const resumed = await useCase.resumeTriage("r9", "approve", "ops-coordinator");
|
|
168
|
+
assert.equal(resumed.status, "routed");
|
|
169
|
+
assert.equal(resumed.targetQueue, "compliance-queue");
|
|
170
|
+
assert.ok(resumed.ticketRef);
|
|
171
|
+
assert.equal(await log.getPending("r9"), null); // suspension point cleared
|
|
172
|
+
const journal = await log.read("r9");
|
|
173
|
+
assert.ok(journal.some((e) => e.event === "approval_decision"));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("compliance resume reject: the routing never happens", async () => {
|
|
177
|
+
const { useCase, routing, log } = buildRig();
|
|
178
|
+
await useCase.triage({
|
|
179
|
+
requestId: "r10",
|
|
180
|
+
senderAddress: "dpo@initech.example",
|
|
181
|
+
body: "Privacy complaint escalated by the regulator, account ACC-2002.",
|
|
182
|
+
});
|
|
183
|
+
const resumed = await useCase.resumeTriage("r10", "reject", "ops-coordinator");
|
|
184
|
+
assert.equal(resumed.status, "rejected");
|
|
185
|
+
assert.equal(routing.assignments, 0); // never executed
|
|
186
|
+
assert.equal(await log.getPending("r10"), null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// --- Routing port invariants ---------------------------------------------------
|
|
190
|
+
|
|
191
|
+
test("routing port refuses, fail-closed, a record with model-proposed action-bearing fields", async () => {
|
|
192
|
+
const routing = new InMemoryRoutingAdapter();
|
|
193
|
+
const record: TriageRecord = {
|
|
194
|
+
requestId: "r11",
|
|
195
|
+
category: "support",
|
|
196
|
+
confidence: "high",
|
|
197
|
+
accountRef: { value: "ACC-1001", provenance: "model-proposed" },
|
|
198
|
+
};
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
() => routing.assign(record, "support-queue"),
|
|
201
|
+
ProvenanceRefusalError,
|
|
202
|
+
);
|
|
203
|
+
// And a compliance record without a recorded approval is refused too.
|
|
204
|
+
const compliance: TriageRecord = {
|
|
205
|
+
requestId: "r12",
|
|
206
|
+
category: "compliance",
|
|
207
|
+
confidence: "high",
|
|
208
|
+
};
|
|
209
|
+
await assert.rejects(
|
|
210
|
+
() => routing.assign(compliance, "compliance-queue"),
|
|
211
|
+
ApprovalMissingError,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("routing port is idempotent on requestId: no duplicate tickets", async () => {
|
|
216
|
+
const routing = new InMemoryRoutingAdapter();
|
|
217
|
+
const record: TriageRecord = {
|
|
218
|
+
requestId: "r13",
|
|
219
|
+
category: "support",
|
|
220
|
+
confidence: "high",
|
|
221
|
+
};
|
|
222
|
+
const first = await routing.assign(record, "support-queue");
|
|
223
|
+
const second = await routing.assign(record, "support-queue");
|
|
224
|
+
assert.equal(first.ticketRef, second.ticketRef);
|
|
225
|
+
assert.equal(routing.assignments, 1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// --- Boundary -------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
test("input boundary: a malformed payload is rejected, unknown keys included", async () => {
|
|
231
|
+
const { useCase } = buildRig();
|
|
232
|
+
await assert.rejects(() =>
|
|
233
|
+
useCase.triage({ requestId: "", senderAddress: "a@b.c", body: "x" }),
|
|
234
|
+
);
|
|
235
|
+
await assert.rejects(() =>
|
|
236
|
+
useCase.triage({
|
|
237
|
+
requestId: "r14",
|
|
238
|
+
senderAddress: "a@b.c",
|
|
239
|
+
body: "hello",
|
|
240
|
+
role: "admin",
|
|
241
|
+
} as never),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"allowImportingTsExtensions": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "test/**/*.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ADR-0003: port placement and sovereignty behind the four ports
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12
|
|
4
|
+
**Status**: accepted
|
|
5
|
+
**Deciders**: operator, Head of Operations (sponsor)
|
|
6
|
+
**Method**: decision-loop: reality-check against the org's existing infrastructure inventory, challenge on sovereignty per data class, durable position
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The architecture note names four ports; two of their adapters run beyond the process. The model adapter calls a vendor-hosted deployment, and the routing adapter writes into the ticketing system the organization already operates. Placement is an adapter decision behind a stable port — the domain does not change with it — but it is a decision, and it carries the mission's most sensitive constraint: inbound request text may contain personal data, so where each adapter runs decides which jurisdiction and which operator can read it. This ADR is the first member of the mission's infra ADR family (placement, sovereignty, trace export); it locks the placement and sovereignty rows recorded in `execution-topology.md`.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
One placement per port, sovereignty graduated by the class of data crossing it — never a wholesale switch.
|
|
15
|
+
|
|
16
|
+
- **ModelPort → managed model-vendor runtime**, bound to the organization's approved deployment. Residency is enforced at the adapter: the deployment is region-pinned, and request text (internal, possibly personal) never leaves the approved region. Sovereignty: **raised**. The port keeps the placement reversible — a self-hosted or gateway-fronted deployment is an adapter swap, not a rework.
|
|
17
|
+
- **RoutingPort → existing infrastructure** (the ticketing system of record), reached only through the anticorruption adapter and the deterministic guard (ADR-0002). Only the validated `TriageRecord` (internal business data) crosses; compliance-flagged records are approval-gated. Sovereignty: **standard**.
|
|
18
|
+
- **RequestIntakePort and PersistencePort → in-app**, the sober default; the intake adapter consumes the existing mailbox, the log is in-process and append-only.
|
|
19
|
+
- **No third-party trace export.** Observability is in-app structured logs (`governance/observability-schema.md`). Traces carry request text — exporting them would be a data transfer requiring its own ADR naming recipient, data class and retention.
|
|
20
|
+
|
|
21
|
+
## Alternatives discarded
|
|
22
|
+
|
|
23
|
+
- **Self-hosted model runtime**: discarded at the floor. Operating a model server costs more than the floor's value proof justifies, and the port makes the move cheap later. Trigger named below.
|
|
24
|
+
- **A dedicated intake queue now**: discarded. No volume signal; in-process handling holds the observed load. Premature distribution would buy eventual consistency and idempotence work with no gain.
|
|
25
|
+
- **Exporting traces to a managed observability service**: discarded. Traces contain the prompts and the extracted fields — the most sensitive payloads in the system. Convenience does not clear a data transfer; if the need appears, it is its own decision with recipient, class and retention named.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
- **Positive**: every port's placement and sovereignty is a traced, reversible decision; residency is pinned where it is actually enforced (the adapter binding, not a policy document); the two non-in-app placements are auditable from one table.
|
|
30
|
+
- **Negative, accepted**: the model path depends on a vendor runtime — bounded by the port and the approved-deployment binding; the ticketing coupling is real but contained in the anticorruption adapter.
|
|
31
|
+
- **On other boundaries**: `execution-topology.md` §2 records the rows this ADR locks; the usage registry (§3) classes the deployment's risk; the threat model treats both external surfaces as untrusted.
|
|
32
|
+
|
|
33
|
+
## Reevaluation trigger (mandatory, dated)
|
|
34
|
+
|
|
35
|
+
Reopen this decision if any of: a second application needs the same model routing, quotas or audit (→ shared gateway on existing infra); the ticketing system is migrated or multi-tenanted; anyone requests third-party trace export; or the data classification of inbound requests changes (e.g. confirmed regulated data). Until a trigger fires, apply without reopening.
|
|
36
|
+
|
|
37
|
+
**Trigger set on**: 2026-05-12 · **Watched via**: the usage registry review at each iterate gate, and the architecture review before any new consumer of the model path
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
- [execution-topology.md](../execution-topology.md) §2 — the port → placement map this ADR locks.
|
|
42
|
+
- [architecture.md](../architecture.md) §2, §3 — the ports and the data residency constraint.
|
|
43
|
+
- [shared-bricks.md](../shared-bricks.md) — the five location families and six criteria used to arbitrate.
|
|
44
|
+
- [ADR-0002](ADR-0002-deterministic-guard-on-extracted-fields.md) — the guard standing before RoutingPort.
|
|
@@ -39,8 +39,8 @@ The output contract is the **TriageRecord v1.0** schema: closed category vocabul
|
|
|
39
39
|
| contracts-governance | applied | §3 TriageRecord v1.0 — versioned, additive, tolerant reader, fail-closed; contracts/ |
|
|
40
40
|
| hexa-architecture | applied | §2 pure triage domain, four ports; code/src/core/ |
|
|
41
41
|
| hexa-adapter-pattern | applied | §3 every dependency behind a port; code/src/adapters/ |
|
|
42
|
-
| hexa-typescript-native | n/a | language deliberately left open at this note (§5); locked at floor kickoff (ADR-
|
|
43
|
-
| process-adr-and-journal | applied | adr/ADR-0001, adr/ADR-0002 — dated decisions with reevaluation triggers |
|
|
42
|
+
| hexa-typescript-native | n/a | language deliberately left open at this note (§5); locked at floor kickoff (ADR-0004 pending) |
|
|
43
|
+
| process-adr-and-journal | applied | adr/ADR-0001, adr/ADR-0002, adr/ADR-0003 — dated decisions with reevaluation triggers |
|
|
44
44
|
| security-mcp-server-pinning | n/a | the floor consumes no MCP or external tool server; tools are in-process and deterministic |
|
|
45
45
|
|
|
46
46
|
## 5. What stays open
|
|
@@ -61,4 +61,5 @@ Per the framing note: auto-drafted acknowledgments under approval, priority scor
|
|
|
61
61
|
|---|---|
|
|
62
62
|
| Single orchestrator, sequential triage | [ADR-0001](adr/ADR-0001-single-orchestrator.md) |
|
|
63
63
|
| Deterministic guard on extracted fields | [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md) |
|
|
64
|
-
|
|
|
64
|
+
| Port placement and sovereignty (the infra ADR family) | [ADR-0003](adr/ADR-0003-port-placement-and-sovereignty.md) |
|
|
65
|
+
| Core language (open — to be locked at floor kickoff) | ADR-0004 (pending) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Decision Matrix
|
|
2
2
|
|
|
3
|
-
**Mission state (2026-05-12, Architect gate)** — matrix adopted as the arbitration reference for this mission. Positions taken so far, all on the sober default: number of agents → single orchestrator, locked in [ADR-0001](adr/ADR-0001-single-orchestrator.md); untrusted input and deterministic frontier → guard on extracted fields, locked in [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md); core language → open, to be locked at floor kickoff (ADR-
|
|
3
|
+
**Mission state (2026-05-12, Architect gate)** — matrix adopted as the arbitration reference for this mission. Positions taken so far, all on the sober default: number of agents → single orchestrator, locked in [ADR-0001](adr/ADR-0001-single-orchestrator.md); untrusted input and deterministic frontier → guard on extracted fields, locked in [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md); port placement and sovereignty → the two non-in-app placements locked in [ADR-0003](adr/ADR-0003-port-placement-and-sovereignty.md); core language → open, to be locked at floor kickoff (ADR-0004, pending). No trigger has fired; watched signals are tracked in [floor.md](floor.md) §4.
|
|
4
4
|
|
|
5
5
|
How to use this file: every structural decision below carries **one sober default and one explicit trigger**. You start on the default. You only move when the trigger fires — measured, observed, named. **No trigger, no change. Every switch is an ADR** (`runward/adr/`), with the evidence that fired the trigger and a date to re-evaluate. Fill the matrix during the Architect phase; reopen it at every Iterate gate.
|
|
6
6
|
|
|
@@ -11,8 +11,8 @@ The triage domain says *what* the system does (raw request in, validated `Triage
|
|
|
11
11
|
| Port | Adapter (what runs) | Location family | Data class(es) crossing it | Sovereignty level | ADR / evidence | Re-evaluation trigger |
|
|
12
12
|
|---|---|---|---|---|---|---|
|
|
13
13
|
| RequestIntakePort | mailbox / web-form adapter, in-process | In-app, consuming existing infra (mailbox) | inbound request text (internal, may carry personal data) | raised | architecture.md §3 | intake volume outgrows in-process handling → dedicated queue |
|
|
14
|
-
| ModelPort | model adapter bound to the approved deployment | Managed model-vendor runtime | request text (internal / personal) | raised — residency enforced at the adapter, not in the domain | architecture.md §2
|
|
15
|
-
| RoutingPort | anticorruption adapter → ticketing system of record | Existing infrastructure | `TriageRecord` (internal business) | standard; approval-gated for compliance-flagged records |
|
|
14
|
+
| ModelPort | model adapter bound to the approved deployment | Managed model-vendor runtime | request text (internal / personal) | raised — residency enforced at the adapter, not in the domain | ADR-0003 (placement + residency); architecture.md §2 | a second application needs the same routing/quotas → shared model gateway |
|
|
15
|
+
| RoutingPort | anticorruption adapter → ticketing system of record | Existing infrastructure | `TriageRecord` (internal business) | standard; approval-gated for compliance-flagged records | ADR-0003 (placement); ADR-0002 (deterministic guard) | the ticketing system moves or multi-tenants → re-evaluate the adapter placement |
|
|
16
16
|
| PersistencePort | append-only log, in-process | In-app | `TriageRecord` plus provenance (internal) | standard | architecture.md §3 | multi-instance required → externalized store (iterate) |
|
|
17
17
|
|
|
18
18
|
Traces are data: this floor exports none to a third party (see the conformance note below).
|
|
@@ -29,7 +29,7 @@ Risk is classed by deployment, not by platform. This floor is one deployment.
|
|
|
29
29
|
|
|
30
30
|
| Rule | Status | Evidence |
|
|
31
31
|
|---|---|---|
|
|
32
|
-
| topology-port-placement-mapped | applied | §2 map — all four ports placed; the two non-in-app placements (ModelPort → managed vendor runtime
|
|
32
|
+
| topology-port-placement-mapped | applied | §2 map — all four ports placed; the two non-in-app placements (ModelPort → managed vendor runtime, RoutingPort → existing ticketing infra) are locked in ADR-0003 (the infra ADR family) |
|
|
33
33
|
| topology-sovereignty-by-data-class | applied | §2 map — a data class and a sovereignty level per port; request text is bound to the approved model deployment (residency), the `TriageRecord` is kept internal |
|
|
34
34
|
| topology-trace-export-decision | n/a | the floor exports no execution traces to a third party; observability is in-app structured logs per governance/observability-schema.md |
|
|
35
35
|
| topology-usage-registry-present | applied | §3 usage registry — the single prod deployment with its risk class, data classes, action scopes and owner |
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
|
|
20
20
|
| Rule | Status | Evidence |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| frontier-deterministic-boundary | applied | code/src/core/domain/guard.ts — every action-bearing field recomputed/verified (ADR-0002), fail-closed |
|
|
23
|
-
| hexa-move-deterministic-out | applied | code/src/core/domain/guard.ts
|
|
22
|
+
| frontier-deterministic-boundary | applied | file:code/src/core/domain/guard.ts#guardFields; test:code/test/triage.test.ts — every action-bearing field recomputed/verified (ADR-0002), fail-closed |
|
|
23
|
+
| hexa-move-deterministic-out | applied | file:code/src/core/domain/guard.ts#parseDeadline — classification and validation are deterministic, out of the model |
|
|
24
24
|
| config-secrets-boundary | n/a | the illustrative floor runs the deterministic keyword classifier; no provider secret is read in this example code |
|
|
25
25
|
| provider-llm-auto-detection | n/a | only the deterministic keyword adapter ships here; no real provider to auto-detect |
|
|
26
26
|
| security-prompt-injection | applied | threat-model §3 + ADR-0002 — model-proposed values never act; request text is data, not instruction |
|
|
27
27
|
| hexa-architecture | applied | code/src/core/ pure domain behind four ports |
|
|
28
28
|
| hexa-adapter-pattern | applied | code/src/adapters/ — mailbox/web, keyword-model, routing, log behind ports |
|
|
29
|
-
| provider-no-crash-missing-env | applied | code/src/adapters/keyword-model.adapter.ts — deterministic fallback runs with no key |
|
|
30
|
-
| state-event-sourcing | applied | code/src/adapters/in-memory-triage-log.adapter.ts — append-only, keyed by request ID |
|
|
29
|
+
| provider-no-crash-missing-env | applied | file:code/src/adapters/keyword-model.adapter.ts — deterministic fallback runs with no key |
|
|
30
|
+
| state-event-sourcing | applied | file:code/src/adapters/in-memory-triage-log.adapter.ts — append-only, keyed by request ID |
|
|
31
31
|
| tools-scope-atomicity | applied | architecture §2 middleware chain + approval on RoutingPort for compliance records |
|
|
32
32
|
|
|
33
33
|
## 2. Proof against the success criterion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runward",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "After the spec: ship and run. A delivery framework for agentic systems
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "After the spec: ship and run. A delivery framework for agentic systems \u2014 floor first, evolution on evidence, governance from day zero, handover with proof.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic",
|
|
7
7
|
"ai-agents",
|
|
@@ -22,31 +22,43 @@
|
|
|
22
22
|
"url": "https://github.com/stranxik/runward/issues"
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
25
28
|
"bin": {
|
|
26
29
|
"runward": "dist/cli.js"
|
|
27
30
|
},
|
|
28
31
|
"files": [
|
|
29
32
|
"dist",
|
|
30
33
|
"templates",
|
|
34
|
+
"regimes",
|
|
31
35
|
"examples/request-triage/runward",
|
|
36
|
+
"examples/request-triage/code/src",
|
|
37
|
+
"examples/request-triage/code/test",
|
|
38
|
+
"examples/request-triage/code/package.json",
|
|
39
|
+
"examples/request-triage/code/tsconfig.json",
|
|
40
|
+
"examples/request-triage/code/README.md",
|
|
32
41
|
"README.md",
|
|
33
|
-
"NOTICE.md"
|
|
42
|
+
"NOTICE.md",
|
|
43
|
+
"LICENSE"
|
|
34
44
|
],
|
|
35
45
|
"engines": {
|
|
36
46
|
"node": ">=20"
|
|
37
47
|
},
|
|
38
48
|
"scripts": {
|
|
39
49
|
"build": "tsc",
|
|
40
|
-
"test": "npm run build && node test/smoke.js && node test/oscal-schema.js",
|
|
41
|
-
"
|
|
50
|
+
"test": "npm run build && node --test test/unit/*.test.js && node test/smoke.js && node test/oscal-schema.js",
|
|
51
|
+
"test:unit": "npm run build && node --test test/unit/*.test.js",
|
|
52
|
+
"coverage": "npm run build && node --test --experimental-test-coverage test/unit/*.test.js",
|
|
53
|
+
"prepublishOnly": "npm run build && node --test test/unit/*.test.js && node test/smoke.js && node test/oscal-schema.js"
|
|
42
54
|
},
|
|
43
55
|
"dependencies": {
|
|
44
|
-
"@inquirer/prompts": "^
|
|
56
|
+
"@inquirer/prompts": "^8.5.2",
|
|
45
57
|
"chalk": "^5.6.0",
|
|
46
|
-
"commander": "^
|
|
58
|
+
"commander": "^15.0.0"
|
|
47
59
|
},
|
|
48
60
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^
|
|
61
|
+
"@types/node": "^26.1.1",
|
|
50
62
|
"ajv": "^8.20.0",
|
|
51
63
|
"ajv-formats": "^3.0.1",
|
|
52
64
|
"js-yaml": "^5.2.1",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Regime mapping data (ADR-0022). Cut line: this file carries only what tracks Regulation (EU) 2024/1689 — the Annex III applicability date, the article references, the Annex IV point-by-point coverage rows, and the provider-required list. runward's stable product voice stays in src/lib/compliance.ts. An Annex IV amendment or corrigendum is a NEW regimes/eu-ai-act@<version>.json, never an edit of this file.",
|
|
3
|
+
"regime": "eu-ai-act",
|
|
4
|
+
"label": "EU AI Act (Annex IV)",
|
|
5
|
+
"version": "2024-1689",
|
|
6
|
+
"notes": "Mapped 2026-07 against Regulation (EU) 2024/1689 (OJ L, 12 July 2024). High-risk (Annex III) obligations bind from 2 August 2026. Annex IV wording moves — confirm against the Official Journal before filing.",
|
|
7
|
+
"highRisk": {
|
|
8
|
+
"bindFrom": "2 August 2026",
|
|
9
|
+
"scope": "Annex III"
|
|
10
|
+
},
|
|
11
|
+
"articles": {
|
|
12
|
+
"runtimeLogging": "art. 12"
|
|
13
|
+
},
|
|
14
|
+
"annexIv": [
|
|
15
|
+
{ "point": "1. General description", "supplies": "UI, HW/SW/firmware notes", "provider": "intended purpose, provider, versioning, distribution" },
|
|
16
|
+
{ "point": "2. Elements & development", "supplies": "**architecture, validation procedures + metrics, cybersecurity (manifest + rubric + threat model); design choices, alternatives, assumptions, pre-determined changes = the ADR journal**", "provider": "third-party sourcing/licensing, sign-off on test logs" },
|
|
17
|
+
{ "point": "3. Monitoring & control", "supplies": "accuracy characterization, input-data specs, oversight tooling", "provider": "fundamental-rights / discrimination risk sourcing" },
|
|
18
|
+
{ "point": "4. Performance metrics", "supplies": "metric-choice justification (rubric)", "provider": "—" },
|
|
19
|
+
{ "point": "5. Risk management (art. 9)", "supplies": "technical inputs (threat model, testing)", "provider": "**risk acceptance / RMS governance**" },
|
|
20
|
+
{ "point": "6. Lifecycle changes", "supplies": "engineering change record (ADR journal)", "provider": "release/change-management governance" },
|
|
21
|
+
{ "point": "7. Harmonised standards", "supplies": "technical notes", "provider": "**standards selection** (compliance strategy)" },
|
|
22
|
+
{ "point": "8. Declaration of conformity", "supplies": "—", "provider": "**signed legal act (art. 47)**" },
|
|
23
|
+
{ "point": "9. Post-market monitoring", "supplies": "telemetry/logging backbone", "provider": "**post-market monitoring plan (art. 72)**" }
|
|
24
|
+
],
|
|
25
|
+
"operatorRequired": [
|
|
26
|
+
"**Point 1** general description (intended purpose, provider, versioning) · **Point 5** RMS governance & risk acceptance (art. 9).",
|
|
27
|
+
"**Point 7** harmonised-standards selection · **Point 8** the signed **EU declaration of conformity** (art. 47) · **Point 9** the **post-market monitoring plan** (art. 72).",
|
|
28
|
+
"**Art. 12 runtime event logs** — produced by the running system, not by runward."
|
|
29
|
+
],
|
|
30
|
+
"disclaimerTail": "Annex IV wording moves — confirm against the Official Journal (Reg. (EU) 2024/1689) before filing."
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Regime mapping data (ADR-0022). Cut line: this file carries only what tracks the ISO/IEC 42001:2023 text — clause references used in the draft, the operator-required clause list, and the dated caveat. runward's stable product voice (draft framing, section narrative, table glue) stays in src/lib/compliance.ts. A revision of the standard is a NEW regimes/iso-42001@<version>.json, never an edit of this file.",
|
|
3
|
+
"regime": "iso-42001",
|
|
4
|
+
"label": "ISO/IEC 42001",
|
|
5
|
+
"version": "2023",
|
|
6
|
+
"notes": "Mapped 2026-07 against ISO/IEC 42001:2023 (first edition, 2023-12). ISO Annex A control counts/templates are behind the paywalled standard — confirm against the purchased text.",
|
|
7
|
+
"clauses": {
|
|
8
|
+
"riskAssessment": "6.1.2",
|
|
9
|
+
"controlSelection": "6.1.3",
|
|
10
|
+
"annexControls": "Annex A"
|
|
11
|
+
},
|
|
12
|
+
"operatorRequired": [
|
|
13
|
+
"**AI policy** (5.2) and **AIMS scope** (4.3).",
|
|
14
|
+
"**Statement of Applicability — the applicability decisions and inclusion/exclusion justifications** (6.1.3): runward supplies the status + evidence columns; the *applicability* judgment is yours.",
|
|
15
|
+
"**Risk methodology, acceptance criteria and risk-acceptance decisions** (6.1.2, 8.3).",
|
|
16
|
+
"**AI system impact assessment report and deployment authorization** (6.1.4).",
|
|
17
|
+
"**Objectives and targets** (6.2), **roles and competence** (A.3, 7.2).",
|
|
18
|
+
"**Internal audit** (9.2) and **management review** minutes (9.3).",
|
|
19
|
+
"**Runtime AI event logs** (A.6.2.8) — produced by the running system, not by runward."
|
|
20
|
+
],
|
|
21
|
+
"disclaimerTail": "ISO Annex A control counts/templates are behind the paywalled standard — confirm against the purchased text."
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Regime mapping data (ADR-0022). Cut line: this file carries only what tracks NIST AI 100-1 (AI RMF 1.0, January 2023) — the ASI crosswalk target functions, the MEASURE reference, and the operator-required function list. runward's stable product voice stays in src/lib/compliance.ts. An AI RMF update is a NEW regimes/nist-ai-rmf@<version>.json, never an edit of this file.",
|
|
3
|
+
"regime": "nist-ai-rmf",
|
|
4
|
+
"label": "NIST AI RMF",
|
|
5
|
+
"version": "1.0",
|
|
6
|
+
"notes": "Mapped 2026-07 against NIST AI 100-1 (AI RMF 1.0, January 2023) and the companion Playbook.",
|
|
7
|
+
"crosswalk": {
|
|
8
|
+
"primary": "each agentic-security risk lands primarily under **MEASURE** (test & evaluate, esp. security & resilience) and **MANAGE** (risk treatment)",
|
|
9
|
+
"confirmAgainst": "AI RMF §5"
|
|
10
|
+
},
|
|
11
|
+
"measureRef": "MEASURE 2.x",
|
|
12
|
+
"operatorRequired": [
|
|
13
|
+
"**GOVERN** — policies, roles, accountability, **risk tolerance** (almost entirely organizational).",
|
|
14
|
+
"**MAP** — intended purpose, business/legal context, use-case risk enumeration.",
|
|
15
|
+
"**MANAGE** — the **go/no-go acceptance** decision, resourcing, response planning.",
|
|
16
|
+
"**Profiles** — Current/Target selection, prioritization, the risk-tolerance choices behind them."
|
|
17
|
+
],
|
|
18
|
+
"disclaimerTail": "NIST prescribes no report template — confirm against AI 100-1 and the Playbook."
|
|
19
|
+
}
|
|
@@ -39,6 +39,10 @@ A non-zero exit aborts the commit. Bypass a single commit with `git commit --no-
|
|
|
39
39
|
|
|
40
40
|
Copy the job into a workflow under `.github/workflows/` (or merge it into an existing one). Make it a required status check on your protected branch so no gap merges. This is the audit-evidence seam for a regulated pipeline: a dated, versioned record that the gate passed on every merge.
|
|
41
41
|
|
|
42
|
+
## `gitlab-ci.yml` — the same required check, GitLab flavor
|
|
43
|
+
|
|
44
|
+
Merge the job into your `.gitlab-ci.yml` and require the pipeline on your protected branch (Settings → Merge requests). Same port, same one line — the gate does not care which forge reads its exit code.
|
|
45
|
+
|
|
42
46
|
## `claude-code-settings.json` — run the gate at the agent's turn-end (one example)
|
|
43
47
|
|
|
44
48
|
Merge the `hooks` block into your `.claude/settings.json` (or `.claude/settings.local.json`). The `Stop` hook runs the gate when the agent finishes a turn and surfaces the verdict in the loop — so an agent can no longer close out with the gate never run.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# runward gate — GitLab CI adapter (sample).
|
|
2
|
+
# Inert until you install it: see runward/adapters/README.md.
|
|
3
|
+
# Merge this job into your .gitlab-ci.yml, then make the pipeline required
|
|
4
|
+
# on your protected branch (Settings → Merge requests → pipelines must
|
|
5
|
+
# succeed) so no gap merges.
|
|
6
|
+
#
|
|
7
|
+
# The gate is deterministic and zero-LLM: no secrets, no model key needed.
|
|
8
|
+
# Exit codes (the port contract): 0 clean · 1 gaps · 2 no mission found.
|
|
9
|
+
|
|
10
|
+
runward-gate:
|
|
11
|
+
image: node:20
|
|
12
|
+
# Use --strict to also enforce the rule-conformance manifests.
|
|
13
|
+
# Drop --strict for the deliverable audit only.
|
|
14
|
+
script:
|
|
15
|
+
- npx --yes runward check --strict
|
|
16
|
+
rules:
|
|
17
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
18
|
+
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
## Rule conformance
|
|
41
41
|
|
|
42
42
|
> Account for every CRITICAL/HIGH craft rule mapped to the architect phase (`runward/rules/`, frontmatter `phases: [architect]`). `applied` needs a pointer; `deviated` needs an ADR; `n/a` needs a reason. `runward check --strict` verifies this table is complete — it checks a traced decision, not the quality of your architecture.
|
|
43
|
+
>
|
|
44
|
+
> Evidence can be **typed**, and typed pointers are verified deterministically at the gate: `file:PATH[:LINE][#SYMBOL]` · `test:PATH[::NAME]` · `adr:NNNN` — several per cell, separated by `;`. The gate checks resolution, non-emptiness, line count, symbol/test-name presence, and the rule's `signature:` when it declares one (ADR-0019/0020). Free prose stays valid — it is your judgment; a path it cites must simply not point at an empty file.
|
|
43
45
|
|
|
44
46
|
| Rule | Status | Evidence |
|
|
45
47
|
|---|---|---|
|
|
@@ -40,6 +40,8 @@ Risk is classed **by deployment, not by platform** (§15): the same platform hos
|
|
|
40
40
|
## Rule conformance
|
|
41
41
|
|
|
42
42
|
> Account for every CRITICAL/HIGH craft rule mapped to the topology phase (`runward/rules/`, frontmatter `phases: [topology]`). `applied` needs a pointer; `deviated` needs an ADR; `n/a` needs a reason. `runward check --strict` verifies this table is complete — it checks a traced placement decision, not where you should run.
|
|
43
|
+
>
|
|
44
|
+
> Evidence can be **typed**, and typed pointers are verified deterministically at the gate: `file:PATH[:LINE][#SYMBOL]` · `test:PATH[::NAME]` · `adr:NNNN` — several per cell, separated by `;`. The gate checks resolution, non-emptiness, line count, symbol/test-name presence, and the rule's `signature:` when it declares one (ADR-0019/0020). Free prose stays valid — it is your judgment; a path it cites must simply not point at an empty file.
|
|
43
45
|
|
|
44
46
|
| Rule | Status | Evidence |
|
|
45
47
|
|---|---|---|
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
## Rule conformance
|
|
21
21
|
|
|
22
22
|
> Account for every CRITICAL/HIGH craft rule mapped to the floor phase (`runward/rules/`, frontmatter `phases: [floor]`). Status: `applied` needs an evidence pointer (a `file:line` or a test); `deviated` needs an ADR reference; `n/a` needs a one-line reason. `runward check --strict` verifies this table is complete and well-formed — it does not judge your implementation; you do, at the gate.
|
|
23
|
+
>
|
|
24
|
+
> Evidence can be **typed**, and typed pointers are verified deterministically at the gate: `file:PATH[:LINE][#SYMBOL]` · `test:PATH[::NAME]` · `adr:NNNN` — several per cell, separated by `;`. The gate checks resolution, non-emptiness, line count, symbol/test-name presence, and the rule's `signature:` when it declares one (ADR-0019/0020). Free prose stays valid — it is your judgment; a path it cites must simply not point at an empty file.
|
|
23
25
|
|
|
24
26
|
| Rule | Status | Evidence |
|
|
25
27
|
|---|---|---|
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
## Rule conformance
|
|
52
52
|
|
|
53
53
|
> Account for every CRITICAL/HIGH craft rule mapped to the govern phase (`runward/rules/`, frontmatter `phases: [govern]`). `applied` needs a pointer; `deviated` needs an ADR; `n/a` needs a reason. `runward check --strict` verifies this table is complete — it checks a traced decision, not the quality of your governance.
|
|
54
|
+
>
|
|
55
|
+
> Evidence can be **typed**, and typed pointers are verified deterministically at the gate: `file:PATH[:LINE][#SYMBOL]` · `test:PATH[::NAME]` · `adr:NNNN` — several per cell, separated by `;`. The gate checks resolution, non-emptiness, line count, symbol/test-name presence, and the rule's `signature:` when it declares one (ADR-0019/0020). Free prose stays valid — it is your judgment; a path it cites must simply not point at an empty file.
|
|
54
56
|
|
|
55
57
|
| Rule | Status | Evidence |
|
|
56
58
|
|---|---|---|
|
|
@@ -3,6 +3,7 @@ title: Deterministic Boundary of the Model
|
|
|
3
3
|
impact: CRITICAL
|
|
4
4
|
asi: [ASI01]
|
|
5
5
|
phases: [floor]
|
|
6
|
+
signature: assertGrounded|GroundingError|fail[-\s]?closed
|
|
6
7
|
impactDescription: Keeps every fact, figure and decision that can be checked out of the model, so the system is verifiable and cannot hallucinate load-bearing values
|
|
7
8
|
tags: [architecture, llm, frontier, grounding, safety, determinism]
|
|
8
9
|
---
|
|
@@ -12,7 +12,7 @@ This project is delivered with the Runward method: floor first, evolution on evi
|
|
|
12
12
|
|
|
13
13
|
## How to work
|
|
14
14
|
|
|
15
|
-
- Apply the craft rules in `runward/rules/` while building — and confront them at the point of action, not from memory. Each rule declares where it applies (`phases:`); a build phase surfaces its CRITICAL/HIGH rules to open and account for in the deliverable's `Rule conformance` manifest: `applied` with a `file:
|
|
15
|
+
- Apply the craft rules in `runward/rules/` while building — and confront them at the point of action, not from memory (`runward explain <rule>` prints a rule's why and full text). Each rule declares where it applies (`phases:`); a build phase surfaces its CRITICAL/HIGH rules to open and account for in the deliverable's `Rule conformance` manifest. `runward manifest --sync` scaffolds the missing rows; you fill the decision: `applied` with a **typed pointer the gate verifies** — `file:PATH[:LINE][#SYMBOL]`, `test:PATH[::NAME]`, `adr:NNNN` (or prose, which stays the operator's judgment) — `deviated` with an ADR, `n/a` with a real reason. A signed rule (frontmatter `signature:`) needs evidence whose content matches its signature; a pointer at an empty or vanished file fails the gate. When a rule and a habit conflict, the rule wins; deviating requires an ADR. `runward check --strict` verifies the manifest and the evidence's shape — never the quality of the code; you judge that at the gate. When the gate is green, run `runward/workflows/verify.md` before crossing: an advisory, adversarial cite-vs-apply pass (ideally on a different model) that judges whether the code an `applied` row points at actually applies the rule or merely cites it, and records its findings in `runward/governance/verify-findings.md`. It is advisory — it never blocks the gate (ADR-0007). Once the operator crosses, they may seal the evidence with `runward check --freeze` (ADR-0021).
|
|
16
16
|
- Consult `runward/decision-matrix.md` before adding any capability: 22 arbitrations, each with a sober default and an explicit trigger. No trigger, no change.
|
|
17
17
|
- Before any structural decision, run `runward/workflows/decision-loop.md`: verify in the real code, check the sourced state of the art, challenge the source, take a durable position, lock it in an ADR — only then edit.
|
|
18
18
|
- One ADR per structural decision, in `runward/adr/`, with a dated re-evaluation trigger. Use the template `runward/adr/ADR-0000-template.md`.
|