Skip to main content

aidens_cli/
package.rs

1use super::*;
2
3pub fn package_command(command: PackageCommand) -> Result<String> {
4    match command {
5        PackageCommand::Examples { root } => {
6            let manifest = example_app_manifest(Path::new(&root))?;
7            report_json_with_support_tiers(&manifest, support_tiers_from_examples(&manifest))
8        }
9        PackageCommand::InstallSmoke {
10            root,
11            config,
12            include_verify,
13        } => {
14            let receipt = install_smoke_receipt(Path::new(&root), &config, include_verify)?;
15            Ok(serde_json::to_string_pretty(&receipt)?)
16        }
17        PackageCommand::Readiness {
18            root,
19            config,
20            include_verify,
21        } => {
22            let report = release_readiness_report(Path::new(&root), &config, include_verify)?;
23            let encoded = report_json_with_support_tiers(
24                &report,
25                support_tiers_from_surfaces(&report.surfaces),
26            )?;
27            if report.blocks_release() {
28                bail!("{encoded}");
29            }
30            Ok(encoded)
31        }
32        PackageCommand::CompletionAudit {
33            root,
34            config,
35            gate_results,
36        } => {
37            let report = completion_audit_report(Path::new(&root), &config, gate_results)?;
38            Ok(serde_json::to_string_pretty(&report)?)
39        }
40    }
41}
42
43pub fn release_readiness_report(
44    root: &Path,
45    config: &str,
46    include_verify: bool,
47) -> Result<ReleaseReadinessReportV1> {
48    let manifest = example_app_manifest(root)?;
49    let smoke = install_smoke_receipt(root, config, include_verify)?;
50    let findings = scan_public_docs_for_scaffold_claims(root)?;
51    Ok(ReleaseReadinessReportV1::new(
52        release_surfaces(),
53        findings,
54        manifest,
55        smoke,
56    ))
57}
58
59pub(crate) const P24_REQUIRED_GATE_COMMANDS: &[&str] = &[
60    "cargo fmt --all --check",
61    "cargo check --workspace --all-targets --all-features",
62    "cargo test --workspace --all-targets --all-features",
63    "cargo clippy --workspace --all-targets --all-features -- -D warnings",
64    "bash scripts/p24_verify.sh",
65];
66
67const SOURCE_BASIS_HEADER_PREFIX: &str = "# Source Basis - ";
68
69pub fn completion_audit_report(
70    root: &Path,
71    config: &str,
72    gate_results: Vec<String>,
73) -> Result<CompletionAuditReportV1> {
74    let release_readiness = release_readiness_report(root, config, false)?;
75    let source_basis = workspace_source_basis_label(root);
76    let traceability_matrix = cross_pass_traceability_matrix(root)?;
77    let release_artifact_manifest = release_artifact_manifest(root)?;
78    let known_limitations = known_limitations_register();
79    let regression_debt = regression_debt_ledger();
80    Ok(CompletionAuditReportV1::new(
81        source_basis,
82        parse_gate_results(gate_results)?,
83        release_readiness,
84        traceability_matrix,
85        release_artifact_manifest,
86        known_limitations,
87        regression_debt,
88    ))
89}
90
91fn parse_gate_results(gate_results: Vec<String>) -> Result<Vec<GateCommandResultV1>> {
92    let mut parsed = BTreeMap::new();
93    for raw in gate_results {
94        let (command, status) = raw
95            .rsplit_once('=')
96            .or_else(|| raw.split_once('|'))
97            .with_context(|| {
98                format!("gate result must be '<command>=passed|failed|waived|not-run': {raw}")
99            })?;
100        let command = command.trim();
101        let status = status.trim();
102        let result = match status {
103            "passed" => GateCommandResultV1::passed(command, "cli:gate-result"),
104            "failed" => GateCommandResultV1::failed(
105                command,
106                1,
107                "external gate result reported failure",
108                "gate-command-failed",
109            ),
110            "waived" => GateCommandResultV1::waived(command, "governance-waiver:external"),
111            "not-run" => GateCommandResultV1::not_run(command, "gate-command-not-run"),
112            _ => bail!("unknown gate result status for {command}: {status}"),
113        };
114        parsed.insert(command.to_string(), result);
115    }
116    let mut results = Vec::new();
117    for command in P24_REQUIRED_GATE_COMMANDS {
118        results.push(
119            parsed.remove(*command).unwrap_or_else(|| {
120                GateCommandResultV1::not_run(*command, "gate-result-not-supplied")
121            }),
122        );
123    }
124    results.extend(parsed.into_values());
125    Ok(results)
126}
127
128fn release_artifact_manifest(root: &Path) -> Result<ReleaseArtifactManifestV1> {
129    let required = [
130        ("AGENTS.md", ReleaseArtifactKindV1::Document),
131        ("SOURCE_BASIS.md", ReleaseArtifactKindV1::Document),
132        ("BUILD_ORDER_DAG.md", ReleaseArtifactKindV1::Document),
133        ("README.md", ReleaseArtifactKindV1::Document),
134        ("STATUS.md", ReleaseArtifactKindV1::Document),
135        (
136            "ARTIFACT_SCHEMA_REGISTRY.md",
137            ReleaseArtifactKindV1::Document,
138        ),
139        ("pass_manifest.json", ReleaseArtifactKindV1::Manifest),
140        ("scripts/verify.sh", ReleaseArtifactKindV1::Script),
141        (
142            "scripts/assert_no_fake_completion.sh",
143            ReleaseArtifactKindV1::Script,
144        ),
145        (
146            "scripts/assert_no_scaffold_promoted.sh",
147            ReleaseArtifactKindV1::Script,
148        ),
149        (".github/workflows/ci.yml", ReleaseArtifactKindV1::Ci),
150        (
151            "schemas/generated_schema_manifest_v1.json",
152            ReleaseArtifactKindV1::Manifest,
153        ),
154        ("schemas", ReleaseArtifactKindV1::Schema),
155        ("tests/fixtures", ReleaseArtifactKindV1::Fixture),
156        ("examples", ReleaseArtifactKindV1::Example),
157        ("passes", ReleaseArtifactKindV1::Document),
158        ("docs", ReleaseArtifactKindV1::Document),
159        ("handoffs", ReleaseArtifactKindV1::Handoff),
160    ];
161    let mut artifacts = Vec::new();
162    for (path, kind) in required {
163        artifacts.push(release_artifact_entry(root, path, kind, true)?);
164    }
165    Ok(ReleaseArtifactManifestV1::new(
166        "aidens-p19-release",
167        workspace_source_basis_label(root),
168        artifacts,
169    ))
170}
171
172pub(crate) fn workspace_source_basis_label(root: &Path) -> String {
173    let run_basis = source_basis_from_markdown(&root.join("SOURCE_BASIS.md"))
174        .or_else(|| {
175            current_run_from_markdown(&root.join("docs").join("codex-runs").join("CURRENT_RUN.md"))
176        })
177        .unwrap_or_else(|| "current-workspace".to_string());
178    format!("aidens-{run_basis}-current-workspace")
179}
180
181fn source_basis_from_markdown(path: &Path) -> Option<String> {
182    let text = std::fs::read_to_string(path).ok()?;
183    for line in text.lines() {
184        let line = line.trim();
185        if line.starts_with(SOURCE_BASIS_HEADER_PREFIX) {
186            let raw = line.trim_start_matches(SOURCE_BASIS_HEADER_PREFIX);
187            return Some(normalize_basis_token(raw));
188        }
189    }
190    None
191}
192
193fn current_run_from_markdown(path: &Path) -> Option<String> {
194    let text = std::fs::read_to_string(path).ok()?;
195    for line in text.lines() {
196        let line = line.trim();
197        if !line.to_lowercase().starts_with("current run:") {
198            continue;
199        }
200        if let Some(run) = line.split_once('`').and_then(|(_, rest)| {
201            let (run, _) = rest.split_once('`')?;
202            Some(run)
203        }) {
204            return Some(normalize_basis_token(run));
205        }
206    }
207    None
208}
209
210fn normalize_basis_token(raw: &str) -> String {
211    raw.chars()
212        .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '-')
213        .collect::<String>()
214        .to_lowercase()
215}
216
217fn release_artifact_entry(
218    root: &Path,
219    relative: &str,
220    kind: ReleaseArtifactKindV1,
221    required: bool,
222) -> Result<ReleaseArtifactEntryV1> {
223    let path = root.join(relative);
224    if !path.exists() {
225        return Ok(ReleaseArtifactEntryV1::missing(relative, kind, required));
226    }
227    let (digest, byte_len) = if path.is_dir() {
228        digest_directory(root, &path)?
229    } else {
230        let bytes =
231            std::fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?;
232        (
233            non_authoritative_text_display_digest(&String::from_utf8_lossy(&bytes)),
234            bytes.len() as u64,
235        )
236    };
237    Ok(ReleaseArtifactEntryV1::present(
238        relative, kind, required, digest, byte_len,
239    ))
240}
241
242fn digest_directory(root: &Path, dir: &Path) -> Result<(String, u64)> {
243    let mut files = Vec::new();
244    collect_files_recursive(dir, &mut files)?;
245    files.sort();
246    let mut material = String::new();
247    let mut bytes_total = 0_u64;
248    for path in files {
249        let bytes =
250            std::fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?;
251        bytes_total += bytes.len() as u64;
252        let relative = relative_path(root, &path)?;
253        material.push_str(&relative);
254        material.push('\0');
255        material.push_str(&non_authoritative_text_display_digest(
256            &String::from_utf8_lossy(&bytes),
257        ));
258        material.push('\n');
259    }
260    Ok((
261        non_authoritative_text_display_digest(&material),
262        bytes_total,
263    ))
264}
265
266fn collect_files_recursive(root: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
267    if !root.exists() {
268        return Ok(());
269    }
270    for entry in
271        std::fs::read_dir(root).with_context(|| format!("failed to read {}", root.display()))?
272    {
273        let path = entry?.path();
274        if path.is_dir() {
275            collect_files_recursive(&path, files)?;
276        } else {
277            files.push(path);
278        }
279    }
280    Ok(())
281}
282
283fn cross_pass_traceability_matrix(root: &Path) -> Result<CrossPassTraceabilityMatrixV1> {
284    let tasks_root = root.join("tasks");
285    let mut rows = Vec::new();
286    for task_path in sorted_json_files(&tasks_root)? {
287        let value: serde_json::Value = serde_json::from_slice(&std::fs::read(&task_path)?)?;
288        let Some(pass_id) = value.get("pass_id").and_then(serde_json::Value::as_str) else {
289            continue;
290        };
291        if !is_p19_release_pass_id(pass_id) {
292            continue;
293        }
294        let title = value
295            .get("title")
296            .and_then(serde_json::Value::as_str)
297            .unwrap_or("pass acceptance");
298        let crates = string_array(&value, "crates");
299        let artifacts = string_array(&value, "artifacts");
300        let acceptance = string_array(&value, "acceptance");
301        let pass_doc = value
302            .get("pass_doc")
303            .and_then(serde_json::Value::as_str)
304            .unwrap_or("passes")
305            .to_string();
306        let prompt_file = value
307            .get("prompt_file")
308            .and_then(serde_json::Value::as_str)
309            .unwrap_or("prompts")
310            .to_string();
311        let handoff = handoff_for_pass(root, pass_id);
312        let mut docs = vec![pass_doc, prompt_file, "STATUS.md".into()];
313        let mut evidence_refs = vec!["STATUS.md".into()];
314        if let Some(handoff) = handoff {
315            docs.push(handoff.clone());
316            evidence_refs.push(handoff);
317        }
318        let state = if pass_id == "P19"
319            || evidence_refs
320                .iter()
321                .any(|path| path.starts_with("handoffs/"))
322        {
323            PassCompletionStateV1::Done
324        } else {
325            PassCompletionStateV1::Deferred
326        };
327        rows.push(
328            CrossPassTraceabilityRowV1::new(format!("{pass_id}:acceptance"), pass_id, title, state)
329                .with_crates(crates)
330                .with_artifacts(artifacts)
331                .with_tests(vec![
332                    "cargo test --workspace".into(),
333                    "bash scripts/verify.sh".into(),
334                ])
335                .with_docs(docs)
336                .with_acceptance_gates(acceptance)
337                .with_evidence(evidence_refs),
338        );
339    }
340    for run_dir in ["p27", "p24", "p23", "p22"] {
341        if rows.is_empty() {
342            rows.extend(handoff_traceability_rows(root, run_dir)?);
343        }
344    }
345    Ok(CrossPassTraceabilityMatrixV1::new(rows))
346}
347
348fn handoff_traceability_rows(
349    root: &Path,
350    run_dir: &str,
351) -> Result<Vec<CrossPassTraceabilityRowV1>> {
352    let reports_root = root.join("handoffs").join(run_dir);
353    let run_id = run_dir.to_ascii_uppercase();
354    let verifier_command = format!("bash scripts/{}_verify.sh", run_dir);
355    let mut rows = Vec::new();
356    for report_path in sorted_markdown_files(&reports_root)? {
357        let Some(name) = report_path.file_name().and_then(|name| name.to_str()) else {
358            continue;
359        };
360        let Some(phase) = name
361            .strip_prefix("PHASE_")
362            .and_then(|rest| rest.strip_suffix("_REPORT.md"))
363        else {
364            continue;
365        };
366        let rel = relative_path(root, &report_path)?;
367        rows.push(
368            CrossPassTraceabilityRowV1::new(
369                format!("{run_id}:phase-{phase}"),
370                &run_id,
371                format!("{run_id} phase {phase} acceptance evidence"),
372                PassCompletionStateV1::Done,
373            )
374            .with_crates(vec!["AiDENs workspace".into()])
375            .with_artifacts(vec![
376                "z.py-package-certifier".into(),
377                format!("{run_dir}-assertion-suite"),
378                "phase-handoff-report".into(),
379            ])
380            .with_tests(vec![
381                "cargo test --workspace --all-targets".into(),
382                verifier_command.clone(),
383            ])
384            .with_docs(vec![
385                "README.md".into(),
386                "STATUS.md".into(),
387                "SOURCE_BASIS.md".into(),
388                rel.clone(),
389            ])
390            .with_acceptance_gates(vec![format!(
391                "{run_id} phase acceptance gates recorded in handoff"
392            )])
393            .with_evidence(vec![rel]),
394        );
395    }
396    Ok(rows)
397}
398
399fn is_p19_release_pass_id(pass_id: &str) -> bool {
400    let Some(number) = pass_id
401        .strip_prefix('P')
402        .filter(|suffix| suffix.len() == 2)
403        .and_then(|suffix| suffix.parse::<u8>().ok())
404    else {
405        return false;
406    };
407    number <= 19
408}
409
410fn sorted_markdown_files(root: &Path) -> Result<Vec<PathBuf>> {
411    if !root.exists() {
412        return Ok(Vec::new());
413    }
414    let mut paths = Vec::new();
415    for entry in
416        std::fs::read_dir(root).with_context(|| format!("failed to read {}", root.display()))?
417    {
418        let path = entry?.path();
419        if path.extension().and_then(|extension| extension.to_str()) == Some("md") {
420            paths.push(path);
421        }
422    }
423    paths.sort();
424    Ok(paths)
425}
426
427fn sorted_json_files(root: &Path) -> Result<Vec<PathBuf>> {
428    if !root.exists() {
429        return Ok(Vec::new());
430    }
431    let mut paths = Vec::new();
432    for entry in
433        std::fs::read_dir(root).with_context(|| format!("failed to read {}", root.display()))?
434    {
435        let path = entry?.path();
436        if path.extension().and_then(|extension| extension.to_str()) == Some("json") {
437            paths.push(path);
438        }
439    }
440    paths.sort();
441    Ok(paths)
442}
443
444fn string_array(value: &serde_json::Value, key: &str) -> Vec<String> {
445    value
446        .get(key)
447        .and_then(serde_json::Value::as_array)
448        .map(|items| {
449            items
450                .iter()
451                .filter_map(serde_json::Value::as_str)
452                .map(ToOwned::to_owned)
453                .collect()
454        })
455        .unwrap_or_default()
456}
457
458fn handoff_for_pass(root: &Path, pass_id: &str) -> Option<String> {
459    let handoff_root = root.join("handoffs");
460    let entries = std::fs::read_dir(handoff_root).ok()?;
461    for entry in entries.flatten() {
462        let path = entry.path();
463        let file_name = path.file_name()?.to_string_lossy();
464        if file_name.starts_with(pass_id) && file_name.ends_with(".md") {
465            return Some(format!("handoffs/{file_name}"));
466        }
467    }
468    None
469}
470
471fn known_limitations_register() -> KnownLimitationsRegisterV1 {
472    let mut limitations = SCAFFOLD_ONLY_CRATES
473        .iter()
474        .map(|(crate_name, note)| {
475            KnownLimitationV1::deferred(
476                format!("crate:{crate_name}"),
477                format!("{crate_name} remains scaffold-only"),
478                format!("operators must treat this crate as deferred; {note}"),
479            )
480            .with_review_after_pass("post-P19-roadmap")
481        })
482        .collect::<Vec<_>>();
483    limitations.push(
484        KnownLimitationV1::partial(
485            "provider:http-api-boundaries",
486            "OpenAI, OpenRouter, and Anthropic provider routes are diagnostic unavailable boundaries",
487            "mock and local diagnostic routes are supported; HTTP API execution is not claimed",
488        )
489        .with_workaround("use examples/aidens.mock.toml for release smoke"),
490    );
491    limitations.push(
492        KnownLimitationV1::partial(
493            "release:binary-packaging",
494            "P19 produces release manifests and audit artifacts, not signed binary installers",
495            "operators can audit the source package but installer publication remains an external release operation",
496        )
497        .with_workaround("ship the source tree plus generated schemas and fixtures listed in the release artifact manifest"),
498    );
499    KnownLimitationsRegisterV1::new(limitations)
500}
501
502fn regression_debt_ledger() -> RegressionDebtLedgerV1 {
503    RegressionDebtLedgerV1::new(vec![
504        RegressionDebtItemV1::guarded(
505            "release:docs",
506            "public docs must not claim scaffold-only crates are complete",
507            "scripts/assert_no_scaffold_promoted.sh",
508            vec!["bash scripts/verify.sh".into()],
509        ),
510        RegressionDebtItemV1::guarded(
511            "schemas:registry",
512            "schema files can drift from Rust-owned artifact types",
513            "aidens schemas check",
514            vec!["cargo test -p aidens-cli schemas".into()],
515        ),
516        RegressionDebtItemV1::guarded(
517            "provider:routes",
518            "provider labels can regress into native-tool claims without executable proof",
519            "provider route/readiness tests",
520            vec!["cargo test -p aidens-provider-kit".into()],
521        ),
522        RegressionDebtItemV1::guarded(
523            "memory:required-mode",
524            "MemoryRequired paths can regress into running without a durable store",
525            "doctor/run memory-required tests",
526            vec!["cargo test -p aidens-cli memory_required".into()],
527        ),
528    ])
529}
530
531pub fn example_app_manifest(root: &Path) -> Result<ExampleAppManifestV1> {
532    let examples_root = root.join("examples");
533    let mut examples = Vec::new();
534    for path in example_config_paths(&examples_root)? {
535        let loaded = load_config_file(&path)?;
536        let relative = relative_path(root, &path)?;
537        let profile_id = loaded
538            .config
539            .profile_id
540            .clone()
541            .unwrap_or_else(|| "chat-only".into());
542        let mut entry = ExampleAppEntryV1::new(
543            relative.clone(),
544            profile_id.clone(),
545            loaded.config.provider.kind.clone(),
546            loaded.config.memory_mode.clone(),
547            example_status(&loaded.config, &profile_id),
548        )
549        .with_command(format!("aidens provider-check --config {relative}"));
550        if loaded.config.provider.kind == "mock" {
551            entry = entry.with_command(format!("aidens run --config {relative} hello"));
552        }
553        if profile_id == "coding-agent" {
554            entry = entry.with_command(format!("aidens inspect-tools --config {relative}"));
555        }
556        for reason in example_reason_codes(&loaded.config, &profile_id) {
557            entry = entry.with_reason(reason);
558        }
559        examples.push(entry);
560    }
561    Ok(ExampleAppManifestV1::new(
562        examples,
563        SCAFFOLD_ONLY_CRATES
564            .iter()
565            .map(|(crate_name, note)| format!("{crate_name}: {note}"))
566            .collect(),
567    ))
568}
569
570fn install_smoke_receipt(
571    root: &Path,
572    config: &str,
573    include_verify: bool,
574) -> Result<InstallSmokeReportV1> {
575    let smoke_root = root
576        .join("target")
577        .join("p14-install-smoke")
578        .join(unique_smoke_segment());
579    std::fs::create_dir_all(&smoke_root)
580        .with_context(|| format!("failed to create {}", smoke_root.display()))?;
581    let mut steps = Vec::new();
582
583    let app_dir = smoke_root.join("new-user-app");
584    match scaffold_project_at(AiDENsProfile::CodingAgent, &app_dir) {
585        Ok(summary) => steps.push(InstallSmokeStepV1::passed(
586            "new-app",
587            format!("aidens new coding-agent {}", app_dir.display()),
588            &format!("created {}", summary.app_dir.display()),
589        )),
590        Err(error) => steps.push(InstallSmokeStepV1::failed(
591            "new-app",
592            format!("aidens new coding-agent {}", app_dir.display()),
593            &error.to_string(),
594            "new-app-failed",
595        )),
596    }
597
598    let config_path = root.join(config);
599    let config_arg = config_path.display().to_string();
600    steps.push(smoke_step(
601        "provider-check",
602        format!("aidens provider-check --config {config}"),
603        || provider_check(Some(config_arg.clone())),
604    ));
605    steps.push(smoke_step(
606        "inspect-tools",
607        format!("aidens inspect-tools --config {config}"),
608        || inspect_tools(Some(config_arg.clone())),
609    ));
610    steps.push(smoke_step(
611        "mock-turn",
612        format!("aidens run --config {config} hello"),
613        || run_once_command(Some(config_arg.clone()), vec!["hello".into()]),
614    ));
615    steps.push(smoke_step(
616        "receipts-list",
617        format!("aidens receipts list --config {config}"),
618        || {
619            receipts_command(EventLogCommand::List {
620                store: None,
621                config: Some(config_arg.clone()),
622            })
623        },
624    ));
625    if include_verify {
626        steps.push(smoke_step("verify", "bash scripts/verify.sh", || {
627            run_shell_command(root, "bash", &["scripts/verify.sh"])
628        }));
629    } else {
630        steps.push(InstallSmokeStepV1::passed(
631            "verify-script-present",
632            "test -f scripts/verify.sh",
633            &root.join("scripts").join("verify.sh").exists().to_string(),
634        ));
635    }
636
637    Ok(InstallSmokeReportV1::new(steps))
638}
639
640fn smoke_step(
641    step: impl Into<String>,
642    command: impl Into<String>,
643    run: impl FnOnce() -> Result<String>,
644) -> InstallSmokeStepV1 {
645    let step = step.into();
646    let command = command.into();
647    match run() {
648        Ok(output) => InstallSmokeStepV1::passed(step, command, &output),
649        Err(error) => {
650            InstallSmokeStepV1::failed(step, command, &error.to_string(), "smoke-step-failed")
651        }
652    }
653}
654
655fn run_shell_command(root: &Path, program: &str, args: &[&str]) -> Result<String> {
656    let output = std::process::Command::new(program)
657        .args(args)
658        .current_dir(root)
659        .output()
660        .with_context(|| format!("failed to run {program} {}", args.join(" ")))?;
661    let mut combined = String::new();
662    combined.push_str(&String::from_utf8_lossy(&output.stdout));
663    combined.push_str(&String::from_utf8_lossy(&output.stderr));
664    if !output.status.success() {
665        bail!("{program} {} failed: {combined}", args.join(" "));
666    }
667    Ok(combined)
668}
669
670fn example_config_paths(examples_root: &Path) -> Result<Vec<PathBuf>> {
671    if !examples_root.exists() {
672        return Ok(Vec::new());
673    }
674    let mut paths = Vec::new();
675    for entry in std::fs::read_dir(examples_root).with_context(|| {
676        format!(
677            "failed to read examples directory {}",
678            examples_root.display()
679        )
680    })? {
681        let path = entry?.path();
682        if path.extension().and_then(|extension| extension.to_str()) == Some("toml") {
683            paths.push(path);
684        }
685    }
686    paths.sort();
687    Ok(paths)
688}
689
690fn example_status(cfg: &AiDENsConfigV1, profile_id: &str) -> ReleaseSurfaceStateV1 {
691    if provider_readiness_for_spec(&provider_spec_from_config(&cfg.provider)).executable {
692        if matches!(
693            profile_id,
694            "memory-agent" | "autonomous-daemon" | "research-workbench"
695        ) || cfg.provider.kind == "ollama"
696        {
697            ReleaseSurfaceStateV1::Partial
698        } else {
699            ReleaseSurfaceStateV1::Supported
700        }
701    } else if is_disabled_provider(&cfg.provider.kind) {
702        ReleaseSurfaceStateV1::Deferred
703    } else {
704        ReleaseSurfaceStateV1::Degraded
705    }
706}
707
708fn example_reason_codes(cfg: &AiDENsConfigV1, profile_id: &str) -> Vec<String> {
709    let mut reasons = Vec::new();
710    if matches!(
711        profile_id,
712        "memory-agent" | "autonomous-daemon" | "research-workbench"
713    ) {
714        reasons.push(format!("profile-surface-partial:{profile_id}"));
715    }
716    if !provider_readiness_for_spec(&provider_spec_from_config(&cfg.provider)).executable {
717        reasons.push("provider-not-executable-in-local-smoke".into());
718    }
719    if cfg.provider.kind == "ollama" {
720        reasons.push("provider-local-service-required:ollama".into());
721        reasons.push("provider-surface-partial:ollama-chat".into());
722    }
723    if cfg.memory_mode == MemoryModeV1::Required && cfg.memory.store_root.is_none() {
724        reasons.push("memory-required-without-store".into());
725    }
726    reasons
727}
728
729fn release_surfaces() -> Vec<ReleaseSurfaceV1> {
730    let mut surfaces = vec![
731        ReleaseSurfaceV1::new(
732            "cli:status",
733            ReleaseSurfaceStateV1::Supported,
734            "operator status reports provider, memory, receipts, degraded, and blocked modes",
735        )
736        .with_command("aidens status --config examples/aidens.mock.toml"),
737        ReleaseSurfaceV1::new(
738            "cli:provider-check",
739            ReleaseSurfaceStateV1::Supported,
740            "provider-check uses executable backend truth",
741        )
742        .with_command("aidens provider-check --config examples/aidens.mock.toml"),
743        ReleaseSurfaceV1::new(
744            "cli:tools",
745            ReleaseSurfaceStateV1::Supported,
746            "tool listing distinguishes declared, registered, executable, hidden, and blocked tools",
747        )
748        .with_command("aidens tools inspect --config examples/aidens.mock.toml"),
749        ReleaseSurfaceV1::new(
750            "cli:receipts",
751            ReleaseSurfaceStateV1::Supported,
752            "receipt commands inspect durable NDJSON evidence",
753        )
754        .with_command("aidens receipts list --config examples/aidens.mock.toml"),
755        ReleaseSurfaceV1::new(
756            "cli:schemas",
757            ReleaseSurfaceStateV1::Supported,
758            "schema generation and compatibility checks are Rust-owned",
759        )
760        .with_command("aidens schemas check"),
761        ReleaseSurfaceV1::new(
762            "cli:memory",
763            ReleaseSurfaceStateV1::Partial,
764            "memory status and runtime views exist; profile wiring remains explicit",
765        )
766        .with_command("aidens memory status --config examples/aidens.memory.toml"),
767        ReleaseSurfaceV1::new(
768            "cli:queue",
769            ReleaseSurfaceStateV1::Partial,
770            "queue/daemon substrate exists; daemon profile packaging is deferred",
771        )
772        .with_command("aidens queue list --root target/aidens-daemon --name default"),
773        ReleaseSurfaceV1::new(
774            "cli:package",
775            ReleaseSurfaceStateV1::Supported,
776            "package readiness blocks on false scaffold claims and failed smoke steps",
777        )
778        .with_command("aidens package readiness"),
779    ];
780    surfaces.extend(SCAFFOLD_ONLY_CRATES.iter().map(|(crate_name, note)| {
781        ReleaseSurfaceV1::new(
782            format!("crate:{crate_name}"),
783            ReleaseSurfaceStateV1::Deferred,
784            format!("scaffold-only; {note}"),
785        )
786    }));
787    surfaces
788}
789
790fn scan_public_docs_for_scaffold_claims(root: &Path) -> Result<Vec<PublicDocFindingV1>> {
791    let mut paths = vec![
792        root.join("README.md"),
793        root.join("STATUS.md"),
794        root.join("STATUS_TEMPLATE.md"),
795        root.join("ARTIFACT_SCHEMA_REGISTRY.md"),
796    ];
797    collect_markdown_paths(&root.join("docs"), &mut paths)?;
798    paths.sort();
799    paths.dedup();
800
801    let mut findings = Vec::new();
802    for path in paths.into_iter().filter(|path| path.exists()) {
803        let contents = std::fs::read_to_string(&path)
804            .with_context(|| format!("failed to read public doc {}", path.display()))?;
805        for (index, line) in contents.lines().enumerate() {
806            let lower = line.to_ascii_lowercase();
807            for (crate_name, _) in SCAFFOLD_ONLY_CRATES {
808                if public_doc_line_promotes_scaffold(&lower, crate_name) {
809                    findings.push(PublicDocFindingV1::scaffold_claim(
810                        relative_path(root, &path)?,
811                        (index + 1) as u32,
812                        format!("crate:{crate_name}"),
813                        line.trim().chars().take(240).collect::<String>(),
814                    ));
815                }
816            }
817        }
818    }
819    Ok(findings)
820}
821
822fn collect_markdown_paths(root: &Path, paths: &mut Vec<PathBuf>) -> Result<()> {
823    if !root.exists() {
824        return Ok(());
825    }
826    for entry in std::fs::read_dir(root)
827        .with_context(|| format!("failed to read docs directory {}", root.display()))?
828    {
829        let path = entry?.path();
830        if path.is_dir() {
831            collect_markdown_paths(&path, paths)?;
832        } else if path.extension().and_then(|extension| extension.to_str()) == Some("md") {
833            paths.push(path);
834        }
835    }
836    Ok(())
837}
838
839fn public_doc_line_promotes_scaffold(line: &str, crate_name: &str) -> bool {
840    line.contains(crate_name)
841        && [
842            "complete",
843            "ready",
844            "production",
845            "healthy",
846            "implemented",
847            "available",
848        ]
849        .iter()
850        .any(|word| line.contains(word))
851        && ![
852            "scaffold",
853            "deferred",
854            "horizon",
855            "unsupported",
856            "not available",
857            "not implemented",
858        ]
859        .iter()
860        .any(|word| line.contains(word))
861}
862
863fn unique_smoke_segment() -> String {
864    let nanos = SystemTime::now()
865        .duration_since(UNIX_EPOCH)
866        .map(|duration| duration.as_nanos())
867        .unwrap_or_default();
868    format!("{}-{nanos}", std::process::id())
869}