objective: remove one-shot import

This commit is contained in:
Keisuke Hirata 2026-07-26 12:47:42 +09:00
parent e653f8f08b
commit 0eb840ab4b
No known key found for this signature in database
6 changed files with 77 additions and 470 deletions

View File

@ -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,10 +240,28 @@ mod tests {
}) })
.await .await
.unwrap(); .unwrap();
crate::objective_import::import_legacy_objectives_and_memory_staging( store
dir.path(), .upsert_objective(&ObjectiveRecord {
workspace_id: "workspace-test".to_string(),
objective_id: "00000000001J3".to_string(),
title: "Control plane".to_string(),
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", "workspace-test",
&store, "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(); .unwrap();
write_objective( write_objective(

View File

@ -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;

View File

@ -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"

View File

@ -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(())
}

View File

@ -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 {
workspace_id: TEST_WORKSPACE_ID.to_string(),
objective_id: "00000000001J3".to_string(),
title: "API Objective".to_string(),
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, TEST_WORKSPACE_ID,
&sqlite_store, "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(); .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();

View File

@ -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[@]}"