codex-cli-captain 0.0.9

Codex-Cli-Captain runtime, installer, and MCP server for Codex CLI.
const DEFAULT_COMMIT_MESSAGE_TYPE: &str = "fix";
const DEFAULT_COMMIT_MESSAGE_SCOPE: &str = "hub, worker";
const DEFAULT_COMMIT_MESSAGE_SUMMARY: &str = "비전 기본 가중치를 metric 0.4 text 0.6으로 조정";
pub(crate) const DEFAULT_COMMIT_MESSAGE_TEMPLATE: &str = "<type>(<scope>): <summary>";

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct CommitMessageTemplateDivergence {
    pub(crate) expected: String,
    pub(crate) actual: String,
}

impl std::fmt::Display for CommitMessageTemplateDivergence {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "generated fallback commit message diverged from canonical template rendering: expected `{}`, got `{}`",
            self.expected, self.actual
        )
    }
}

impl CommitMessageTemplateDivergence {
    fn operator_report(&self) -> String {
        format!(
            "Commit message validation: status=mismatch template=`{DEFAULT_COMMIT_MESSAGE_TEMPLATE}` expected=`{}` actual=`{}` action=stop_before_publish_or_merge",
            self.expected, self.actual
        )
    }
}

fn render_commit_message_template(
    template: &str,
    commit_type: &str,
    scope: &str,
    summary: &str,
) -> String {
    template
        .replace("<type>", commit_type)
        .replace("<scope>", scope)
        .replace("<summary>", summary)
}

fn render_default_generated_commit_message() -> String {
    render_commit_message_template(
        DEFAULT_COMMIT_MESSAGE_TEMPLATE,
        DEFAULT_COMMIT_MESSAGE_TYPE,
        DEFAULT_COMMIT_MESSAGE_SCOPE,
        DEFAULT_COMMIT_MESSAGE_SUMMARY,
    )
}

pub(crate) fn validate_generated_commit_message(
    message: &str,
) -> Result<(), CommitMessageTemplateDivergence> {
    let expected = render_default_generated_commit_message();
    if message == expected {
        Ok(())
    } else {
        Err(CommitMessageTemplateDivergence {
            expected,
            actual: message.to_string(),
        })
    }
}

pub(crate) fn assert_generated_commit_message_matches_template(message: &str) {
    if let Err(divergence) = validate_generated_commit_message(message) {
        panic!("{}", divergence.operator_report());
    }
}

pub(crate) fn generated_commit_message_validation_report(message: &str) -> String {
    match validate_generated_commit_message(message) {
        Ok(()) => format!(
            "Commit message validation: status=ok template=`{DEFAULT_COMMIT_MESSAGE_TEMPLATE}` expected=`{}` actual=`{message}` action=continue",
            render_default_generated_commit_message()
        ),
        Err(divergence) => divergence.operator_report(),
    }
}

fn default_commit_message_validation_visibility_guidance(ok_report: &str) -> String {
    let expected = render_default_generated_commit_message();
    let mismatch_report = CommitMessageTemplateDivergence {
        expected,
        actual: "<candidate>".to_string(),
    }
    .operator_report();
    format!(
        "Before publish or merge, keep commit-message validation visible in fan-in: report `{ok_report}`. If validation mismatches, report `{mismatch_report}` and stop before publish/merge."
    )
}

pub(crate) fn default_generated_commit_message() -> String {
    let message = render_default_generated_commit_message();
    assert_generated_commit_message_matches_template(&message);
    message
}

fn default_commit_message_guidance_with_validation_report(
    default_message: &str,
    ok_report: &str,
) -> String {
    format!(
        "Commit message guidance: for commit-related delegated work, if the operator did not provide commit message, style, or language instructions, use the default Conventional Commit-style fallback `{}` using template `{DEFAULT_COMMIT_MESSAGE_TEMPLATE}`. If the operator supplies a commit message/style/language instruction, that instruction wins. {}",
        default_message,
        default_commit_message_validation_visibility_guidance(ok_report)
    )
}

pub(crate) fn default_commit_message_guidance() -> String {
    let default_message = default_generated_commit_message();
    let ok_report = generated_commit_message_validation_report(&default_message);
    default_commit_message_guidance_with_validation_report(&default_message, &ok_report)
}

fn field_mentions_commit_formatting_work(field: &str) -> bool {
    let field = field.to_ascii_lowercase();
    field.contains("commit")
        || field.contains("release-note")
        || field.contains("release notes")
        || field.contains("readme-maintenance")
        || field.contains("changelog")
        || field.contains("maintenance")
}

fn task_fields_need_commit_message_guidance(fields: &[&str]) -> bool {
    fields
        .iter()
        .any(|value| field_mentions_commit_formatting_work(value))
}

pub(crate) fn default_commit_message_guidance_for_task_fields(fields: &[&str]) -> Option<String> {
    let default_message = default_generated_commit_message();
    generated_commit_message_validation_report_for_task_fields(fields, &default_message)?;
    Some(default_commit_message_guidance())
}

pub(crate) fn generated_commit_message_validation_report_for_task_fields(
    fields: &[&str],
    message: &str,
) -> Option<String> {
    if task_fields_need_commit_message_guidance(fields) {
        Some(generated_commit_message_validation_report(message))
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::{
        assert_generated_commit_message_matches_template, default_commit_message_guidance,
        default_commit_message_guidance_for_task_fields, default_generated_commit_message,
        generated_commit_message_validation_report,
        generated_commit_message_validation_report_for_task_fields, render_commit_message_template,
        validate_generated_commit_message, DEFAULT_COMMIT_MESSAGE_TEMPLATE,
    };

    #[test]
    fn generated_commit_message_uses_canonical_template_components() {
        assert_eq!(
            default_generated_commit_message(),
            "fix(hub, worker): 비전 기본 가중치를 metric 0.4 text 0.6으로 조정"
        );
        assert_eq!(
            DEFAULT_COMMIT_MESSAGE_TEMPLATE,
            "<type>(<scope>): <summary>"
        );
        assert_eq!(
            render_commit_message_template(DEFAULT_COMMIT_MESSAGE_TEMPLATE, "fix", "hub", "repair"),
            "fix(hub): repair"
        );
        assert!(validate_generated_commit_message(&default_generated_commit_message()).is_ok());
    }

    #[test]
    fn generated_commit_message_validation_reports_template_divergence() {
        let divergence = validate_generated_commit_message("fix(hub): repair")
            .expect_err("divergent message should fail validation");

        assert_eq!(divergence.actual, "fix(hub): repair");
        assert_eq!(divergence.expected, default_generated_commit_message());
        assert!(divergence
            .to_string()
            .contains("generated fallback commit message diverged"));
    }

    #[test]
    #[should_panic(
        expected = "Commit message validation: status=mismatch template=`<type>(<scope>): <summary>` expected=`fix(hub, worker): 비전 기본 가중치를 metric 0.4 text 0.6으로 조정` actual=`fix(hub): repair` action=stop_before_publish_or_merge"
    )]
    fn generated_commit_message_assertion_fails_fast_on_template_divergence() {
        assert_generated_commit_message_matches_template("fix(hub): repair");
    }

    #[test]
    fn generated_commit_message_validation_report_keeps_mismatch_details_visible() {
        let report = generated_commit_message_validation_report("fix(hub): repair");

        assert!(report.contains("status=mismatch"));
        assert!(report.contains("template=`<type>(<scope>): <summary>`"));
        assert!(report.contains(
            "expected=`fix(hub, worker): 비전 기본 가중치를 metric 0.4 text 0.6으로 조정`"
        ));
        assert!(report.contains("actual=`fix(hub): repair`"));
        assert!(report.contains("action=stop_before_publish_or_merge"));
    }

    #[test]
    fn commit_message_guidance_uses_generated_default_message() {
        let guidance = default_commit_message_guidance();

        assert!(guidance.contains(&default_generated_commit_message()));
        assert!(guidance.contains(DEFAULT_COMMIT_MESSAGE_TEMPLATE));
        assert!(guidance.contains("Before publish or merge"));
        assert!(guidance.contains("Commit message validation: status=ok"));
        assert!(guidance.contains("Commit message validation: status=mismatch"));
        assert!(guidance.contains("actual=`<candidate>`"));
    }

    #[test]
    fn commit_message_guidance_for_task_fields_matches_commit_related_text() {
        let guidance = default_commit_message_guidance_for_task_fields(&[
            "Commit release documentation update",
            "Update release docs and plugin packaging guidance.",
            "Use the default format.",
            "Create the git commit for the completed slice.",
        ]);

        assert_eq!(guidance, Some(default_commit_message_guidance()));
    }

    #[test]
    fn commit_message_guidance_for_task_fields_matches_release_and_maintenance_text() {
        let guidance = default_commit_message_guidance_for_task_fields(&[
            "Update release-note and readme-maintenance automation.",
            "Refresh changelog wording for the maintenance workflow.",
            "Keep release-note and maintenance surfaces on the canonical formatter.",
        ]);

        assert_eq!(guidance, Some(default_commit_message_guidance()));
    }

    #[test]
    fn commit_message_validation_report_for_task_fields_matches_release_and_maintenance_text() {
        let message = default_generated_commit_message();
        let expected_report = generated_commit_message_validation_report(&message);
        let release_report = generated_commit_message_validation_report_for_task_fields(
            &[
                "Commit release documentation update",
                "Update release docs and plugin packaging guidance.",
                "Create the git commit for the release docs.",
            ],
            &message,
        );
        let maintenance_report = generated_commit_message_validation_report_for_task_fields(
            &[
                "Commit readme maintenance update",
                "Update readme-maintenance docs and changelog guidance.",
                "Create the git commit for the maintenance docs.",
            ],
            &message,
        );

        assert_eq!(release_report, Some(expected_report.clone()));
        assert_eq!(maintenance_report, Some(expected_report));
    }

    #[test]
    fn commit_message_guidance_for_task_fields_skips_non_commit_text() {
        let guidance = default_commit_message_guidance_for_task_fields(&[
            "Backfill sample fixture",
            "Copy data into docs/examples/sample.json",
            "Use only the provided source path and report if blocked.",
            "Summarize what changed.",
        ]);

        assert!(guidance.is_none());
    }
}