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