objective: remove one-shot import
This commit is contained in:
parent
e653f8f08b
commit
0eb840ab4b
|
|
@ -17,8 +17,8 @@ const DETAIL_BODY_LIMIT: usize = 64 * 1024;
|
||||||
/// Workspace-scoped runtime authority for project resources.
|
/// Workspace-scoped runtime authority for project resources.
|
||||||
///
|
///
|
||||||
/// Normal Backend API handlers should depend on this authority abstraction, not
|
/// Normal Backend API handlers should depend on this authority abstraction, not
|
||||||
/// on legacy filesystem layouts. Filesystem readers belong in explicit import
|
/// on legacy filesystem layouts. Filesystem readers belong in explicitly
|
||||||
/// / migration helpers such as `objective_import`.
|
/// temporary migration/repair tools and tests, not normal runtime authority paths.
|
||||||
pub trait WorkspaceAuthority: ObjectiveAuthority + TicketAuthority + MemoryAuthorityMarker {}
|
pub trait WorkspaceAuthority: ObjectiveAuthority + TicketAuthority + MemoryAuthorityMarker {}
|
||||||
|
|
||||||
impl<T> WorkspaceAuthority for T where
|
impl<T> WorkspaceAuthority for T where
|
||||||
|
|
@ -216,7 +216,7 @@ mod tests {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::store::WorkspaceRecord;
|
use crate::store::{ObjectiveRecord, ObjectiveTicketLinkRecord, WorkspaceRecord};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn sqlite_workspace_authority_reads_sqlite_records_without_filesystem_authority() {
|
async fn sqlite_workspace_authority_reads_sqlite_records_without_filesystem_authority() {
|
||||||
|
|
@ -228,7 +228,6 @@ mod tests {
|
||||||
dir.path().join(".yoi/tickets"),
|
dir.path().join(".yoi/tickets"),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
write_objective(dir.path(), "00000000001J3", "Control plane", "active");
|
|
||||||
let store = SqliteWorkspaceStore::open(&db_path).unwrap();
|
let store = SqliteWorkspaceStore::open(&db_path).unwrap();
|
||||||
store
|
store
|
||||||
.upsert_workspace(&WorkspaceRecord {
|
.upsert_workspace(&WorkspaceRecord {
|
||||||
|
|
@ -241,12 +240,30 @@ mod tests {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
crate::objective_import::import_legacy_objectives_and_memory_staging(
|
store
|
||||||
dir.path(),
|
.upsert_objective(&ObjectiveRecord {
|
||||||
"workspace-test",
|
workspace_id: "workspace-test".to_string(),
|
||||||
&store,
|
objective_id: "00000000001J3".to_string(),
|
||||||
)
|
title: "Control plane".to_string(),
|
||||||
.unwrap();
|
state: "active".to_string(),
|
||||||
|
body_md: "Objective body.\n".to_string(),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.replace_objective_ticket_links(
|
||||||
|
"workspace-test",
|
||||||
|
"00000000001J3",
|
||||||
|
&[ObjectiveTicketLinkRecord {
|
||||||
|
workspace_id: "workspace-test".to_string(),
|
||||||
|
objective_id: "00000000001J3".to_string(),
|
||||||
|
ticket_id: "00000000001J2".to_string(),
|
||||||
|
kind: "linked".to_string(),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
write_objective(
|
write_objective(
|
||||||
dir.path(),
|
dir.path(),
|
||||||
"00000000001J4",
|
"00000000001J4",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ pub mod config;
|
||||||
pub mod hosts;
|
pub mod hosts;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod memory_staging;
|
pub mod memory_staging;
|
||||||
pub mod objective_import;
|
|
||||||
pub mod observation;
|
pub mod observation;
|
||||||
pub mod profile_settings;
|
pub mod profile_settings;
|
||||||
pub mod records;
|
pub mod records;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ enum Command {
|
||||||
Init(InitOptions),
|
Init(InitOptions),
|
||||||
ConfigDefault,
|
ConfigDefault,
|
||||||
ConfigDiff(WorkspacePathOptions),
|
ConfigDiff(WorkspacePathOptions),
|
||||||
ImportObjectives(ImportObjectivesOptions),
|
|
||||||
Skills(SkillsCommand),
|
Skills(SkillsCommand),
|
||||||
Help,
|
Help,
|
||||||
}
|
}
|
||||||
|
|
@ -37,12 +36,6 @@ struct WorkspacePathOptions {
|
||||||
workspace: PathBuf,
|
workspace: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ImportObjectivesOptions {
|
|
||||||
workspace: PathBuf,
|
|
||||||
database: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum SkillsCommand {
|
enum SkillsCommand {
|
||||||
List(WorkspacePathOptions),
|
List(WorkspacePathOptions),
|
||||||
|
|
@ -79,7 +72,6 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Command::Init(options) => run_init(options).await,
|
Command::Init(options) => run_init(options).await,
|
||||||
Command::ConfigDefault => run_config_default(),
|
Command::ConfigDefault => run_config_default(),
|
||||||
Command::ConfigDiff(options) => run_config_diff(options),
|
Command::ConfigDiff(options) => run_config_diff(options),
|
||||||
Command::ImportObjectives(options) => run_import_objectives(options).await,
|
|
||||||
Command::Skills(command) => run_skills(command),
|
Command::Skills(command) => run_skills(command),
|
||||||
Command::Help => Ok(()),
|
Command::Help => Ok(()),
|
||||||
}
|
}
|
||||||
|
|
@ -100,15 +92,6 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
|
||||||
Ok(Command::Init(parse_init_options(rest)?))
|
Ok(Command::Init(parse_init_options(rest)?))
|
||||||
}
|
}
|
||||||
"config" => parse_config_command(rest),
|
"config" => parse_config_command(rest),
|
||||||
"import-objectives" => {
|
|
||||||
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
|
||||||
print_import_objectives_help();
|
|
||||||
return Ok(Command::Help);
|
|
||||||
}
|
|
||||||
Ok(Command::ImportObjectives(parse_import_objectives_options(
|
|
||||||
rest,
|
|
||||||
)?))
|
|
||||||
}
|
|
||||||
"skills" => parse_skills_command(rest),
|
"skills" => parse_skills_command(rest),
|
||||||
"serve" => {
|
"serve" => {
|
||||||
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||||
|
|
@ -122,7 +105,7 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
|
||||||
Ok(Command::Help)
|
Ok(Command::Help)
|
||||||
}
|
}
|
||||||
other => Err(CliError(format!(
|
other => Err(CliError(format!(
|
||||||
"unknown command `{other}`; expected `init`, `config`, `import-objectives`, `skills`, or `serve`"
|
"unknown command `{other}`; expected `init`, `config`, `skills`, or `serve`"
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,45 +169,6 @@ fn run_config_diff(options: WorkspacePathOptions) -> Result<(), Box<dyn std::err
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_import_objectives(
|
|
||||||
options: ImportObjectivesOptions,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let identity = WorkspaceIdentity::load_required(&options.workspace)?;
|
|
||||||
let database_path = options
|
|
||||||
.database
|
|
||||||
.unwrap_or_else(ServerConfig::default_server_database_path);
|
|
||||||
let store = SqliteWorkspaceStore::open(&database_path)?;
|
|
||||||
store
|
|
||||||
.upsert_workspace(&WorkspaceRecord {
|
|
||||||
workspace_id: identity.workspace_id.clone(),
|
|
||||||
owner_account_id: None,
|
|
||||||
display_name: identity.display_name.clone(),
|
|
||||||
state: "active".to_string(),
|
|
||||||
created_at: identity.created_at.clone(),
|
|
||||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let report =
|
|
||||||
yoi_workspace_server::objective_import::import_legacy_objectives_and_memory_staging(
|
|
||||||
&options.workspace,
|
|
||||||
&identity.workspace_id,
|
|
||||||
&store,
|
|
||||||
)?;
|
|
||||||
println!(
|
|
||||||
"imported objectives={} resources={} ticket_links={} skipped_ticket_links={} memory_staging={} invalid_records={}",
|
|
||||||
report.objectives_imported,
|
|
||||||
report.objective_resources_imported,
|
|
||||||
report.objective_ticket_links_imported,
|
|
||||||
report.objective_ticket_links_skipped,
|
|
||||||
report.memory_staging_records_imported,
|
|
||||||
report.invalid_records.len(),
|
|
||||||
);
|
|
||||||
for invalid in report.invalid_records {
|
|
||||||
eprintln!("invalid objective record: {invalid}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_skills(command: SkillsCommand) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_skills(command: SkillsCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match command {
|
match command {
|
||||||
SkillsCommand::List(options) => {
|
SkillsCommand::List(options) => {
|
||||||
|
|
@ -433,47 +377,6 @@ fn parse_workspace_path_options(args: &[String]) -> Result<WorkspacePathOptions,
|
||||||
Ok(WorkspacePathOptions { workspace })
|
Ok(WorkspacePathOptions { workspace })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_import_objectives_options(args: &[String]) -> Result<ImportObjectivesOptions, CliError> {
|
|
||||||
let mut workspace = std::env::current_dir()
|
|
||||||
.map_err(|error| CliError(format!("failed to read current dir: {error}")))?;
|
|
||||||
let mut database = None;
|
|
||||||
let mut iter = args.iter();
|
|
||||||
while let Some(arg) = iter.next() {
|
|
||||||
match arg.as_str() {
|
|
||||||
"--workspace" => {
|
|
||||||
let value = iter
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| CliError("--workspace requires a path".to_string()))?;
|
|
||||||
workspace = PathBuf::from(value);
|
|
||||||
}
|
|
||||||
value if value.starts_with("--workspace=") => {
|
|
||||||
workspace = PathBuf::from(value_after_equals(arg, "--workspace")?);
|
|
||||||
}
|
|
||||||
"--database" => {
|
|
||||||
let value = iter
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| CliError("--database requires a path".to_string()))?;
|
|
||||||
database = Some(PathBuf::from(value));
|
|
||||||
}
|
|
||||||
value if value.starts_with("--database=") => {
|
|
||||||
database = Some(PathBuf::from(value_after_equals(arg, "--database")?));
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
return Err(CliError(format!(
|
|
||||||
"unknown import-objectives option `{other}`"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let workspace = workspace
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|error| CliError(format!("failed to canonicalize workspace: {error}")))?;
|
|
||||||
Ok(ImportObjectivesOptions {
|
|
||||||
workspace,
|
|
||||||
database,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_init_options(args: &[String]) -> Result<InitOptions, CliError> {
|
fn parse_init_options(args: &[String]) -> Result<InitOptions, CliError> {
|
||||||
let mut workspace = std::env::current_dir()
|
let mut workspace = std::env::current_dir()
|
||||||
.map_err(|error| CliError(format!("failed to read current dir: {error}")))?;
|
.map_err(|error| CliError(format!("failed to read current dir: {error}")))?;
|
||||||
|
|
@ -550,7 +453,7 @@ fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server config <COMMAND> [OPTIONS]\n yoi-workspace-server import-objectives [OPTIONS]\n yoi-workspace-server skills <COMMAND> [OPTIONS]\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
|
"yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server config <COMMAND> [OPTIONS]\n yoi-workspace-server skills <COMMAND> [OPTIONS]\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -566,12 +469,6 @@ fn print_config_help() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_import_objectives_help() {
|
|
||||||
println!(
|
|
||||||
"yoi-workspace-server import-objectives\n\nUsage:\n yoi-workspace-server import-objectives [OPTIONS]\n\nDescription:\n Imports legacy .yoi/objectives records and .yoi/memory/_staging JSON files into the Workspace SQLite authority.\n\nOptions:\n --workspace <PATH> Workspace root (defaults to cwd)\n --database <PATH> Server SQLite DB path (defaults to canonical server DB)\n -h, --help Print help"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_skills_help() {
|
fn print_skills_help() {
|
||||||
println!(
|
println!(
|
||||||
"yoi-workspace-server skills\n\nUsage:\n yoi-workspace-server skills list [OPTIONS]\n yoi-workspace-server skills lint [OPTIONS]\n yoi-workspace-server skills show <NAME> [OPTIONS]\n\nDescription:\n Uses the Workspace backend Skill catalog/lint/detail authority. Catalog output is lightweight and omits full SKILL.md bodies; detail output includes the body. allowed-tools and scripts are diagnostics only.\n\nOptions:\n --workspace <PATH> Workspace root (defaults to cwd)\n -h, --help Print help"
|
"yoi-workspace-server skills\n\nUsage:\n yoi-workspace-server skills list [OPTIONS]\n yoi-workspace-server skills lint [OPTIONS]\n yoi-workspace-server skills show <NAME> [OPTIONS]\n\nDescription:\n Uses the Workspace backend Skill catalog/lint/detail authority. Catalog output is lightweight and omits full SKILL.md bodies; detail output includes the body. allowed-tools and scripts are diagnostics only.\n\nOptions:\n --workspace <PATH> Workspace root (defaults to cwd)\n -h, --help Print help"
|
||||||
|
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
use project_record::validate_record_id;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::store::{
|
|
||||||
ControlPlaneStore, MemoryStagingRecord, ObjectiveRecord, ObjectiveResourceRecord,
|
|
||||||
ObjectiveTicketLinkRecord,
|
|
||||||
};
|
|
||||||
use crate::{Error, Result};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ObjectiveImportReport {
|
|
||||||
pub objectives_imported: usize,
|
|
||||||
pub objective_resources_imported: usize,
|
|
||||||
pub objective_ticket_links_imported: usize,
|
|
||||||
pub objective_ticket_links_skipped: usize,
|
|
||||||
pub memory_staging_records_imported: usize,
|
|
||||||
pub invalid_records: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct ObjectiveFrontmatter {
|
|
||||||
title: String,
|
|
||||||
state: String,
|
|
||||||
#[serde(default)]
|
|
||||||
created_at: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
updated_at: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
linked_tickets: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn import_legacy_objectives_and_memory_staging<S: ControlPlaneStore>(
|
|
||||||
workspace_root: &Path,
|
|
||||||
workspace_id: &str,
|
|
||||||
store: &S,
|
|
||||||
) -> Result<ObjectiveImportReport> {
|
|
||||||
let mut report = ObjectiveImportReport {
|
|
||||||
objectives_imported: 0,
|
|
||||||
objective_resources_imported: 0,
|
|
||||||
objective_ticket_links_imported: 0,
|
|
||||||
objective_ticket_links_skipped: 0,
|
|
||||||
memory_staging_records_imported: 0,
|
|
||||||
invalid_records: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
import_objectives(workspace_root, workspace_id, store, &mut report)?;
|
|
||||||
import_memory_staging(workspace_root, workspace_id, store, &mut report)?;
|
|
||||||
Ok(report)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_objectives<S: ControlPlaneStore>(
|
|
||||||
workspace_root: &Path,
|
|
||||||
workspace_id: &str,
|
|
||||||
store: &S,
|
|
||||||
report: &mut ObjectiveImportReport,
|
|
||||||
) -> Result<()> {
|
|
||||||
let root = workspace_root.join(".yoi/objectives");
|
|
||||||
if !root.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
for entry in fs::read_dir(root)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if !path.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let objective_id = entry.file_name().to_string_lossy().to_string();
|
|
||||||
if let Err(err) = validate_record_id(&objective_id) {
|
|
||||||
report
|
|
||||||
.invalid_records
|
|
||||||
.push(format!("{objective_id}: invalid objective id: {err}"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match import_one_objective(&path, workspace_id, &objective_id, store) {
|
|
||||||
Ok(item) => {
|
|
||||||
report.objectives_imported += 1;
|
|
||||||
report.objective_resources_imported += item.resources;
|
|
||||||
report.objective_ticket_links_imported += item.links;
|
|
||||||
report.objective_ticket_links_skipped += item.skipped_links;
|
|
||||||
}
|
|
||||||
Err(err) => report
|
|
||||||
.invalid_records
|
|
||||||
.push(format!("{objective_id}: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ImportedObjectiveCounts {
|
|
||||||
resources: usize,
|
|
||||||
links: usize,
|
|
||||||
skipped_links: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_one_objective<S: ControlPlaneStore>(
|
|
||||||
objective_dir: &Path,
|
|
||||||
workspace_id: &str,
|
|
||||||
objective_id: &str,
|
|
||||||
store: &S,
|
|
||||||
) -> Result<ImportedObjectiveCounts> {
|
|
||||||
let item_path = objective_dir.join("item.md");
|
|
||||||
let raw = fs::read_to_string(&item_path)?;
|
|
||||||
let (frontmatter, body) = split_frontmatter(&raw, objective_id)?;
|
|
||||||
let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?;
|
|
||||||
let now = Utc::now().to_rfc3339();
|
|
||||||
let created_at = meta.created_at.clone().unwrap_or_else(|| now.clone());
|
|
||||||
let updated_at = meta.updated_at.clone().unwrap_or_else(|| now.clone());
|
|
||||||
|
|
||||||
store.upsert_objective(&ObjectiveRecord {
|
|
||||||
workspace_id: workspace_id.to_string(),
|
|
||||||
objective_id: objective_id.to_string(),
|
|
||||||
title: meta.title,
|
|
||||||
state: meta.state,
|
|
||||||
body_md: body.trim_start().to_string(),
|
|
||||||
created_at: created_at.clone(),
|
|
||||||
updated_at: updated_at.clone(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut links = Vec::new();
|
|
||||||
let mut skipped_links = 0;
|
|
||||||
for ticket_id in meta.linked_tickets {
|
|
||||||
if validate_record_id(&ticket_id).is_ok() {
|
|
||||||
links.push(ObjectiveTicketLinkRecord {
|
|
||||||
workspace_id: workspace_id.to_string(),
|
|
||||||
objective_id: objective_id.to_string(),
|
|
||||||
ticket_id,
|
|
||||||
kind: "linked".to_string(),
|
|
||||||
created_at: now.clone(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
skipped_links += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let imported_links = links.len();
|
|
||||||
let links_imported = if let Err(err) =
|
|
||||||
store.replace_objective_ticket_links(workspace_id, objective_id, &links)
|
|
||||||
{
|
|
||||||
// The legacy filesystem allowed links to tickets that may no longer
|
|
||||||
// exist. Preserve the Objective itself and make the skipped count clear
|
|
||||||
// rather than failing the whole import on a foreign-key mismatch.
|
|
||||||
skipped_links += imported_links;
|
|
||||||
eprintln!(
|
|
||||||
"warning: skipped {imported_links} ticket link(s) for objective {objective_id}: {err}"
|
|
||||||
);
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
imported_links
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut resources = 0;
|
|
||||||
for file in walk_files(objective_dir)? {
|
|
||||||
let relative = file
|
|
||||||
.strip_prefix(objective_dir)
|
|
||||||
.map_err(|err| Error::Store(err.to_string()))?
|
|
||||||
.to_string_lossy()
|
|
||||||
.replace('\\', "/");
|
|
||||||
if relative == "item.md" || relative.starts_with("_staging/") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Ok(body) = fs::read_to_string(&file) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
store.upsert_objective_resource(&ObjectiveResourceRecord {
|
|
||||||
workspace_id: workspace_id.to_string(),
|
|
||||||
objective_id: objective_id.to_string(),
|
|
||||||
resource_path: relative,
|
|
||||||
body,
|
|
||||||
media_type: Some("text/markdown".to_string()),
|
|
||||||
created_at: created_at.clone(),
|
|
||||||
updated_at: updated_at.clone(),
|
|
||||||
})?;
|
|
||||||
resources += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ImportedObjectiveCounts {
|
|
||||||
resources,
|
|
||||||
links: links_imported,
|
|
||||||
skipped_links,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_memory_staging<S: ControlPlaneStore>(
|
|
||||||
workspace_root: &Path,
|
|
||||||
workspace_id: &str,
|
|
||||||
store: &S,
|
|
||||||
report: &mut ObjectiveImportReport,
|
|
||||||
) -> Result<()> {
|
|
||||||
let root = workspace_root.join(".yoi/memory/_staging");
|
|
||||||
if !root.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let now = Utc::now().to_rfc3339();
|
|
||||||
for entry in fs::read_dir(root)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let raw_json = fs::read_to_string(&path)?;
|
|
||||||
let candidate_id = candidate_id_from_json(&raw_json)
|
|
||||||
.or_else(|| {
|
|
||||||
path.file_stem()
|
|
||||||
.map(|stem| stem.to_string_lossy().to_string())
|
|
||||||
})
|
|
||||||
.ok_or_else(|| Error::Store(format!("missing candidate id: {}", path.display())))?;
|
|
||||||
store.upsert_memory_staging_record(&MemoryStagingRecord {
|
|
||||||
workspace_id: workspace_id.to_string(),
|
|
||||||
candidate_id,
|
|
||||||
raw_json,
|
|
||||||
source_path: Some(path.to_string_lossy().to_string()),
|
|
||||||
imported_at: now.clone(),
|
|
||||||
})?;
|
|
||||||
report.memory_staging_records_imported += 1;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn candidate_id_from_json(raw_json: &str) -> Option<String> {
|
|
||||||
serde_json::from_str::<serde_json::Value>(raw_json)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| {
|
|
||||||
value
|
|
||||||
.get("id")
|
|
||||||
.and_then(|id| id.as_str())
|
|
||||||
.map(str::to_string)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_frontmatter<'a>(raw: &'a str, label: &str) -> Result<(&'a str, &'a str)> {
|
|
||||||
let rest = raw
|
|
||||||
.strip_prefix("---\n")
|
|
||||||
.ok_or_else(|| Error::MissingFrontmatter(label.to_string()))?;
|
|
||||||
let Some((frontmatter, body)) = rest.split_once("\n---\n") else {
|
|
||||||
return Err(Error::MissingFrontmatter(label.to_string()));
|
|
||||||
};
|
|
||||||
Ok((frontmatter, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_files(root: &Path) -> Result<Vec<PathBuf>> {
|
|
||||||
let mut files = Vec::new();
|
|
||||||
walk_files_inner(root, &mut files)?;
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_files_inner(path: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
|
|
||||||
for entry in fs::read_dir(path)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
walk_files_inner(&path, files)?;
|
|
||||||
} else if path.is_file() {
|
|
||||||
files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -6607,7 +6607,10 @@ mod tests {
|
||||||
TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
|
TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
|
||||||
WorkerSpawnIntent,
|
WorkerSpawnIntent,
|
||||||
};
|
};
|
||||||
use crate::store::SqliteWorkspaceStore;
|
use crate::store::{
|
||||||
|
MemoryStagingRecord, ObjectiveRecord, ObjectiveResourceRecord, ObjectiveTicketLinkRecord,
|
||||||
|
SqliteWorkspaceStore,
|
||||||
|
};
|
||||||
|
|
||||||
const TEST_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001";
|
const TEST_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001";
|
||||||
const TEST_REPOSITORY_ID: &str = "main";
|
const TEST_REPOSITORY_ID: &str = "main";
|
||||||
|
|
@ -8309,8 +8312,6 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serves_bounded_read_apis_and_static_spa_separately() {
|
async fn serves_bounded_read_apis_and_static_spa_separately() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
write_objective(dir.path(), "00000000001J3", "API Objective", "active");
|
|
||||||
write_memory_staging(dir.path(), "00000000001J4");
|
|
||||||
let static_dir = dir.path().join("static");
|
let static_dir = dir.path().join("static");
|
||||||
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
|
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
|
||||||
std::fs::write(static_dir.join("index.html"), "<main>Yoi Workspace</main>").unwrap();
|
std::fs::write(static_dir.join("index.html"), "<main>Yoi Workspace</main>").unwrap();
|
||||||
|
|
@ -8336,12 +8337,50 @@ mod tests {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
crate::objective_import::import_legacy_objectives_and_memory_staging(
|
sqlite_store
|
||||||
dir.path(),
|
.upsert_objective(&ObjectiveRecord {
|
||||||
TEST_WORKSPACE_ID,
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
&sqlite_store,
|
objective_id: "00000000001J3".to_string(),
|
||||||
)
|
title: "API Objective".to_string(),
|
||||||
.unwrap();
|
state: "active".to_string(),
|
||||||
|
body_md: "Objective body.\n".to_string(),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
sqlite_store
|
||||||
|
.replace_objective_ticket_links(
|
||||||
|
TEST_WORKSPACE_ID,
|
||||||
|
"00000000001J3",
|
||||||
|
&[ObjectiveTicketLinkRecord {
|
||||||
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
|
objective_id: "00000000001J3".to_string(),
|
||||||
|
ticket_id: "00000000001J2".to_string(),
|
||||||
|
kind: "linked".to_string(),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
sqlite_store
|
||||||
|
.upsert_objective_resource(&ObjectiveResourceRecord {
|
||||||
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
|
objective_id: "00000000001J3".to_string(),
|
||||||
|
resource_path: "memory-architecture-overview.md".to_string(),
|
||||||
|
body: "# Memory architecture\n\nResource body.\n".to_string(),
|
||||||
|
media_type: Some("text/markdown".to_string()),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
sqlite_store
|
||||||
|
.upsert_memory_staging_record(&MemoryStagingRecord {
|
||||||
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
|
candidate_id: "00000000001J4".to_string(),
|
||||||
|
raw_json: r#"{"id":"00000000001J4","kind":"working_assumption"}"#.to_string(),
|
||||||
|
source_path: None,
|
||||||
|
imported_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sqlite_store
|
sqlite_store
|
||||||
.count_memory_staging_records(TEST_WORKSPACE_ID)
|
.count_memory_staging_records(TEST_WORKSPACE_ID)
|
||||||
|
|
@ -9363,25 +9402,6 @@ mod tests {
|
||||||
backend.create(input).unwrap();
|
backend.create(input).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_memory_staging(root: &Path, candidate_id: &str) {
|
|
||||||
let staging_dir = root.join(".yoi/memory/_staging");
|
|
||||||
std::fs::create_dir_all(&staging_dir).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
staging_dir.join(format!("{candidate_id}.json")),
|
|
||||||
format!(
|
|
||||||
r#"{{
|
|
||||||
"schema_version": 2,
|
|
||||||
"id": "{candidate_id}",
|
|
||||||
"kind": "working_assumption",
|
|
||||||
"claim": "Objective staging import test",
|
|
||||||
"why_useful": "Verifies SQLite push"
|
|
||||||
}}
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
|
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
|
||||||
let objective_dir = root.join(".yoi/objectives").join(id);
|
let objective_dir = root.join(".yoi/objectives").join(id);
|
||||||
std::fs::create_dir_all(&objective_dir).unwrap();
|
std::fs::create_dir_all(&objective_dir).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/import-objectives-and-staging-to-sqlite-20260726.sh [--workspace PATH] [--database PATH] [--apply]
|
|
||||||
|
|
||||||
Imports legacy .yoi/objectives records and .yoi/memory/_staging JSON files into
|
|
||||||
Workspace SQLite authority by invoking yoi-workspace-server import-objectives.
|
|
||||||
|
|
||||||
By default this is a dry-run of the command line only. Pass --apply to execute.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace="$(pwd)"
|
|
||||||
database="${YOI_SERVER_DB:-$HOME/.local/share/yoi/server/server.db}"
|
|
||||||
apply=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--workspace)
|
|
||||||
workspace="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--workspace=*)
|
|
||||||
workspace="${1#--workspace=}"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--database)
|
|
||||||
database="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--database=*)
|
|
||||||
database="${1#--database=}"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--apply)
|
|
||||||
apply=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "unknown argument: $1" >&2
|
|
||||||
usage >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
cmd=(cargo run -q -p yoi-workspace-server -- import-objectives --workspace "$workspace" --database "$database")
|
|
||||||
|
|
||||||
printf 'Workspace: %s\n' "$workspace"
|
|
||||||
printf 'Database: %s\n' "$database"
|
|
||||||
printf 'Command: '
|
|
||||||
printf '%q ' "${cmd[@]}"
|
|
||||||
printf '\n'
|
|
||||||
|
|
||||||
if [[ "$apply" -ne 1 ]]; then
|
|
||||||
echo 'Dry run only. Re-run with --apply to import.'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${cmd[@]}"
|
|
||||||
Loading…
Reference in New Issue
Block a user