Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions specs/port/trios-railway/crates/trios-railway-experience/src/lib.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// The append-only experience writer for .trinity/experience/.
// Port of gHashTag/trios-railway:crates/trios-railway-experience/src/lib.rs
// (at 8b229e9489ee, 140+ lines) — Roadmap Stage 6, goal #4548, target t27c
// gen-rust.
//
// The original enforces L7 (the experience log) and L21 (context
// immutability): the writer only ever opens the daily file in append mode
// and never seeks or truncates — a caller that would truncate has no
// method to call. An ExperienceLine is built from a RailwayHash only when
// its phi_step is one of the eleven allowed PHI LOOP steps (mirroring
// NOW.json); any other step is rejected. The file layout is fixed: the
// experience directory under the root, the daily file named
// trios_railway_<YYYYMMDD>.trinity, one serialized JSON line per append,
// each with a trailing newline.
//
// The filesystem, the clock, the JSON serializer and the RailwayHash are
// plumbing; the decisions — the eleven-step catalogue and the membership
// gate, the line's built/invalid verdict with its carried fields and the
// absent evidence, and the file layout rules with the append-only
// contract — are ported here as pure functions and tested, including the
// original's own rejects_unknown_phi_step test. The clock and the hash
// triplet arrive as parameters (Utc::now and hash.triplet() are the
// caller's plumbing), and the append_line transport keeps its name with
// an `undefined;` body that no test calls (issue #9769, "Port the
// decision, not the plumbing").

module port::trios_railway_experience_lib;

// ---------------------------------------------------------------------------
// Original lines 18-31: `pub const PHI_STEPS` — the eleven allowed PHI
// LOOP steps, mirroring NOW.json.
// ---------------------------------------------------------------------------

pub fn phi_step_count() -> usize {
return 11;
}

pub fn phi_step_in_order(index: usize) -> str {
if (index == 0) { return "CLAIM"; }
if (index == 1) { return "NAME"; }
if (index == 2) { return "SPEC"; }
if (index == 3) { return "SEAL"; }
if (index == 4) { return "GEN"; }
if (index == 5) { return "TEST"; }
if (index == 6) { return "VERDICT"; }
if (index == 7) { return "EXPERIENCE"; }
if (index == 8) { return "REPORT"; }
if (index == 9) { return "COMMIT"; }
if (index == 10) { return "PUSH"; }
return "";
}

// Original line 57: `if !PHI_STEPS.contains(&phi_step) { bail!(...) }` —
// the membership gate.
pub fn is_valid_phi_step(step: str) -> bool {
var i: usize = 0;
while (i < phi_step_count()) {
if (step == phi_step_in_order(i)) {
return true;
}
i += 1;
}
return false;
}

// ---------------------------------------------------------------------------
// Original lines 33-72: `pub struct ExperienceLine` and
// `pub fn from_hash(...) -> Result<Self>`. The line carries the audit
// facts; the evidence is None (skip_serializing_if) — carried as the
// absent flag. The clock (Utc::now) and the hash triplet arrive as
// parameters; the built flag is the Ok/bail verdict.
// ---------------------------------------------------------------------------

pub struct ExperienceLine {
built: bool,
ts: str,
agent: str,
soul_name: str,
issue: str,
task: str,
status: str,
phi_step: str,
triplet: str,
evidence_present: bool,
}

pub fn ExperienceLine_from_hash(
agent: str,
soul_name: str,
issue: str,
task: str,
status: str,
phi_step: str,
triplet: str,
ts: str,
) -> ExperienceLine {
if (!is_valid_phi_step(phi_step)) {
return ExperienceLine {
built: false,
ts: "",
agent: "",
soul_name: "",
issue: "",
task: "",
status: "",
phi_step: phi_step,
triplet: "",
evidence_present: false,
};
}
return ExperienceLine {
built: true,
ts: ts,
agent: agent,
soul_name: soul_name,
issue: issue,
task: task,
status: status,
phi_step: phi_step,
triplet: triplet,
evidence_present: false,
};
}

// ---------------------------------------------------------------------------
// Original lines 74-98: `pub async fn append_line(root, line) ->
// Result<PathBuf>`. The append-only writer: creates the experience
// directory, opens the daily file with create(true).append(true) — never
// truncating, per L21 — writes the serialized line with its trailing
// newline, and returns the path. Pure transport; the layout rules it
// fixes are the decisions, ported below.
// ---------------------------------------------------------------------------

pub fn append_line(root: str, day: str) -> ?str {
return undefined; // plumbing: append-only write to the daily experience file (lines 74-98)
}

pub fn experience_dir_name() -> str {
return "experience";
}

pub fn daily_file_prefix() -> str {
return "trios_railway_";
}

pub fn daily_file_suffix() -> str {
return ".trinity";
}

pub fn day_format() -> str {
return "%Y%m%d";
}

// Original lines 88-90: `OpenOptions::new().create(true).append(true)` —
// append-only, never truncate (L21: no truncating method exists).
pub fn opens_append_only() -> bool {
return true;
}

// Original line 86: `serialized.push('\n')` — every appended line ends
// with a newline, so the file is line-per-record.
pub fn line_ends_with_newline() -> bool {
return true;
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original's own tests (lines 105-140). No test calls the append_line
// transport.
// ---------------------------------------------------------------------------

test valid_step_builds_the_line {
const line = ExperienceLine_from_hash("GENERAL", "DustyDeployer", "#1", "bootstrap", "OK", "EXPERIENCE", "triplet-x", "2026-10-17T02:39:00Z");
assert(line.built);
assert(line.agent == "GENERAL");
assert(line.soul_name == "DustyDeployer");
assert(line.issue == "#1");
assert(line.task == "bootstrap");
assert(line.status == "OK");
assert(line.phi_step == "EXPERIENCE");
assert(line.triplet == "triplet-x");
assert(!line.evidence_present);
}

test rejects_unknown_phi_step {
const line = ExperienceLine_from_hash("GENERAL", "DustyDeployer", "#1", "bootstrap", "OK", "RUN", "triplet-x", "2026-10-17T02:39:00Z");
assert(!line.built);
}

test phi_steps_catalog {
assert(phi_step_count() == 11);
assert(phi_step_in_order(0) == "CLAIM");
assert(phi_step_in_order(6) == "VERDICT");
assert(phi_step_in_order(7) == "EXPERIENCE");
assert(phi_step_in_order(10) == "PUSH");
assert(phi_step_in_order(11) == "");
}

test membership_gate {
assert(is_valid_phi_step("CLAIM"));
assert(is_valid_phi_step("EXPERIENCE"));
assert(is_valid_phi_step("COMMIT"));
assert(is_valid_phi_step("PUSH"));
assert(!is_valid_phi_step("RUN"));
assert(!is_valid_phi_step(""));
}

test daily_file_layout_and_append_contract {
assert(experience_dir_name() == "experience");
assert(daily_file_prefix() == "trios_railway_");
assert(daily_file_suffix() == ".trinity");
assert(day_format() == "%Y%m%d");
assert(opens_append_only());
assert(line_ends_with_newline());
}
Loading