Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d642334
feat(catalog): add graph hash module for content-based deduplication
senamakel Sep 30, 2026
66ad279
chore: files changed Cargo.lock,crates/tinyflows-catalog/src/graph_ha…
senamakel Sep 30, 2026
6af032c
chore: files changed crates/tinyflows-catalog/src/graph_hash_tests.rs
senamakel Sep 30, 2026
0f21663
chore: files changed crates/tinyflows-catalog/src/graph_hash_tests.rs…
senamakel Sep 30, 2026
714c960
feat(copilot): add trail_off module and update changelog
senamakel Sep 30, 2026
2d7a73b
fix(schedule): correct gap detection for overlapping time windows
senamakel Sep 30, 2026
e496895
feat(schedule): add cron-based scheduling with timezone support
senamakel Sep 30, 2026
5aca914
docs(schedule): changelog and crate map for tinyflows-schedule
senamakel Sep 30, 2026
b88f5d0
refactor(trail-off): track code-span state across paragraph boundary
senamakel Sep 30, 2026
00d2922
feat(schedule): validate every intervals that cannot advance
senamakel Sep 30, 2026
e0d2768
fix: correct paragraph start detection when text contains blank code …
senamakel Sep 30, 2026
a7a0e84
style: reformat code blocks and reorder imports for consistency
senamakel Sep 30, 2026
1ff7e67
fix: remove redundant variable shadow in trail_off.rs
senamakel Sep 30, 2026
66dc1e4
fix(schedule): raise candidate limits to cover annual DST transitions
senamakel Sep 30, 2026
9b6fd12
fix(trail_off): avoid panic on CRLF input with multi-line paragraph
senamakel Sep 30, 2026
a04a4c5
Merge pull request #98 from tinyhumansai/oh-extract-cron
senamakel Sep 30, 2026
5119907
fix(trail_off): treat tilde-fenced code blocks as code context
senamakel Sep 30, 2026
7053f9b
style: format control flow blocks with consistent braces
senamakel Sep 30, 2026
71dd76d
fix: preserve malformed graph collections during probing
senamakel Sep 30, 2026
f0991bb
refactor(tests): extract router and prompt compatibility tests into s…
senamakel Sep 30, 2026
6f4ee4a
feat(migrate): prefer malformed collection error over member error
senamakel Sep 30, 2026
9d65a28
merge: resolve compat router test conflict
senamakel Sep 30, 2026
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`crates/tinyflows-schedule`** — the schedule model (`Schedule`,
`ActiveHours`, the job/run records) and its pure logic: cron-expression
normalisation, time-zone and active-window aware next-run computation,
validation and the minimum-cadence check. Extracted from OpenHuman's `cron`
domain; the serde shapes are byte-identical and pinned by literal-JSON
fixtures. No runtime, store or config.
- **`tinyflows_catalog::graph_hash::compute_graph_hash`** — the content pin over
a graph plus its `require_approval` flag that a parked run records and a
resume re-checks. Extracted from OpenHuman; the digest is byte-identical to
what that host persisted, and a fixed-vector test keeps it so.
- **`tinyflows_copilot::trail_off`** — `text_looks_like_question` and the
code-span / URL / paragraph heuristics behind the builder-convergence
question backstop.
- A nested inline `sub_workflow` case in the engine-compatibility tests.

### Changed

- **The repository is a virtual workspace; every crate lives under `crates/`.**
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ uses, so a reader who knows one knows them all.
| `crates/tinyflows-sqlite` | SQLite implementations of the catalog and of the engine's checkpoint store, plus the JSON draft store. Every entry point takes a directory, never a host config type. |
| `crates/tinyflows-copilot` | The authoring copilot's *words*: the `workflow_builder` / `flow_discovery` standing archetypes, and the turn brief that opens a builder turn. Names no tool trait, no agent registry, no model client. |
| `crates/tinyflows-adaptive` | The adaptive loop over the engine: select or author a workflow, run it, judge it, learn. |
| `crates/tinyflows-schedule` | Schedule model and pure next-run logic (cron / interval / one-shot, time zones, active hours, cadence floor). No runtime, store or config. |

**Where a new thing goes.** Ask what it depends on, not what it is about. If it
needs storage, it is not `tinyflows-catalog`. If it needs a tool trait or a model
Expand Down
76 changes: 76 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/tinyflows-catalog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ tokio-util = { version = "0.7", default-features = false }
# Diagnostics only: the registries and the importers log what they refuse and
# why. No subscriber is installed here — a host that installs none pays nothing.
tracing = { workspace = true }
# `graph_hash` only: the SHA-256 that pins a graph's content for approval
# resume. Hex-encoded, so the digest is the same string a host persisted before
# this moved here.
sha2 = { workspace = true }
hex = "0.4"

[dev-dependencies]
serde_json = { workspace = true }
81 changes: 81 additions & 0 deletions crates/tinyflows-catalog/src/graph_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! A stable content hash over a graph and its approval flag.
//!
//! A run that parks for approval records this hash; when it is resumed the host
//! recomputes it and refuses to continue if the graph (or the approval setting)
//! changed while the run was parked. Because the value is persisted, the digest
//! is a compatibility surface: it must stay byte-for-byte identical across
//! releases, so the tests pin a fixed vector.

use serde_json::Value;
use sha2::{Digest, Sha256};
use tinyflows::model::WorkflowGraph;

/// Hashes `graph` together with `require_approval` into a lowercase hex SHA-256.
///
/// The hash is over graph *content*, never incidental key order: the graph is
/// serialized to a JSON value and every object's keys are recursively sorted
/// before the value is rendered, so two graphs that differ only in the order
/// `serde_json` happened to emit their object keys hash identically. Array
/// order is preserved because it is semantically meaningful.
///
/// Returns `None` (never panics) when the graph fails to serialize. The two
/// sides of a resume treat `None` differently: at park time it simply stores no
/// pin, so the run later takes the legacy "unknown, allow with a warning" path;
/// at resume time `Some(expected) != None` is a mismatch, so the run is refused.
/// A hash failure therefore fails closed on resume.
#[must_use]
pub fn compute_graph_hash(graph: &WorkflowGraph, require_approval: bool) -> Option<String> {
let raw = match serde_json::to_value(graph) {
Ok(v) => v,
Err(e) => {
tracing::warn!(
target: "flows",
error = %e,
"[flows] compute_graph_hash: failed to serialize graph to JSON — proceeding without a graph pin"
);
return None;
}
};
let raw = serde_json::json!({ "graph": raw, "require_approval": require_approval });
let canonical = canonicalize_json(&raw);
let serialized = match serde_json::to_string(&canonical) {
Ok(s) => s,
Err(e) => {
tracing::warn!(
target: "flows",
error = %e,
"[flows] compute_graph_hash: failed to serialize canonicalized graph — proceeding without a graph pin"
);
return None;
}
};
let digest = Sha256::digest(serialized.as_bytes());
Some(hex::encode(digest))
}

/// Recursively rewrites every JSON object's keys into sorted order, leaving
/// arrays (whose element order is semantically meaningful) and scalars
/// unchanged.
///
/// Sorting is explicit rather than trusting `serde_json`'s default map order,
/// which is sorted only when the `preserve_order` feature is off; another crate
/// in the build enabling it would otherwise change every persisted hash.
fn canonicalize_json(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let mut sorted = serde_json::Map::new();
for key in keys {
sorted.insert(key.clone(), canonicalize_json(&map[key]));
}
Value::Object(sorted)
}
Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()),
other => other.clone(),
}
}

#[cfg(test)]
#[path = "graph_hash_tests.rs"]
mod tests;
Loading
Loading