use std::fs;
use serde::Serialize;
use sha2::{Digest, Sha256};
use crate::config::Config;
use crate::error::{Error, IoContext, Result};
use crate::git::GitRepo;
use crate::hex::encode_lower;
use crate::path::reject_symlink_traversal;
use crate::policy::{INSTRUCTION_POLICY_VERSION, REPOSITORY_MODULE_MAX_BYTES};
pub const BOOTSTRAP: &str = "# Repository instructions\n\nBefore doing any repository work, run:\n\n workspace-mgr instructions --repo .\n\nFollow its output as the repository instructions for this session. If\n`workspace-mgr` is unavailable, stop repository work, tell the user, and ask\npermission to install the latest stable release from crates.io:\n\n cargo install --locked workspace-mgr\n workspace-mgr setup\n\nAfter installation, retry `workspace-mgr instructions --repo .`. If installation\nis not approved, installation fails, or the instructions command still fails,\nstop and report the problem. Do not substitute lower-level version-control or\nstorage mutation commands.\n";
pub const MANAGEMENT_MODEL: &str = include_str!("../docs/management-model.md");
#[derive(Debug, Clone, Serialize)]
pub struct InstructionDocument {
pub cli_version: String,
pub policy_version: u32,
pub policy_hash: String,
pub topic: String,
pub markdown: String,
}
pub fn render(repo: &GitRepo, config: &Config, topic: Option<&str>) -> Result<InstructionDocument> {
let topic = topic.unwrap_or("all");
let valid_topics = [
"all",
"model",
"core",
"task",
"publish",
"artifacts",
"storage",
"shared-checkout",
"infrastructure",
"repository",
];
if !valid_topics.contains(&topic) {
return Err(Error::message(format!(
"unknown instruction topic {topic:?}; expected one of {}",
valid_topics.join(", ")
)));
}
let mut sections = Vec::new();
let mut repository_module_hash_input = None;
if topic == "all" || topic == "model" {
sections.push(MANAGEMENT_MODEL.trim().to_owned());
}
if topic != "model" {
sections.push(format!(
"# Effective repository instructions\n\nGenerated by `workspace-mgr {}` using its fixed product policy and the Git/S3 facts in `{}`. These instructions apply to this repository for the current session. Explicit user directions can authorize a narrow exception; record the exact scope and do not broaden it.",
env!("CARGO_PKG_VERSION"),
crate::config::CONFIG_NAME
));
}
if topic == "all" {
sections.push(format!(
"## Repository control facts\n\nThe shared checkout stays on `{}` and uses remote `{}`. Task-scoped commands discover a deliverable manifest from the selected path; infrastructure tasks always use their explicit `--manifest`. Read the relevant command's `--help` before acting. Detailed compatibility topics remain available with `workspace-mgr instructions <topic>`.",
config.git.branch, config.git.remote
));
} else if topic != "model" && topic != "repository" {
sections.push(crate::guidance::topic(topic, config).ok_or_else(|| {
Error::message(format!("missing instruction guidance for {topic:?}"))
})?);
}
if topic == "all" || topic == "repository" {
let module = ".workspace-mgr/instructions/repository.md";
reject_symlink_traversal(&repo.root, module, "repository instruction module")?;
let extra = repo.root.join(module);
if extra.is_file() {
let content = fs::read_to_string(&extra).at(&extra)?;
if content.len() > REPOSITORY_MODULE_MAX_BYTES {
return Err(Error::message(format!(
"repository instruction module exceeds 64 KiB: {}",
extra.display()
)));
}
if topic == "all" {
repository_module_hash_input = Some(content.clone());
}
if !content.trim().is_empty() {
if topic == "repository" {
sections.push(format!(
"# Repository-specific additions\n\nRepository-owned instructions reproduced below. They do not change the fixed task, storage, publication, or review policy.\n\n{content}"
));
} else {
sections.push(format!(
"## Repository-specific instructions\n\nThis repository provides `{module}`. Read it before task work, either directly or with `workspace-mgr instructions repository --repo .`. The default output indexes this user-owned module instead of repeating its body."
));
}
}
} else if topic == "repository" {
sections.push(
"# Repository-specific additions\n\nThis repository has no instruction module."
.to_owned(),
);
}
}
let body = sections.join("\n\n");
let mut hasher = Sha256::new();
hasher.update(env!("CARGO_PKG_VERSION"));
hasher.update(config.render()?);
hasher.update(topic);
hasher.update(&body);
if let Some(content) = repository_module_hash_input {
hasher.update(b"repository-instruction-module\0");
hasher.update(content.as_bytes());
}
hasher.update(INSTRUCTION_POLICY_VERSION.to_be_bytes());
if topic == "all" {
// Indexing guidance instead of printing it must not conceal policy
// changes from the effective-policy fingerprint.
hasher.update(b"bootstrap\0");
hasher.update(BOOTSTRAP.as_bytes());
for name in [
"core",
"task",
"publish",
"artifacts",
"storage",
"shared-checkout",
"infrastructure",
] {
hasher.update(b"topic\0");
hasher.update(name.as_bytes());
hasher.update(b"\0");
hasher.update(
crate::guidance::topic(name, config)
.expect("known guidance topic")
.as_bytes(),
);
}
for operation in crate::command_guidance::OPERATIONS {
hasher.update(b"operation\0");
hasher.update(operation.as_bytes());
hasher.update(b"\0");
hasher.update(crate::command_guidance::command(operation).as_bytes());
}
}
let policy_hash = encode_lower(hasher.finalize());
let markdown = format!(
"<!-- workspace-mgr: cli={} policy-version={} policy={} topic={} -->\n{}\n",
env!("CARGO_PKG_VERSION"),
INSTRUCTION_POLICY_VERSION,
&policy_hash[..16],
topic,
body
);
Ok(InstructionDocument {
cli_version: env!("CARGO_PKG_VERSION").to_owned(),
policy_version: INSTRUCTION_POLICY_VERSION,
policy_hash,
topic: topic.to_owned(),
markdown,
})
}