Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,18 @@ dispatches ready DAG nodes with `program` dispatch mode.

`decodex research compile` is the native Decodex research/design entrypoint. It
accepts minimal natural-language intake or a structured research/design JSON packet,
then persists a `decodex.decision_contract/1` payload in local runtime SQLite. The
Decodex research method frames the question first, records evidence, compares
realistic options, forms a challenge-ready judgment, resolves skeptic objections, and
then ends as `decision_ready`, `not_decision_ready`, `blocked`, or
`needs_human_decision`. A compiled contract is latent and cannot queue work, mutate
tracker state, set goals, or authorize implementation. `decodex research promote`
records explicit acceptance for a stored contract; only promoted contracts may later
feed issue shaping or internal Execution Program readiness.
then persists a contract-first `decodex.decision_contract/1` payload in local runtime
SQLite. The Decodex research method frames the question first, records an evidence
ledger, compares realistic options, forms a challenge-ready judgment, resolves skeptic
objections, and then ends as `decision_ready`, `not_decision_ready`, `blocked`, or
`needs_human_decision`. New Decodex research may use `docs/research/` only for
JSON research artifacts, never the old `research-run/2` event-log shape; removed
legacy JSON runs are consolidated in
`docs/research/legacy-research-goal-audit.json`. A compiled contract is latent and
cannot queue work, mutate tracker state, set goals, or authorize implementation.
`decodex research promote` records explicit acceptance for a stored contract; only
promoted contracts may later feed issue shaping or internal Execution Program
readiness.

`decodex intake goal` materializes a promoted Decision Contract. `--dry-run` prints
the proposed normal Linear issues, dependencies, conflict domains, and dispatch plan
Expand Down Expand Up @@ -390,8 +394,7 @@ The tracked workspace currently keeps:
and content workflow lane
- `docs/reference/` as the current repository and artifact surface map lane
- `docs/decisions/` as the durable design-rationale lane
- `docs/research/` as legacy or supporting machine-authored research artifacts; current
Decodex research authority flows through runtime-local Decision Contracts
- `docs/research/` as supporting JSON research reports and evidence, not runtime authority
- `dev/` as local development helpers outside `dev/skills/`, such as the operator
dashboard mock server
- `assets/` as generated Decodex App icon source notes, Icon Composer foreground,
Expand Down
14 changes: 14 additions & 0 deletions apps/decodex/src/loop_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,16 @@ impl DecisionResearchProvenance {
/// Non-authoritative research evidence retained before promotion.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub(crate) struct DecisionResearchEvidence {
#[serde(default = "default_research_evidence_kind")]
kind: String,
claim: String,
support: String,
#[serde(skip_serializing_if = "Option::is_none")]
source_ref: Option<String>,
}
impl DecisionResearchEvidence {
fn validate(&self) -> Result<()> {
validate_required("decision contract research_evidence.kind", &self.kind)?;
validate_required("decision contract research_evidence.claim", &self.claim)?;
validate_required("decision contract research_evidence.support", &self.support)?;

Expand Down Expand Up @@ -432,6 +435,8 @@ pub(crate) struct DecisionExecutionReadiness {
#[serde(default)]
proposed_issue_summaries: Vec<String>,
#[serde(default)]
promotion_targets: Vec<String>,
#[serde(default)]
conflict_domains: Vec<String>,
#[serde(default)]
queue_intent: Vec<String>,
Expand All @@ -454,6 +459,10 @@ impl DecisionExecutionReadiness {
&self.proposed_issue_summaries
}

pub(crate) fn promotion_targets(&self) -> &[String] {
&self.promotion_targets
}

pub(crate) fn conflict_domains(&self) -> &[String] {
&self.conflict_domains
}
Expand Down Expand Up @@ -482,6 +491,7 @@ impl DecisionExecutionReadiness {
"decision contract proposed_issue_summaries",
&self.proposed_issue_summaries,
)?;
validate_string_list("decision contract promotion_targets", &self.promotion_targets)?;
validate_string_list("decision contract conflict_domains", &self.conflict_domains)?;
validate_string_list("decision contract queue_intent", &self.queue_intent)?;

Expand Down Expand Up @@ -709,6 +719,10 @@ fn decision_contract_record_version() -> u16 {
DECISION_CONTRACT_RECORD_VERSION
}

fn default_research_evidence_kind() -> String {
String::from("unspecified")
}

fn validate_required(name: &str, value: &str) -> Result<()> {
if value.trim().is_empty() {
eyre::bail!("{name} must not be empty.");
Expand Down
57 changes: 57 additions & 0 deletions apps/decodex/src/research_design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ pub(crate) struct ResearchDesignRunInput {
#[serde(default)]
proposed_issue_summaries: Vec<String>,
#[serde(default)]
promotion_targets: Vec<String>,
#[serde(default)]
conflict_domains: Vec<String>,
#[serde(default)]
queue_intent: Vec<String>,
Expand Down Expand Up @@ -122,6 +124,7 @@ impl ResearchDesignRunInput {
validation_expectations: Vec::new(),
risk_notes: Vec::new(),
proposed_issue_summaries: Vec::new(),
promotion_targets: Vec::new(),
conflict_domains: Vec::new(),
queue_intent: Vec::new(),
private_evidence_refs: Vec::new(),
Expand Down Expand Up @@ -157,6 +160,8 @@ impl ResearchProvenanceInput {
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ResearchEvidenceInput {
#[serde(default = "default_input_evidence_kind")]
kind: String,
claim: String,
support: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -165,6 +170,7 @@ pub(crate) struct ResearchEvidenceInput {
impl ResearchEvidenceInput {
fn normalized(self) -> Result<Self> {
Ok(Self {
kind: normalize_required_text("evidence.kind", self.kind)?,
claim: normalize_required_text("evidence.claim", self.claim)?,
support: normalize_required_text("evidence.support", self.support)?,
source_ref: normalize_optional_text("evidence.source_ref", self.source_ref)?,
Expand Down Expand Up @@ -308,6 +314,7 @@ pub(crate) struct ResearchDesignRunReport {
pub(crate) missing_decisions: Vec<String>,
pub(crate) blockers: Vec<String>,
pub(crate) proposed_issue_summaries: Vec<String>,
pub(crate) promotion_targets: Vec<String>,
pub(crate) conflict_domains: Vec<String>,
pub(crate) private_evidence_ref_count: usize,
pub(crate) public_projection_ref_count: usize,
Expand All @@ -332,6 +339,7 @@ impl ResearchDesignRunReport {
.execution_readiness()
.proposed_issue_summaries()
.to_vec(),
promotion_targets: contract.execution_readiness().promotion_targets().to_vec(),
conflict_domains: contract.execution_readiness().conflict_domains().to_vec(),
private_evidence_ref_count: input.private_evidence_refs.len(),
public_projection_ref_count: input.public_projection_refs.len(),
Expand Down Expand Up @@ -385,6 +393,7 @@ struct NormalizedResearchDesignInput {
validation_expectations: Vec<String>,
risk_notes: Vec<String>,
proposed_issue_summaries: Vec<String>,
promotion_targets: Vec<String>,
conflict_domains: Vec<String>,
queue_intent: Vec<String>,
private_evidence_refs: Vec<ResearchPrivateEvidenceRefInput>,
Expand Down Expand Up @@ -452,6 +461,7 @@ impl NormalizedResearchDesignInput {
"proposed_issue_summaries",
input.proposed_issue_summaries,
)?,
promotion_targets: normalize_text_list("promotion_targets", input.promotion_targets)?,
conflict_domains: normalize_text_list("conflict_domains", input.conflict_domains)?,
queue_intent: normalize_text_list("queue_intent", input.queue_intent)?,
private_evidence_refs: input
Expand Down Expand Up @@ -484,6 +494,9 @@ impl NormalizedResearchDesignInput {
if self.evidence.is_empty() {
eyre::bail!("decision-ready research requires at least one evidence claim.");
}
if self.evidence.iter().any(|evidence| evidence.kind == "unspecified") {
eyre::bail!("decision-ready research requires an evidence kind for each claim.");
}
if self.options.is_empty() {
eyre::bail!("decision-ready research requires at least one option comparison.");
}
Expand All @@ -500,6 +513,9 @@ impl NormalizedResearchDesignInput {
"decision-ready research requires at least one proposed issue summary for downstream shaping."
);
}
if self.promotion_targets.is_empty() {
eyre::bail!("decision-ready research requires at least one promotion target.");
}
if !self.unresolved_decisions.is_empty() || !self.evidence_gaps.is_empty() {
eyre::bail!(
"decision-ready research cannot carry unresolved decisions or evidence gaps."
Expand Down Expand Up @@ -696,6 +712,7 @@ fn build_decision_contract(
"validation_expectations": input.validation_expectations,
"risk_notes": risk_notes(input),
"proposed_issue_summaries": input.proposed_issue_summaries,
"promotion_targets": input.promotion_targets,
"conflict_domains": input.conflict_domains,
"queue_intent": queue_intent(input),
},
Expand Down Expand Up @@ -764,6 +781,7 @@ fn research_evidence_json(input: &NormalizedResearchDesignInput) -> Vec<Value> {
.iter()
.map(|item| {
serde_json::json!({
"kind": item.kind,
"claim": item.claim,
"support": item.support,
"source_ref": item.source_ref,
Expand Down Expand Up @@ -871,6 +889,10 @@ fn default_feedback(outcome: ResearchDesignOutcome) -> &'static str {
}
}

fn default_input_evidence_kind() -> String {
String::from("unspecified")
}

fn generated_contract_id(input: &ResearchDesignRunInput) -> Result<String> {
let slug = intent_slug(&input.intent);
let encoded = serde_json::to_vec(input)?;
Expand Down Expand Up @@ -954,6 +976,7 @@ mod tests {
summary: String::from("Research output is latent until accepted or promoted."),
}],
evidence: vec![ResearchEvidenceInput {
kind: String::from("repo_source"),
claim: String::from("Decision-ready research can shape downstream issues."),
support: String::from(
"The compiler carries objectives, validation expectations, and issue summaries.",
Expand Down Expand Up @@ -993,6 +1016,7 @@ mod tests {
proposed_issue_summaries: vec![String::from(
"Wire natural-language research trigger into Decodex plugin surface.",
)],
promotion_targets: vec![String::from("plugins/decodex/skills")],
conflict_domains: vec![String::from("runtime")],
queue_intent: Vec::new(),
private_evidence_refs: vec![ResearchPrivateEvidenceRefInput {
Expand Down Expand Up @@ -1060,6 +1084,35 @@ mod tests {
};

assert!(missing_validation_error.to_string().contains("requires validation expectations"));

let mut missing_evidence_kind = decision_ready_input();

missing_evidence_kind.evidence[0].kind = String::from("unspecified");

let missing_evidence_kind_error =
match research_design::compile_research_design_run(missing_evidence_kind, "decodex") {
Ok(_) => panic!("decision-ready research should require evidence kinds"),
Err(error) => error,
};

assert!(missing_evidence_kind_error.to_string().contains("requires an evidence kind"));

let mut missing_promotion_target = decision_ready_input();

missing_promotion_target.promotion_targets.clear();

let missing_promotion_target_error =
match research_design::compile_research_design_run(missing_promotion_target, "decodex")
{
Ok(_) => panic!("decision-ready research should require a promotion target"),
Err(error) => error,
};

assert!(
missing_promotion_target_error
.to_string()
.contains("requires at least one promotion target")
);
}

#[test]
Expand Down Expand Up @@ -1089,6 +1142,10 @@ mod tests {
record.contract().execution_readiness().proposed_issue_summaries(),
&[String::from("Wire natural-language research trigger into Decodex plugin surface.")]
);
assert_eq!(
record.contract().execution_readiness().promotion_targets(),
&[String::from("plugins/decodex/skills")]
);
assert!(
store
.list_linear_execution_events("decodex", "XY-860")
Expand Down
3 changes: 3 additions & 0 deletions docs/decisions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ Question this index answers: "why was it designed this way?"
- [`decodex-plugin-source.md`](./decodex-plugin-source.md) records why this repository
owns the canonical Decodex plugin and why generic Playbook guidance should only keep
portable routing.
- [`mcp-capability-gateway-and-skill-slimming.md`](./mcp-capability-gateway-and-skill-slimming.md)
records why Decodex should introduce an MCP capability gateway while slimming
skills into static routing, authority, and safety entrypoints.
- [`radar-control-plane-publisher.md`](./radar-control-plane-publisher.md) records the
stable capability names for upstream Codex intelligence, retained-lane orchestration,
and public publishing after the repository integration.
Expand Down
Loading