🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

practicode

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

practicode - npm Package Compare versions

Comparing version
0.2.1
to
0.2.2
+1
-1
Cargo.lock

@@ -456,3 +456,3 @@ # This file is automatically @generated by Cargo.

name = "practicode"
version = "0.2.1"
version = "0.2.2"
dependencies = [

@@ -459,0 +459,0 @@ "anyhow",

[package]
name = "practicode"
version = "0.2.1"
version = "0.2.2"
edition = "2024"

@@ -5,0 +5,0 @@ description = "Offline-first programming language lessons and coding practice in a Rust terminal UI."

{
"name": "practicode",
"version": "0.2.1",
"version": "0.2.2",
"description": "Offline-first programming language lessons and coding practice in a Rust terminal UI.",

@@ -5,0 +5,0 @@ "license": "MIT",

+75
-10
use crate::{
core::{
AppState, CLAUDE_AI_EFFORTS, LANGUAGES, PROBLEM_NOTES_PATH, Problem, Settings,
SyntaxLesson, UI_LANGUAGES, ensure_submission, ensure_syntax_submission,
normalize_ai_provider, regular_file_exists, render_problem, save_user_text,
syntax_lesson_study_context, ui_text,
AppState, BANK_PATH, CLAUDE_AI_EFFORTS, LANGUAGES, PROBLEM_NOTES_PATH, Problem, STATE_PATH,
Settings, SyntaxLesson, UI_LANGUAGES, ensure_submission, ensure_syntax_submission,
load_bank, load_state, normalize_ai_provider, regular_file_exists, render_problem,
save_bank, save_state, save_user_text, syntax_lesson_study_context, ui_text,
},
process::{run_capture, sh_quote, shell_process, unique_temp_path, which},
process::{RunOutput, run_capture, sh_quote, shell_process, unique_temp_path, which},
};
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use std::{

@@ -27,2 +27,3 @@ env, fs,

const CLAUDE_MODEL_FALLBACKS: &[&str] = &["sonnet", "opus", "fable", "claude-fable-5"];
const PROBLEM_BANK_SCHEMA: &str = "problem_bank.json is an array. Preserve every existing problem. Each item must contain id and slug as strings; difficulty as a string; topics as an array of strings; title, statement, input, and output as objects mapping UI language codes to strings; examples and cases as arrays of objects with string input and output fields; answers as an object mapping code language names to complete source strings.";

@@ -128,3 +129,3 @@ pub fn build_lesson_ai_prompt(

.env("PRACTICODE_AI_EFFORT", &state.settings.ai_effort);
match run_capture(&mut process, "", Duration::from_secs(900)) {
match run_problem_generation_command(root, Some(state), &mut process) {
Ok(run) if run.code == Some(0) => {

@@ -193,3 +194,3 @@ let output = output_text(&run.stdout, &run.stderr);

.env("PRACTICODE_AI_EFFORT", &state.settings.ai_effort);
match run_capture(&mut process, "", Duration::from_secs(900)) {
match run_problem_generation_command(root, None, &mut process) {
Ok(run) if run.code == Some(0) => {

@@ -206,2 +207,66 @@ AiGenerationResult::Succeeded(output_text(&run.stdout, &run.stderr))

fn run_problem_generation_command(
root: &Path,
state_snapshot: Option<&AppState>,
process: &mut Command,
) -> Result<RunOutput> {
let bank = load_bank(root)?;
save_bank(root, &bank).context("materialize problem bank before AI command")?;
if let Some(state) = state_snapshot {
save_state(root, state).context("save problem state before AI command")?;
}
let run = match run_capture(process, "", Duration::from_secs(900)) {
Ok(run) => run,
Err(error) => {
if let Err(restore_error) = restore_ai_metadata(root, &bank, state_snapshot) {
return Err(error.context(format!(
"restore Practicode metadata after AI command failure: {restore_error}"
)));
}
return Err(error);
}
};
if run.code != Some(0) {
restore_ai_metadata(root, &bank, state_snapshot)?;
return Ok(run);
}
let validation: Result<()> = (|| {
if !regular_file_exists(&root.join(BANK_PATH))? {
bail!("AI command removed {BANK_PATH}");
}
let generated_bank = load_bank(root)?;
if state_snapshot.is_some() {
if !regular_file_exists(&root.join(STATE_PATH))? {
bail!("AI command removed {STATE_PATH}");
}
load_state(root, &generated_bank)?;
}
Ok(())
})();
if let Err(error) = validation {
if let Err(restore_error) = restore_ai_metadata(root, &bank, state_snapshot) {
return Err(error.context(format!(
"restore Practicode metadata after invalid AI output: {restore_error}"
)));
}
return Err(error.context("AI command produced invalid Practicode metadata"));
}
Ok(run)
}
fn restore_ai_metadata(
root: &Path,
bank: &[Problem],
state_snapshot: Option<&AppState>,
) -> Result<()> {
save_bank(root, bank).context("restore problem bank after AI command")?;
if let Some(state) = state_snapshot {
save_state(root, state).context("restore problem state after AI command")?;
}
Ok(())
}
pub fn default_ai_next_command(root: &Path, settings: &Settings, request: &str) -> String {

@@ -290,3 +355,3 @@ match normalize_ai_provider(&settings.ai_provider).as_str() {

format!(
"Read problem_notes.md if present, problems/INDEX.md if present, problem_bank.json if present, and problem-state.json. Create exactly one new non-duplicate coding practice problem. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. User profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; code language: {}; UI language: {}; generated answer languages: {}; generated UI languages: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. Make the smallest valid edits: update problem_bank.json, one problem directory, problems/INDEX.md, and problem-state.json. Do not include the answer in the problem statement. Do not create solution.*, test_solution.*, or any answer-revealing file inside the problem directory.",
"Read problem_notes.md if present, problems/INDEX.md if present, problem_bank.json if present, and problem-state.json. {PROBLEM_BANK_SCHEMA} Create exactly one new non-duplicate coding practice problem. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. User profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; code language: {}; UI language: {}; generated answer languages: {}; generated UI languages: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. Make the smallest valid edits: update problem_bank.json, one problem directory, problems/INDEX.md, and problem-state.json. Do not include the answer in the problem statement. Do not create solution.*, test_solution.*, or any answer-revealing file inside the problem directory.",
if request.is_empty() {

@@ -309,3 +374,3 @@ "(none)"

format!(
"Read problem_notes.md if present, problems/INDEX.md if present, problem_bank.json if present, and problem-state.json. Create exactly one new non-duplicate coding practice problem for later use. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. User profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; current code language: {}; current UI language: {}; generated answer languages: {}; generated UI languages: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. Make the smallest valid edits: update problem_bank.json, one problem directory, and problems/INDEX.md. Preserve problem-state.json current_problem, history, solved, and settings; do not switch the current problem. Do not include the answer in the problem statement. Do not create solution.*, test_solution.*, or any answer-revealing file inside the problem directory.",
"Read problem_notes.md if present, problems/INDEX.md if present, problem_bank.json if present, and problem-state.json. {PROBLEM_BANK_SCHEMA} Create exactly one new non-duplicate coding practice problem for later use. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. User profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; current code language: {}; current UI language: {}; generated answer languages: {}; generated UI languages: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. Make the smallest valid edits: update problem_bank.json, one problem directory, and problems/INDEX.md. Preserve problem-state.json current_problem, history, solved, and settings; do not switch the current problem. Do not include the answer in the problem statement. Do not create solution.*, test_solution.*, or any answer-revealing file inside the problem directory.",
if request.is_empty() {

@@ -312,0 +377,0 @@ "(none)"

@@ -15,5 +15,5 @@ use crate::{

parse_ui_language_list, previous_problem, problem_by_id, record_pass, record_syntax_result,
render_syntax_lesson, save_state, save_user_text, set_current_syntax_lesson, syntax_cases,
syntax_core_progress_count, syntax_language_name, syntax_review_due_at, template_for,
ui_text,
render_syntax_lesson, save_bank, save_state, save_user_text, set_current_syntax_lesson,
syntax_cases, syntax_core_progress_count, syntax_language_name, syntax_review_due_at,
template_for, ui_text,
},

@@ -20,0 +20,0 @@ text::{

@@ -83,26 +83,35 @@ use super::*;

let old_len = self.generate_bank_len;
let (added, reload_error) = match load_bank(&self.root) {
Ok(bank)
if !bank
.iter()
.any(|problem| problem.id == self.state.current_problem) =>
{
(
0,
Some(format!(
let (added, reload_error) =
match save_state(&self.root, &self.state).and_then(|()| load_bank(&self.root)) {
Ok(bank)
if !bank
.iter()
.any(|problem| problem.id == self.state.current_problem) =>
{
let detail = format!(
"current problem {} is missing from the reloaded bank",
self.state.current_problem
);
(
0,
Some(restore_generated_bank(&self.root, &self.bank, detail)),
)
}
Ok(bank) => {
let added = bank.len().saturating_sub(old_len);
self.bank = bank;
if let Some(cursor) = self.list_cursor.as_mut() {
*cursor = (*cursor).min(self.bank.len() - 1);
}
(added, None)
}
Err(error) => (
0,
Some(restore_generated_bank(
&self.root,
&self.bank,
error.to_string(),
)),
)
}
Ok(bank) => {
let added = bank.len().saturating_sub(old_len);
self.bank = bank;
if let Some(cursor) = self.list_cursor.as_mut() {
*cursor = (*cursor).min(self.bank.len() - 1);
}
(added, None)
}
Err(error) => (0, Some(error.to_string())),
};
),
};
self.generate_notice = Some(match result {

@@ -405,2 +414,9 @@ AiGenerationResult::Failed { status, detail } => GenerationNotice::Failed {

fn restore_generated_bank(root: &std::path::Path, bank: &[Problem], detail: String) -> String {
match save_bank(root, bank) {
Ok(()) => detail,
Err(error) => format!("{detail}; restore previous problem bank: {error}"),
}
}
#[cfg(test)]

@@ -432,2 +448,27 @@ mod tests {

#[test]
fn background_generation_restores_invalid_metadata() {
let mut app = localized_app("practicode-invalid-background-metadata", "en");
let current_problem = app.state.current_problem.clone();
std::fs::write(app.root.join("problem_bank.json"), "not json").unwrap();
std::fs::write(app.root.join("problem-state.json"), "not json").unwrap();
let bank_len = app.bank.len();
finish_generation(
&mut app,
AiGenerationResult::Succeeded(String::new()),
bank_len,
);
let bank = load_bank(&app.root).unwrap();
assert_eq!(
load_state(&app.root, &bank).unwrap().current_problem,
current_problem
);
assert!(matches!(
app.generate_notice,
Some(GenerationNotice::ReloadFailed(_))
));
}
fn learning_app(name: &str) -> PracticodeApp {

@@ -686,2 +727,3 @@ let root = crate::process::unique_temp_path(name, "dir");

);
load_bank(&root).unwrap();

@@ -705,7 +747,3 @@ finish_generation(

);
assert!(
failed_reload.contains("問題バンクを再読み込みできませんでした"),
"{failed_reload}"
);
assert!(failed_reload.contains("parse"), "{failed_reload}");
assert!(!failed_reload.contains("parse"), "{failed_reload}");
}

@@ -712,0 +750,0 @@