refactor: extract workflow crate

This commit is contained in:
2026-05-11 22:49:07 +09:00
parent 520895f1c9
commit f70975789e
25 changed files with 686 additions and 311 deletions
+4 -11
View File
@@ -1,5 +1,4 @@
//! Walks `<workspace>/memory/{decisions,requests}/`,
//! `<workspace>/workflow/`, and `<workspace>/knowledge/` to collect
//! Walks `<workspace>/memory/{decisions,requests}/` and `<workspace>/knowledge/` to collect
//! the slug set the linter needs for reference-integrity and
//! same-slug-duplication checks.
//!
@@ -11,8 +10,7 @@ use std::io;
use std::path::Path;
use crate::schema::{
DecisionFrontmatter, KnowledgeFrontmatter, RequestFrontmatter, WorkflowFrontmatter,
split_frontmatter,
DecisionFrontmatter, KnowledgeFrontmatter, RequestFrontmatter, split_frontmatter,
};
use crate::slug::Slug;
use crate::workspace::{RecordKind, WorkspaceLayout};
@@ -28,7 +26,6 @@ pub struct ExistingRecords {
decisions: HashMap<Slug, DecisionMeta>,
requests: HashSet<Slug>,
knowledge: HashSet<Slug>,
workflow: HashSet<Slug>,
}
#[derive(Debug, Clone)]
@@ -42,7 +39,7 @@ impl ExistingRecords {
RecordKind::Decision => self.decisions.contains_key(slug),
RecordKind::Request => self.requests.contains(slug),
RecordKind::Knowledge => self.knowledge.contains(slug),
RecordKind::Workflow => self.workflow.contains(slug),
RecordKind::Workflow => false,
RecordKind::Summary => false,
}
}
@@ -56,7 +53,7 @@ impl ExistingRecords {
RecordKind::Decision => self.decisions.keys().collect(),
RecordKind::Request => self.requests.iter().collect(),
RecordKind::Knowledge => self.knowledge.iter().collect(),
RecordKind::Workflow => self.workflow.iter().collect(),
RecordKind::Workflow => Vec::new(),
RecordKind::Summary => Vec::new(),
}
}
@@ -82,10 +79,6 @@ pub fn scan_existing(layout: &WorkspaceLayout) -> io::Result<ExistingRecords> {
let _ = parse_silent::<KnowledgeFrontmatter>(path);
out.knowledge.insert(slug);
})?;
scan_dir(&layout.workflow_dir(), |path, slug| {
let _ = parse_silent::<WorkflowFrontmatter>(path);
out.workflow.insert(slug);
})?;
Ok(out)
}
+4 -155
View File
@@ -23,9 +23,8 @@ use serde::de::DeserializeOwned;
use crate::error::{LintError, LintWarning};
use crate::schema::{
DecisionFrontmatter, KnowledgeFrontmatter, RequestFrontmatter, SummaryFrontmatter,
WorkflowFrontmatter, split_frontmatter,
split_frontmatter,
};
use crate::workflow::WORKFLOW_DESCRIPTION_HARD_CAP;
use crate::workspace::{ClassifiedPath, RecordKind, WorkspaceLayout};
pub use existing::{ExistingRecords, scan_existing};
@@ -99,12 +98,6 @@ impl Linter {
}
};
// 2. Workflow paths are sub-Worker-forbidden at the tool layer.
if classified.kind == RecordKind::Workflow {
report.push_error(LintError::WorkflowWriteForbidden);
return report;
}
// 3. Frontmatter parse + kind-specific structural checks +
// size limits. Reference-integrity needs the existing
// record set, fetched once below.
@@ -146,7 +139,9 @@ impl Linter {
RecordKind::Summary => {
self.check_kind::<SummaryFrontmatter>(content, &classified, &mut report);
}
RecordKind::Workflow => unreachable!("guarded above"),
RecordKind::Workflow => {
unreachable!("workflow paths are not classified by memory linter")
}
}
report
@@ -240,59 +235,6 @@ impl Linter {
}
}
impl Linter {
/// Workflow record validator exposed for human-edit paths
/// (CLI / pre-commit). Not used by the memory tool, which rejects
/// workflow writes outright.
///
/// Verifies frontmatter shape, body size, and that every slug in
/// `requires` points at an existing Knowledge record under the
/// workspace's `knowledge/` directory.
pub fn lint_workflow(&self, content: &str) -> LintReport {
let mut report = LintReport::default();
let parsed = match parse_frontmatter::<WorkflowFrontmatter>(content) {
Ok(p) => p,
Err(e) => {
report.push_error(e);
return report;
}
};
size::check_body::<WorkflowFrontmatter>(parsed.body, &mut report);
// Mirror the loader's cap so human-edit paths fail fast instead
// of surfacing the same error only at Pod startup.
if parsed.frontmatter.model_invokation {
let actual = parsed.frontmatter.description.chars().count();
if actual > WORKFLOW_DESCRIPTION_HARD_CAP {
report.push_error(LintError::DescriptionTooLong {
actual,
limit: WORKFLOW_DESCRIPTION_HARD_CAP,
});
}
}
let existing = match existing::scan_existing(&self.layout) {
Ok(e) => e,
Err(e) => {
report.push_error(LintError::MalformedFrontmatter(format!(
"failed to scan existing records: {e}"
)));
return report;
}
};
for slug in &parsed.frontmatter.requires {
if !existing.contains(crate::workspace::RecordKind::Knowledge, slug) {
report.push_error(LintError::UnknownReference {
field: "requires",
kind: "knowledge",
slug: slug.to_string(),
});
}
}
report
}
}
struct Parsed<'a, F> {
frontmatter: F,
body: &'a str,
@@ -332,22 +274,6 @@ mod tests {
(dir, linter)
}
#[test]
fn workflow_write_rejected() {
let (dir, linter) = workspace();
let path = dir.path().join(".insomnia/workflow/wf.md");
let content =
"---\ndescription: x\nmodel_invokation: false\nuser_invocable: true\n---\nbody"
.to_string();
let report = linter.lint(&path, &content, WriteMode::Create);
assert!(
report
.errors
.iter()
.any(|e| matches!(e, LintError::WorkflowWriteForbidden))
);
}
#[test]
fn outside_memory_tree_rejected() {
let (dir, linter) = workspace();
@@ -499,83 +425,6 @@ mod tests {
);
}
#[test]
fn workflow_lint_accepts_valid_record() {
let (dir, linter) = workspace();
// Place a Knowledge record that the workflow will reference.
let kn = dir.path().join(".insomnia/knowledge/foo.md");
write(
&kn,
&format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nkind: rule\ndescription: x\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\n",
n = iso_now()
),
);
let wf = "---\ndescription: do thing\nmodel_invokation: false\nuser_invocable: true\nrequires: [foo]\n---\nstep 1\n".to_string();
let report = linter.lint_workflow(&wf);
assert!(!report.has_errors(), "got errors: {:?}", report.errors);
}
#[test]
fn workflow_lint_flags_unknown_requires() {
let (_dir, linter) = workspace();
let wf = "---\ndescription: x\nmodel_invokation: false\nuser_invocable: true\nrequires: [missing-knowledge]\n---\n".to_string();
let report = linter.lint_workflow(&wf);
assert!(report.errors.iter().any(|e| matches!(
e,
LintError::UnknownReference {
field: "requires",
kind: "knowledge",
..
}
)));
}
#[test]
fn workflow_lint_flags_long_description_when_model_invokation() {
let (_dir, linter) = workspace();
let desc = "x".repeat(crate::workflow::WORKFLOW_DESCRIPTION_HARD_CAP + 1);
let wf = format!(
"---\ndescription: {desc}\nmodel_invokation: true\nuser_invocable: true\n---\n"
);
let report = linter.lint_workflow(&wf);
assert!(
report
.errors
.iter()
.any(|e| matches!(e, LintError::DescriptionTooLong { .. })),
);
}
#[test]
fn workflow_lint_allows_long_description_when_not_model_invokation() {
let (_dir, linter) = workspace();
let desc = "x".repeat(crate::workflow::WORKFLOW_DESCRIPTION_HARD_CAP + 1);
let wf = format!(
"---\ndescription: {desc}\nmodel_invokation: false\nuser_invocable: true\n---\n"
);
let report = linter.lint_workflow(&wf);
assert!(
!report
.errors
.iter()
.any(|e| matches!(e, LintError::DescriptionTooLong { .. })),
);
}
#[test]
fn workflow_lint_collects_multiple_unknown_requires() {
let (_dir, linter) = workspace();
let wf = "---\ndescription: x\nmodel_invokation: false\nuser_invocable: true\nrequires: [a, b, c]\n---\n".to_string();
let report = linter.lint_workflow(&wf);
let unknown_count = report
.errors
.iter()
.filter(|e| matches!(e, LintError::UnknownReference { .. }))
.count();
assert_eq!(unknown_count, 3);
}
#[test]
fn similar_slugs_warns_on_cluster() {
let (dir, linter) = workspace();
-4
View File
@@ -1,8 +1,4 @@
//! Reference-integrity checks: `replaced_by` existence + cycle detection.
//!
//! `requires` (Workflow) is checked symmetrically when/if the Workflow
//! linter is invoked from a human-edit path; the memory tool itself
//! never writes Workflow records.
use std::collections::HashSet;