runward 0.12.1 → 0.13.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/NOTICE.md +1 -1
- package/README.md +7 -4
- package/dist/cli.js +2 -1
- package/dist/commands/compliance.js +0 -5
- package/dist/commands/init.js +52 -18
- package/dist/lib/compliance.js +1 -0
- package/dist/lib/constants.js +2 -2
- package/dist/lib/paths.js +2 -0
- package/dist/lib/rule-migrations.js +1 -1
- package/examples/request-triage/README.md +34 -0
- package/examples/request-triage/runward/adr/ADR-0001-single-orchestrator.md +37 -0
- package/examples/request-triage/runward/adr/ADR-0002-deterministic-guard-on-extracted-fields.md +38 -0
- package/examples/request-triage/runward/architecture.md +64 -0
- package/examples/request-triage/runward/contracts/model-port.md +55 -0
- package/examples/request-triage/runward/contracts/persistence-port.md +55 -0
- package/examples/request-triage/runward/contracts/request-intake.md +58 -0
- package/examples/request-triage/runward/contracts/routing-port.md +56 -0
- package/examples/request-triage/runward/decision-matrix.md +39 -0
- package/examples/request-triage/runward/execution-topology.md +41 -0
- package/examples/request-triage/runward/floor.md +60 -0
- package/examples/request-triage/runward/framing.md +57 -0
- package/examples/request-triage/runward/governance/evaluation-rubric.md +62 -0
- package/examples/request-triage/runward/governance/observability-schema.md +43 -0
- package/examples/request-triage/runward/governance/threat-model.md +66 -0
- package/examples/request-triage/runward/mission-contract.md +51 -0
- package/examples/request-triage/runward/runbook.md +63 -0
- package/package.json +2 -1
- package/templates/rules/frontier-deterministic-boundary.md +1 -1
- package/templates/rules/hexa-move-deterministic-out.md +1 -1
- package/templates/rules/security-code-execution-sandbox.md +20 -0
- package/templates/rules/security-human-agent-trust.md +20 -0
- package/templates/workflows/architect.md +2 -2
- package/templates/workflows/method.md +1 -1
package/NOTICE.md
CHANGED
|
@@ -6,7 +6,7 @@ The doctrine **“Designing and Running Agentic Systems”** (Thibault Souris, 2
|
|
|
6
6
|
|
|
7
7
|
What this means in practice:
|
|
8
8
|
|
|
9
|
-
- The **ideas and methods** (the six-phase method, the
|
|
9
|
+
- The **ideas and methods** (the six-phase method, the architecture that frames the model, floor-first delivery, evolution on triggers, lethal trifecta rule…) are free by nature. This repository implements them; you can too.
|
|
10
10
|
- The **expression** of the doctrine (its text, figures, layout) is protected: you may share the published PDFs unmodified, you may not create derivative versions of the text.
|
|
11
11
|
- Workflows and templates in this repository are original MIT-licensed writings that implement the method. They are the contribution surface.
|
|
12
12
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Spec-driven frameworks answered the first half of that problem: write the right
|
|
|
22
22
|
|
|
23
23
|
Agentic systems break four assumptions of classical distributed engineering. The core component is **non-deterministic**: same input, different output, by design. **Input is indistinguishable from instruction**: anything the model reads can try to command it, so prompt injection is structural, not a bug to patch. **Forgetting becomes an engineering problem**: memory that only grows drowns the signal, so decay, invalidation and consolidation have to be designed, not hoped for. And the **blast radius is unprecedented**: an agent with tools acts on the world, so a bad output is no longer just a bad answer.
|
|
24
24
|
|
|
25
|
-
Runward
|
|
25
|
+
Runward is the **FDE method** made executable. Its guiding principle: the architecture frames the model, never the other way around — the model and the infrastructure are adapter decisions, taken behind stable contracts. From there, five architecture gestures: boundaries before the stack; start simple, isolate by contract, grow on evidence; keep the deterministic out of the model; explicit state and governed memory; govern, trace and evaluate from day zero. And a delivery spine: six gated phases (a gate: a checkpoint you cross on evidence, never on assertion), each with a Definition of Ready and a Definition of Done, a 22-arbitration decision matrix, and 60 craft rules. The phases, gestures and rules are what carry it.
|
|
26
26
|
|
|
27
27
|
## Who it's for — and when
|
|
28
28
|
|
|
@@ -45,11 +45,14 @@ The cost is discipline: one accountable operator, governance from day zero, and
|
|
|
45
45
|
## Install
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
npx runward init
|
|
48
|
+
npx runward init --example # a filled reference mission — see the whole chain green in one command
|
|
49
|
+
npx runward init # interactive wizard (blank templates for your own mission)
|
|
49
50
|
npx runward --yes init # non-interactive, defaults (CI-friendly)
|
|
50
51
|
npx runward init --tools claude,cursor,copilot,gemini,windsurf
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
**Fastest way to see what runward does:** `npx runward init --example` scaffolds the `request-triage` reference mission already filled, so `runward check` passes green and `runward compliance iso-42001` emits an audit-ready OSCAL pack out of the box. Then `npx runward init` (no `--example`) starts your own mission from blank templates.
|
|
55
|
+
|
|
53
56
|
New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
|
|
54
57
|
|
|
55
58
|
### Commands
|
|
@@ -83,7 +86,7 @@ your-project/
|
|
|
83
86
|
│ ├── shared-bricks.md # bricks beyond the app: placement families, brick matrix, sovereignty by data class
|
|
84
87
|
│ ├── floor.md # the smallest system that proves value on real traffic
|
|
85
88
|
│ ├── adr/ # one ADR per structural decision, with re-evaluation trigger
|
|
86
|
-
│ ├── rules/ #
|
|
89
|
+
│ ├── rules/ # 60 craft rules your agent applies while building
|
|
87
90
|
│ ├── governance/
|
|
88
91
|
│ │ ├── threat-model.md # lethal trifecta, 2-of-3 rule on the context window
|
|
89
92
|
│ │ ├── evaluation-rubric.md # test the deterministic, evaluate the non-deterministic
|
|
@@ -118,7 +121,7 @@ Phase 5 is transverse: it starts at day zero, not after the incident.
|
|
|
118
121
|
- **Floor, not MVP deck.** The floor is the smallest *running* system that proves value on real traffic. A presentation is not a floor.
|
|
119
122
|
- **Evolution on evidence.** Multi-agent, long-term memory, microservices, a bigger model: each has a sober default and an explicit trigger. No trigger, no complexity. Every switch is an ADR.
|
|
120
123
|
- **Security by architecture, not detection.** Prompt injection is constrained structurally (lethal trifecta, 2-of-3 rule), not filtered heuristically.
|
|
121
|
-
- **Craft rules, not vibes.**
|
|
124
|
+
- **Craft rules, not vibes.** 58 engineering craft rules ship with the mission (memory scoring, tiered retrieval, event sourcing, request-id propagation, multi-provider fallback, cost routing, post-turn pipelines, prompt-injection defenses…) — your agent applies them, `runward check` will not invent them. Code examples follow the reference-stack default (a single language in the core — TypeScript by default, an adapter decision like any other: see the decision matrix). The patterns are the contract; the language is the adapter.
|
|
122
125
|
- **One accountable operator, not a simulated team.** One human owns every gate while the agent executes the workflows — see [the operator role](docs/operator-role.md).
|
|
123
126
|
- **Handover as a deliverable.** The mission ends when the receiving team is autonomous, with proof.
|
|
124
127
|
|
package/dist/cli.js
CHANGED
|
@@ -54,6 +54,7 @@ program
|
|
|
54
54
|
.option("-p, --path <path>", "project directory (default: prompt, or . with --yes)")
|
|
55
55
|
.option("-t, --tools <list>", "comma-separated tool profiles: claude,cursor,copilot,gemini,windsurf")
|
|
56
56
|
.option("--force", "overwrite existing files")
|
|
57
|
+
.option("--example", "scaffold a filled reference mission (request-triage) — the whole chain is green out of the box")
|
|
57
58
|
.action(initCommand);
|
|
58
59
|
program
|
|
59
60
|
.command("check")
|
|
@@ -87,7 +88,7 @@ program
|
|
|
87
88
|
program
|
|
88
89
|
.command("compliance")
|
|
89
90
|
.description("assemble a regime-framed evidence pack from the mission (deterministic, read-only; a readiness draft, never a compliance claim)")
|
|
90
|
-
.argument("[regime]", "iso-42001
|
|
91
|
+
.argument("[regime]", "iso-42001 | nist-ai-rmf | eu-ai-act")
|
|
91
92
|
.option("-p, --path <path>", "project directory")
|
|
92
93
|
.action(complianceCommand);
|
|
93
94
|
program.parseAsync();
|
|
@@ -25,11 +25,6 @@ export async function complianceCommand(regime, opts) {
|
|
|
25
25
|
process.exit(2);
|
|
26
26
|
}
|
|
27
27
|
const spec = REGIMES[key];
|
|
28
|
-
if (!spec.render) {
|
|
29
|
-
console.error(status.error(`The ${spec.label} lens is not assembled yet — only iso-42001 for now (ADR-0016, built piece by piece).`));
|
|
30
|
-
console.log(" " + c.darkGray(`See the framing reference: docs/compliance/${key}.md`));
|
|
31
|
-
process.exit(2);
|
|
32
|
-
}
|
|
33
28
|
const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
|
|
34
29
|
if (!root) {
|
|
35
30
|
console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
|
package/dist/commands/init.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
3
|
import { checkbox, input, select } from "@inquirer/prompts";
|
|
4
|
-
import { TEMPLATES, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
4
|
+
import { TEMPLATES, EXAMPLE_MISSION, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
5
5
|
import { TOOL_PROFILES, TOOL_IDS } from "../lib/tools.js";
|
|
6
6
|
import { makeWriter } from "../lib/write.js";
|
|
7
7
|
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
8
|
+
/** Recursively copy a shipped directory tree (used to lay down the filled reference mission). */
|
|
9
|
+
function copyTree(w, srcDir, destDir) {
|
|
10
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
11
|
+
const src = join(srcDir, entry.name);
|
|
12
|
+
const dest = join(destDir, entry.name);
|
|
13
|
+
if (entry.isDirectory())
|
|
14
|
+
copyTree(w, src, dest);
|
|
15
|
+
else
|
|
16
|
+
w.copy(src, dest);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
export async function initCommand(opts) {
|
|
9
20
|
console.log(createHeader(`Runward v${VERSION}`, "After the spec: ship and run"));
|
|
10
21
|
const yes = isNonInteractive();
|
|
22
|
+
const example = opts.example ?? false;
|
|
11
23
|
const dryRun = process.env.RUNWARD_DRY_RUN === "1";
|
|
12
24
|
// ── Gather answers (wizard, or defaults with --yes) ──────────────
|
|
13
25
|
const dir = opts.path
|
|
14
26
|
?? (yes ? "." : await input({ message: "Project directory", default: "." }));
|
|
15
27
|
// The idea seeds the framing note: you arrive with a project, not with paperwork.
|
|
16
|
-
|
|
28
|
+
// In example mode the framing is already filled from the reference, so we skip it.
|
|
29
|
+
const idea = (yes || example) ? "" : await input({
|
|
17
30
|
message: "What are you building? (one line — it seeds your framing note)",
|
|
18
31
|
default: "",
|
|
19
32
|
});
|
|
@@ -25,14 +38,14 @@ export async function initCommand(opts) {
|
|
|
25
38
|
message: "Tool profiles (AGENTS.md is always written)",
|
|
26
39
|
choices: TOOL_PROFILES.map((t) => ({ value: t.id, name: t.label, checked: t.id === "claude" })),
|
|
27
40
|
});
|
|
28
|
-
const entryMode = yes ? "greenfield" : await select({
|
|
41
|
+
const entryMode = (yes || example) ? "greenfield" : await select({
|
|
29
42
|
message: "Entry mode",
|
|
30
43
|
choices: [
|
|
31
44
|
{ value: "greenfield", name: "Greenfield — new system, run the chain from the top" },
|
|
32
45
|
{ value: "brownfield", name: "Brownfield — existing system, characterize before touching" },
|
|
33
46
|
],
|
|
34
47
|
});
|
|
35
|
-
const tier = yes ? "floor" : await select({
|
|
48
|
+
const tier = (yes || example) ? "floor" : await select({
|
|
36
49
|
message: "Stopping tier (the sponsor's choice — can be revised)",
|
|
37
50
|
choices: [
|
|
38
51
|
{ value: "framing", name: "Framing only" },
|
|
@@ -47,20 +60,30 @@ export async function initCommand(opts) {
|
|
|
47
60
|
const root = resolve(process.cwd(), dir);
|
|
48
61
|
const mission = join(root, "runward");
|
|
49
62
|
const w = makeWriter({ force: opts.force ?? false, dryRun, root });
|
|
50
|
-
console.log(section("Mission structure"));
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
console.log(section(example ? "Mission structure (filled reference)" : "Mission structure"));
|
|
64
|
+
if (example) {
|
|
65
|
+
// Lay down the filled request-triage reference: every gate is green out of the box.
|
|
66
|
+
copyTree(w, EXAMPLE_MISSION, mission);
|
|
67
|
+
// The reference omits the three non-gated scaffolding notes; add them as blank templates so the scaffold stays complete.
|
|
68
|
+
for (const extra of ["reference-stack.md", "shared-bricks.md", "gap-analysis.md"]) {
|
|
69
|
+
w.copy(join(TEMPLATES, "mission", extra), join(mission, extra));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const prefill = (s) => {
|
|
74
|
+
let out = s
|
|
75
|
+
.replace("[greenfield | brownfield M1–M4]", entryMode)
|
|
76
|
+
.replace("[framing | floor | full chain]", tier);
|
|
77
|
+
if (idea) {
|
|
78
|
+
out = out
|
|
79
|
+
.replace("[system or mission name]", idea)
|
|
80
|
+
.replace("## 1. Problem\n", `## 1. Problem\n\n> Seed idea (from init): "${idea}" — now replace this with the process as actually observed.\n`);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
for (const [src, dest] of Object.entries(MISSION_LAYOUT)) {
|
|
85
|
+
w.copy(join(TEMPLATES, "mission", src), join(mission, dest), src === "framing.md" ? prefill : undefined);
|
|
59
86
|
}
|
|
60
|
-
return out;
|
|
61
|
-
};
|
|
62
|
-
for (const [src, dest] of Object.entries(MISSION_LAYOUT)) {
|
|
63
|
-
w.copy(join(TEMPLATES, "mission", src), join(mission, dest), src === "framing.md" ? prefill : undefined);
|
|
64
87
|
}
|
|
65
88
|
console.log(section("Workflows"));
|
|
66
89
|
for (const wf of readdirSync(join(TEMPLATES, "workflows"))) {
|
|
@@ -85,10 +108,21 @@ export async function initCommand(opts) {
|
|
|
85
108
|
// ── Summary ───────────────────────────────────────────────────────
|
|
86
109
|
console.log(section("Done"));
|
|
87
110
|
console.log(status.success(`${w.stats.written} file(s) ${dryRun ? "planned" : "written"}, ${w.stats.skipped} skipped`));
|
|
88
|
-
|
|
111
|
+
if (example) {
|
|
112
|
+
console.log(`
|
|
113
|
+
${c.primaryBold("Filled reference mission — the whole chain is already green.")}
|
|
114
|
+
1. Run ${c.primary("runward check")} — every gate passes; the deliverables are filled, not blank.
|
|
115
|
+
2. Run ${c.primary("runward compliance iso-42001")} — an audit-ready evidence pack (OSCAL) derived from the traced decisions.
|
|
116
|
+
3. Read ${c.white("runward/framing.md")}, the ADRs in ${c.white("runward/adr/")}, and ${c.white("runward/governance/threat-model.md")} to see how a real mission is traced end to end.
|
|
117
|
+
|
|
118
|
+
${c.gray("Start your own mission with")} ${c.primary("runward init")} ${c.gray("(without --example) to get blank templates.")}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log(`
|
|
89
122
|
${c.primaryBold("Next steps")}
|
|
90
123
|
1. Fill ${c.white("runward/framing.md")} — do not architect before the framing gate passes.
|
|
91
124
|
2. Point your agent at ${c.white("AGENTS.md")} and ${c.white("runward/workflows/method.md")}.
|
|
92
125
|
3. Run ${c.primary("runward check")} anytime to see which gate you are at.
|
|
93
126
|
${entryMode === "brownfield" ? c.warning("\n Brownfield entry: start with runward/workflows/brownfield.md — characterize before touching.\n") : ""}`);
|
|
127
|
+
}
|
|
94
128
|
}
|
package/dist/lib/compliance.js
CHANGED
|
@@ -54,6 +54,7 @@ function readRules(missionDir) {
|
|
|
54
54
|
/** The three build-phase deliverables that carry a `## Rule conformance` manifest (as `check --strict`). */
|
|
55
55
|
const CONFORMANCE_DELIVERABLES = [
|
|
56
56
|
["Architect", "architecture.md"],
|
|
57
|
+
["Topology", "execution-topology.md"],
|
|
57
58
|
["Floor", "floor.md"],
|
|
58
59
|
["Govern", "governance/threat-model.md"],
|
|
59
60
|
];
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Total number of craft rules shipped under templates/rules/. */
|
|
2
|
-
export const EXPECTED_RULES =
|
|
2
|
+
export const EXPECTED_RULES = 60;
|
|
3
3
|
/** Gate adapters shipped under templates/adapters/ (ADR-0012): one per harness seam, plus the port-contract README. */
|
|
4
4
|
export const EXPECTED_ADAPTERS = 4;
|
|
5
5
|
/** Routed-count floor: minimum CRITICAL/HIGH rules mapped to each build phase (ADR-0002).
|
|
6
6
|
* Lowering a floor is a deliberate, tracked edit — the `phases:` mapping cannot be silently
|
|
7
7
|
* stripped to make `check --strict` pass vacuously. */
|
|
8
|
-
export const EXPECTED_MAPPED = { architect: 6, topology: 4, floor: 10, govern:
|
|
8
|
+
export const EXPECTED_MAPPED = { architect: 6, topology: 4, floor: 10, govern: 12 };
|
package/dist/lib/paths.js
CHANGED
|
@@ -5,6 +5,8 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
5
5
|
/** Package root (works from dist/lib/ at runtime). */
|
|
6
6
|
export const PKG_ROOT = join(HERE, "..", "..");
|
|
7
7
|
export const TEMPLATES = join(PKG_ROOT, "templates");
|
|
8
|
+
/** Filled reference mission (shipped) — used by `init --example`. */
|
|
9
|
+
export const EXAMPLE_MISSION = join(PKG_ROOT, "examples", "request-triage", "runward");
|
|
8
10
|
export const VERSION = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
9
11
|
/** Mission layout: template file -> destination inside runward/ */
|
|
10
12
|
export const MISSION_LAYOUT = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const RULE_MIGRATIONS = {
|
|
2
2
|
"hexa-llm-boundary-principle": {
|
|
3
3
|
to: "hexa-move-deterministic-out",
|
|
4
|
-
reason: "
|
|
4
|
+
reason: "renamed to name the pattern directly (move the deterministic out of the model)",
|
|
5
5
|
since: "v0.7.0",
|
|
6
6
|
},
|
|
7
7
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Example mission: request triage
|
|
2
|
+
|
|
3
|
+
A complete Runward mission, filled end to end, on a deliberately ordinary case: an organization receives heterogeneous inbound requests (support, sales, compliance) and triages them by hand. The floor is a qualifier that classifies each request, extracts the key fields, and routes it — with an observable success criterion measured on real traffic against the manual baseline.
|
|
4
|
+
|
|
5
|
+
Every document follows the corresponding template in `templates/mission/`, placeholders replaced. All names, volumes and figures are **illustrative**: they show what a filled note looks like, not a real engagement.
|
|
6
|
+
|
|
7
|
+
## How to read it
|
|
8
|
+
|
|
9
|
+
Read in mission order — the order the workflows produce them:
|
|
10
|
+
|
|
11
|
+
1. `runward/framing.md` — the problem, the observable success criterion, floor vs target, named deferrals, DoR check (one condition carried as a named risk).
|
|
12
|
+
2. `runward/mission-contract.md` — the one-page steering contract signed with the sponsor: engagements, DoD, gates.
|
|
13
|
+
3. `runward/architecture.md` — boundaries before stack: ports, versioned contract, default topology with triggers, language explicitly left open.
|
|
14
|
+
4. `runward/decision-matrix.md` — the arbitration reference, adopted at the Architect gate; every position on its sober default.
|
|
15
|
+
5. `runward/contracts/` — the four port contracts the architecture note declares: request intake, model, routing, persistence.
|
|
16
|
+
6. `runward/adr/ADR-0001-single-orchestrator.md` — the sober default, with its dated reevaluation trigger.
|
|
17
|
+
7. `runward/adr/ADR-0002-deterministic-guard-on-extracted-fields.md` — the model never supplies a value the system can compute or verify deterministically.
|
|
18
|
+
8. `runward/governance/` — instrumented from day zero, not retrofitted: threat model (the 2-of-3 rule held by construction), evaluation rubric (floor-tier capabilities: routing fidelity, abstention, criterion compliance — no memory bench, because the floor has no memory), observability schema.
|
|
19
|
+
9. `runward/floor.md` — the proof record: what shipped, what was measured, gaps, and which trigger is watched next.
|
|
20
|
+
10. `runward/runbook.md` — what the receiving team inherits: startup, degraded modes, recovery, failover.
|
|
21
|
+
|
|
22
|
+
The chain passes the gate audit: `runward check -p examples/request-triage` exits 0 (the smoke test asserts it).
|
|
23
|
+
|
|
24
|
+
Notice what the documents *refuse* to do: no stack picked at framing, no complexity without a trigger, no proof on hand-picked cases, no evaluation of capabilities the floor does not have.
|
|
25
|
+
|
|
26
|
+
## Runnable floor
|
|
27
|
+
|
|
28
|
+
The mission is also executable: [`code/`](code/) contains a standalone
|
|
29
|
+
TypeScript package (no key, no network, deterministic model adapter) that
|
|
30
|
+
implements what the documents decide — closed-vocabulary classification,
|
|
31
|
+
the deterministic guard on extracted fields (ADR-0002), guarded routing,
|
|
32
|
+
fail-closed compliance through suspend-and-rehydrate, abstention on unknown.
|
|
33
|
+
`npm install && npm test` in `code/` runs its 14 deterministic tests; see
|
|
34
|
+
[`code/README.md`](code/README.md).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ADR-0001: single orchestrator, sequential triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12
|
|
4
|
+
**Status**: accepted
|
|
5
|
+
**Deciders**: operator, Head of Operations (sponsor)
|
|
6
|
+
**Method**: decision-loop: reality-check on the observed process, sourced state of the art, challenge, durable position
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Triaging one request is a short sequence of dependent steps: normalize the raw input, propose a classification and field extractions (model), validate or recompute the extracted fields (deterministic, see ADR-0002), resolve the target queue, route or escalate to human review, persist. Each step consumes the output of the previous one; none is independent enough to parallelize, none handles content requiring an isolated context. Volume is ~400 requests per week — throughput is not a force here. The decision is taken at the orchestration boundary, behind the primary RequestIntakePort; it is structuring because topology is expensive to unwind once adapters and observability are built around it.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
One orchestrator, running a fixed sequential plan per request, holding no instance state. It composes the steps and carries no business logic: classification rules live in the domain, validation in the guards, translation in the adapters. This applies the framework's sober default: no multi-agent until a genuinely parallelizable or isolation-requiring subtask exists.
|
|
15
|
+
|
|
16
|
+
## Alternatives discarded
|
|
17
|
+
|
|
18
|
+
- **Multi-agent (one agent per step)**: discarded. No step is isolable enough to deserve its own context; the coordination and token overhead buys nothing measurable at this volume.
|
|
19
|
+
- **Model-driven dynamic planning**: discarded. The triage sequence is known and stable; letting the model decide the plan adds non-determinism, cost, and an attack surface without adding value.
|
|
20
|
+
- **Event-driven pipeline (queue between each step)**: tempered rather than eliminated. Useful at scale for backpressure, but at 400 requests/week it multiplies moving parts for no observed load. The persistence log already gives replayability.
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
|
|
24
|
+
- **Positive**: deterministic, replayable trajectories; a single point of cost accounting and tracing; one request ID covers the whole run with no parent-child lineage to manage; the fixed plan keeps model spend to the two calls that need it.
|
|
25
|
+
- **Negative, accepted**: if one step becomes heavy and independent — for example untrusted attachment processing that should run quarantined — the topology must be revisited. Cost taken on knowingly.
|
|
26
|
+
- **On other boundaries**: observability stays flat (one trajectory per request); security review has one path to guard; the contract between steps is internal and free to change until a step crosses a process boundary.
|
|
27
|
+
|
|
28
|
+
## Reevaluation trigger (mandatory, dated)
|
|
29
|
+
|
|
30
|
+
Reopen this decision when either observable signal appears: (1) a subtask requiring an isolated context or genuine parallelism enters scope — the named candidate is attachment processing, currently out of scope; or (2) end-to-end triage latency exceeds 60 seconds at p95 over a full week, measured on the observability schema, indicating the sequential plan no longer holds the load. Until then, apply without reopening.
|
|
31
|
+
|
|
32
|
+
**Trigger set on**: 2026-05-12 · **Watched via**: weekly review of the p95 latency metric and the scope backlog
|
|
33
|
+
|
|
34
|
+
## References
|
|
35
|
+
|
|
36
|
+
- [architecture.md](../architecture.md) §4 — default topology and triggers.
|
|
37
|
+
- [ADR-0002](ADR-0002-deterministic-guard-on-extracted-fields.md) — the guard step this plan sequences.
|
package/examples/request-triage/runward/adr/ADR-0002-deterministic-guard-on-extracted-fields.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ADR-0002: deterministic guard on model-extracted fields
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12
|
|
4
|
+
**Status**: accepted
|
|
5
|
+
**Deciders**: operator, Head of Operations (sponsor)
|
|
6
|
+
**Method**: decision-loop: reality-check on the TriageRecord contract, challenge on failure modes, durable position
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The model proposes the classification and the extracted key fields (requester identity, account reference, stated deadline). Those fields drive an action: routing into the ticketing system, where a wrong account reference attaches the request to the wrong customer and a missed deadline flag can breach a regulatory response window. A model-proposed value is a hypothesis, not a fact — plausible-looking account references and dates are exactly what a language model fabricates most fluently. The framing note's attached condition (no silent miss on compliance requests) makes this the highest-stakes boundary in the system. The decision is taken at the domain's output boundary, between the model port and the routing port.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Every model-extracted field is validated or recomputed deterministically before any action is taken on it. **The model never supplies a value the system can compute or verify itself.** Concretely: account references are resolved against the account registry (a deterministic tool) — a reference that does not resolve is dropped to `unverified`, never routed on; deadlines are re-parsed from the source text by a deterministic date parser, and the model's proposal is discarded in favor of the parsed value; requester identity is matched against the sender address before being trusted. Each field in the TriageRecord carries a provenance marker (`computed`, `verified`, `model-proposed`); the RoutingPort refuses, fail-closed, any record whose action-bearing fields are still `model-proposed`. Records that fail the guard go to the human review queue with the failure reason attached.
|
|
15
|
+
|
|
16
|
+
## Alternatives discarded
|
|
17
|
+
|
|
18
|
+
- **Trust the model's structured output**: discarded. Schema-valid is not correct — a well-formed fabricated account reference passes every JSON check and still routes to the wrong customer. Structure guarantees shape, not truth.
|
|
19
|
+
- **Second model call as verifier**: discarded. A verifier model has the same failure class as the extractor; it converts a deterministic check the system can do for free into a probabilistic one that costs tokens. Verification that can be computed must be computed.
|
|
20
|
+
- **Post-hoc detection (flag anomalies after routing)**: discarded. Detection after the action is too late for deadline-bearing compliance requests; the framing note's attached condition demands the miss never happen, not that it be noticed.
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
|
|
24
|
+
- **Positive**: no fabricated value can trigger an action; the guard is pure and unit-testable without a model in the loop; provenance markers make every routing decision auditable field by field; regressions surface at the boundary, in tests, not in the ticketing system.
|
|
25
|
+
- **Negative, accepted**: every deterministic check needs a maintained data source (account registry access, date-parsing rules), and legitimate requests with unresolvable references land in human review — an accepted false-positive cost, since review is cheaper than misrouting.
|
|
26
|
+
- **On other boundaries**: the TriageRecord contract gains the provenance marker (additive, v1.0); the observability schema logs guard outcomes per field, which feeds the evaluation rubric; the middleware chain enforces that RoutingPort is unreachable except through the guard.
|
|
27
|
+
|
|
28
|
+
## Reevaluation trigger (mandatory, dated)
|
|
29
|
+
|
|
30
|
+
Reopen this decision if the guard's escalation rate — the share of requests sent to human review because fields stayed unverified — exceeds 25% over two consecutive weeks, measured on the observability schema. That level would mean the deterministic sources are too weak for real traffic and the verification strategy (not the principle) needs redesign. Until then, apply without reopening.
|
|
31
|
+
|
|
32
|
+
**Trigger set on**: 2026-05-12 · **Watched via**: guard-escalation rate on the weekly observability review
|
|
33
|
+
|
|
34
|
+
## References
|
|
35
|
+
|
|
36
|
+
- [architecture.md](../architecture.md) §3 — TriageRecord contract and provenance markers.
|
|
37
|
+
- [ADR-0001](ADR-0001-single-orchestrator.md) — the sequential plan that places this guard before RoutingPort.
|
|
38
|
+
- Framing note §3 — attached condition on compliance-category requests.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Architecture Note: Inbound Request Triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12 · **Version**: v0.1 · **Framing note**: [framing.md](framing.md) · **ADR journal**: [adr/](adr/)
|
|
4
|
+
|
|
5
|
+
## 1. Context
|
|
6
|
+
|
|
7
|
+
The organization triages ~400 heterogeneous inbound requests per week by hand; the success criterion is a first-assignment routing accuracy above the manual baseline, measured on real traffic, with zero silent misses on compliance-category requests. This architecture must carry the floor defined in the framing note: classify, extract key fields, route — nothing more. See [framing.md](framing.md).
|
|
8
|
+
|
|
9
|
+
## 2. Boundaries
|
|
10
|
+
|
|
11
|
+
- **Domain ports.** The triage domain is pure: it turns a raw request into a validated triage record and knows nothing about mailboxes, ticketing APIs, or model providers. Every dependency is a contract. The model port is a port like any other — the reasoning engine is bound by its contract (input text in, candidate classification and field extractions out), not by its brand. It is a replaceable adapter behind a stable port, which is also where the data-residency constraint is enforced: the adapter binds to the approved deployment; the domain never sees a provider.
|
|
12
|
+
- **Integration protocol.** Deterministic capabilities (field validation, account lookup, queue resolution) are exposed as tools through a single registry with a middleware chain (logging, access control, cost accounting, approval). Should any capability later move out of process, it is exposed over the standardized tool protocol: a service is an adapter that moved into its own process; the domain does not change.
|
|
13
|
+
|
|
14
|
+
## 3. Ports
|
|
15
|
+
|
|
16
|
+
| Port | Direction | Intent | Contract version | Spec |
|
|
17
|
+
|---|---|---|---|---|
|
|
18
|
+
| RequestIntakePort | primary | receive a raw inbound request (mailbox adapter, web-form adapter) | v1.0 | contracts/request-intake.md |
|
|
19
|
+
| ModelPort | secondary | propose classification and field extractions from raw text | v1.0 | contracts/model-port.md |
|
|
20
|
+
| RoutingPort | secondary | assign a validated triage record to a queue in the ticketing system — approval required: only for compliance-flagged records | v1.0 | contracts/routing-port.md |
|
|
21
|
+
| PersistencePort | secondary | append the triage decision to an immutable log, keyed by request ID | v1.0 | contracts/persistence-port.md |
|
|
22
|
+
|
|
23
|
+
The output contract is the **TriageRecord v1.0** schema: closed category vocabulary (`support`, `sales`, `compliance`, `unknown`), extracted fields each carrying a provenance marker (`computed`, `verified`, `model-proposed`), target queue, confidence level. The contract is versioned and evolves additively; consumers are tolerant readers. A record that fails schema validation is rejected fail-closed, never repaired silently.
|
|
24
|
+
|
|
25
|
+
## 4. Default topology and triggers
|
|
26
|
+
|
|
27
|
+
| Default | Rationale | Evolution trigger |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Modular hexagonal monolith | one deployable; pure domain plus adapters; 400 req/week needs no distribution | throughput or availability measurably insufficient; multi-instance required |
|
|
30
|
+
| Single orchestrator (ADR-0001) | triage is a short dependent sequence; composes, carries no business logic | a genuinely parallelizable or isolation-requiring subtask appears |
|
|
31
|
+
| Tool registry + middleware chain | single transversal surface for logging, access, cost, approval | — |
|
|
32
|
+
| One core language, thin model SDK | no heavy chain framework; the model is one port among four | mature library or proven performance need → sidecar behind the same port |
|
|
33
|
+
| Deterministic guard before RoutingPort (ADR-0002) | no model-proposed value acts unverified | — (a floor invariant, not a default to outgrow) |
|
|
34
|
+
|
|
35
|
+
## Rule conformance
|
|
36
|
+
|
|
37
|
+
| Rule | Status | Evidence |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| contracts-governance | applied | §3 TriageRecord v1.0 — versioned, additive, tolerant reader, fail-closed; contracts/ |
|
|
40
|
+
| hexa-architecture | applied | §2 pure triage domain, four ports; code/src/core/ |
|
|
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-0003 pending) |
|
|
43
|
+
| process-adr-and-journal | applied | adr/ADR-0001, adr/ADR-0002 — dated decisions with reevaluation triggers |
|
|
44
|
+
| security-mcp-server-pinning | n/a | the floor consumes no MCP or external tool server; tools are in-process and deterministic |
|
|
45
|
+
|
|
46
|
+
## 5. What stays open
|
|
47
|
+
|
|
48
|
+
Language, web framework, model provider, and hosting are explicitly undecided at this note's version. Each is an adapter decision, taken behind the contracts above and justified by a local technical reason — the core language will be locked in its own ADR at floor kickoff, chosen for team fluency and SDK maturity, not for the domain, which is language-agnostic by construction.
|
|
49
|
+
|
|
50
|
+
## 6. Legacy integration
|
|
51
|
+
|
|
52
|
+
The ticketing system is the system of record and predates this mission. An anticorruption adapter implements RoutingPort and translates the domain's `TriageRecord` into the ticketing API's dialect (its queue identifiers, its custom-field encoding, its pagination quirks). Named cost: the translation table must be maintained when the ticketing configuration changes, and queue resolution requires one extra lookup per routing. Accepted — the alternative is the ticketing dialect leaking into the domain.
|
|
53
|
+
|
|
54
|
+
## 7. Target, named
|
|
55
|
+
|
|
56
|
+
Per the framing note: auto-drafted acknowledgments under approval, priority scoring, requester memory, and a reassignment-to-evaluation feedback loop. Each would enter as a new adapter behind a new or existing port, without rewriting the core. Named for direction, not built.
|
|
57
|
+
|
|
58
|
+
## 8. Decisions
|
|
59
|
+
|
|
60
|
+
| Decision | ADR |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Single orchestrator, sequential triage | [ADR-0001](adr/ADR-0001-single-orchestrator.md) |
|
|
63
|
+
| Deterministic guard on extracted fields | [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md) |
|
|
64
|
+
| Core language (open — to be locked at floor kickoff) | ADR-0003 (pending) |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Port Contract: ModelPort
|
|
2
|
+
|
|
3
|
+
## Port: ModelPort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: secondary (driven by the domain)
|
|
7
|
+
**Known adapters**: approved-deployment gateway adapter; deterministic keyword fallback classifier
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Propose a classification and key-field extractions for one raw request text. The output is a **hypothesis, never a fact**: every proposed value is marked `model-proposed` and nothing downstream may act on it before the deterministic guard (ADR-0002). The reasoning engine is bound by this contract, not by its brand — the fallback classifier honors the same contract with lower recall.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
propose(requestText) -> triageProposal — not idempotent, sync, no approval (proposes only, acts on nothing)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Input schema
|
|
20
|
+
|
|
21
|
+
| Field | Type | Required | Constraint |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| requestId | uuid | yes | propagated for tracing |
|
|
24
|
+
| requestText | string | yes | subject + body, untrusted; passed as data inside a fixed instruction frame |
|
|
25
|
+
|
|
26
|
+
## Output schema
|
|
27
|
+
|
|
28
|
+
| Field | Type | Always present | Constraint |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| category | enum | yes | closed vocabulary: `support`, `sales`, `compliance`, `unknown` |
|
|
31
|
+
| fields | list | yes | may be empty; each entry: name, proposed value, source span in the text |
|
|
32
|
+
| confidence | enum | yes | `high`, `medium`, `low` |
|
|
33
|
+
|
|
34
|
+
## Invariants
|
|
35
|
+
|
|
36
|
+
- Every proposed field carries the span of source text it was read from — a value with no span is rejected at the schema.
|
|
37
|
+
- The category never leaves the closed vocabulary; anything else is a validation failure, not a new category.
|
|
38
|
+
- All output is provenance-marked `model-proposed`; this port cannot produce `verified` or `computed` values.
|
|
39
|
+
|
|
40
|
+
## Errors
|
|
41
|
+
|
|
42
|
+
| Error | Type | Meaning for the consumer |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| timeout / overload | transient | retry with bounded backoff, then fall back |
|
|
45
|
+
| non-conforming output | validation | single retry with the diagnostic fed back; then treat as `unknown`, low confidence |
|
|
46
|
+
| gateway unavailable | unavailable | switch to the fallback adapter behind this same port; the domain does not notice |
|
|
47
|
+
|
|
48
|
+
## Evolution rule
|
|
49
|
+
|
|
50
|
+
Versioned; the category vocabulary belongs to the TriageRecord contract — extending it (e.g. v1.1 for the unknown-rate trigger, floor.md §5) is a governed, additive contract change gated by a labeled sample, never a free prompt edit.
|
|
51
|
+
|
|
52
|
+
## References
|
|
53
|
+
|
|
54
|
+
- [../adr/ADR-0002-deterministic-guard-on-extracted-fields.md](../adr/ADR-0002-deterministic-guard-on-extracted-fields.md) — why this port's output never acts unguarded.
|
|
55
|
+
- [../governance/evaluation-rubric.md](../governance/evaluation-rubric.md) — how this port's behavior is evaluated.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Port Contract: PersistencePort
|
|
2
|
+
|
|
3
|
+
## Port: PersistencePort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: secondary (driven by the domain)
|
|
7
|
+
**Known adapters**: append-only store adapter
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Append every triage decision to an immutable log, keyed by request ID. The log is the system's truth: audit, replay and recovery all read from it; queue state and metrics are derived views. Nothing is ever updated or deleted — corrections are new entries that reference what they correct.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
append(triageDecision) -> logPosition — idempotent on (requestId, step), sync
|
|
17
|
+
readTrajectory(requestId) -> decisionList — idempotent, sync, read-only
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Input schema
|
|
21
|
+
|
|
22
|
+
| Field | Type | Required | Constraint |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| requestId | uuid | yes | one trajectory per request |
|
|
25
|
+
| step | enum | yes | `intake`, `proposal`, `guard`, `routing`, `escalation` |
|
|
26
|
+
| payload | object | yes | the step's full record, provenance markers included |
|
|
27
|
+
| recordedAt | timestamp | yes | UTC |
|
|
28
|
+
|
|
29
|
+
## Output schema
|
|
30
|
+
|
|
31
|
+
| Field | Type | Always present | Constraint |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| logPosition | integer | yes | strictly increasing; gap-free per trajectory |
|
|
34
|
+
|
|
35
|
+
## Invariants
|
|
36
|
+
|
|
37
|
+
- Append-only: no operation on this port mutates or removes an existing entry.
|
|
38
|
+
- A read never mutates state.
|
|
39
|
+
- A trajectory replays in order and completely from its requestId alone — the property verified at the floor gate (floor.md §2, observability check).
|
|
40
|
+
|
|
41
|
+
## Errors
|
|
42
|
+
|
|
43
|
+
| Error | Type | Meaning for the consumer |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| store unavailable | unavailable | fail closed for action-bearing steps: a decision that cannot be persisted does not act (routing blocks); intake buffers at the channel |
|
|
46
|
+
| idempotency conflict | business | same (requestId, step) with different payload — a bug upstream; the entry is refused and the run escalates |
|
|
47
|
+
|
|
48
|
+
## Evolution rule
|
|
49
|
+
|
|
50
|
+
Versioned; additive by default — new step types and optional payload fields extend the enum and schema without breaking readers, which tolerate unknown steps. Migrations are forward-only; the log is never rewritten.
|
|
51
|
+
|
|
52
|
+
## References
|
|
53
|
+
|
|
54
|
+
- [../governance/observability-schema.md](../governance/observability-schema.md) — lifecycle events persisted through this port.
|
|
55
|
+
- [../runbook.md](../runbook.md) §3 — recovery replays this log, never the model.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Port Contract: RequestIntakePort
|
|
2
|
+
|
|
3
|
+
## Port: RequestIntakePort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: primary (drives the domain)
|
|
7
|
+
**Known adapters**: mailbox adapter, web-form adapter
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Deliver one raw inbound request into the triage domain, whatever channel it arrived on. A "request" here is a single message from one requester asking the organization for something — the channel's envelope (mail headers, form metadata) is normalized at this boundary and never leaks into the domain.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
submit(rawRequest) -> intakeReceipt — idempotent on sourceMessageId, sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Input schema
|
|
20
|
+
|
|
21
|
+
| Field | Type | Required | Constraint |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| source | enum | yes | `mailbox` or `web-form` |
|
|
24
|
+
| sourceMessageId | string | yes | channel-native ID; idempotency key |
|
|
25
|
+
| receivedAt | timestamp | yes | channel receipt time, UTC |
|
|
26
|
+
| senderAddress | string | yes | as given by the channel; verified later by the guard, not here |
|
|
27
|
+
| subject | string | no | absent for some form submissions |
|
|
28
|
+
| body | string | yes | raw free text, untrusted by definition |
|
|
29
|
+
|
|
30
|
+
## Output schema
|
|
31
|
+
|
|
32
|
+
| Field | Type | Always present | Constraint |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| requestId | uuid | yes | unique; the ID propagated through the whole trajectory |
|
|
35
|
+
| accepted | boolean | yes | false only on validation rejection |
|
|
36
|
+
|
|
37
|
+
## Invariants
|
|
38
|
+
|
|
39
|
+
- Every accepted request yields exactly one requestId; resubmitting the same sourceMessageId returns the same one.
|
|
40
|
+
- Intake never mutates the source channel and never triages — it accepts and hands over.
|
|
41
|
+
- The body is stored verbatim; normalization is recorded as derived data, never destructive.
|
|
42
|
+
|
|
43
|
+
## Errors
|
|
44
|
+
|
|
45
|
+
| Error | Type | Meaning for the consumer |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| malformed payload | validation | rejected at the boundary; the channel adapter keeps the original for manual pickup |
|
|
48
|
+
| duplicate sourceMessageId | business | not an error — same receipt returned (idempotent) |
|
|
49
|
+
| store unavailable | unavailable | fail closed: the adapter retries from the channel; nothing is acknowledged unpersisted |
|
|
50
|
+
|
|
51
|
+
## Evolution rule
|
|
52
|
+
|
|
53
|
+
Versioned; additive by default (new optional fields, tolerant readers); a breaking change goes expand-then-contract across both channel adapters before the old shape retires.
|
|
54
|
+
|
|
55
|
+
## References
|
|
56
|
+
|
|
57
|
+
- [../architecture.md](../architecture.md) §3 — port table.
|
|
58
|
+
- [model-port.md](model-port.md) — next step in the fixed sequential plan.
|