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}