1use crate::core::assets;
9use crate::core::capsule_policy::{GENERATED_POLICY_REL_PATH, default_policy_json_pretty};
10use crate::core::error;
11use crate::core::project_specs::{
12 LOCAL_PROJECT_SPECS, LOCAL_PROJECT_SPECS_ARCHITECTURE, LOCAL_PROJECT_SPECS_INTENT,
13 LOCAL_PROJECT_SPECS_INTERFACES, LOCAL_PROJECT_SPECS_MANIFEST,
14 LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA, LOCAL_PROJECT_SPECS_OPERATIONS,
15 LOCAL_PROJECT_SPECS_README, LOCAL_PROJECT_SPECS_SECURITY, LOCAL_PROJECT_SPECS_SEMANTICS,
16 LOCAL_PROJECT_SPECS_VALIDATION, ProjectSpecManifestEntry, ProjectSpecsManifest, hash_text,
17 repo_signal_fingerprint,
18};
19use crate::plugins::container;
20use std::fs;
21use std::path::{Path, PathBuf};
22
23pub struct ScaffoldOptions {
27 pub target_dir: PathBuf,
29 pub force: bool,
31 pub dry_run: bool,
33 pub agent_files: Vec<String>,
35 pub created_backups: bool,
37 pub all: bool,
39 pub generate_specs: bool,
41 pub diagram_style: DiagramStyle,
43 pub specs_seed: Option<SpecsSeed>,
45}
46
47pub struct ScaffoldSummary {
48 pub entrypoints_created: usize,
49 pub entrypoints_unchanged: usize,
50 pub entrypoints_preserved: usize,
51 pub config_created: usize,
52 pub config_unchanged: usize,
53 pub config_preserved: usize,
54 pub specs_created: usize,
55 pub specs_unchanged: usize,
56 pub specs_preserved: usize,
57}
58
59#[derive(Clone, Copy, Debug)]
60pub enum DiagramStyle {
61 Ascii,
62 Mermaid,
63}
64
65#[derive(Clone, Debug)]
66pub struct SpecsSeed {
67 pub product_name: Option<String>,
68 pub product_summary: Option<String>,
69 pub architecture_direction: Option<String>,
70 pub product_type: Option<String>,
71 pub primary_languages: Vec<String>,
72 pub detected_surfaces: Vec<String>,
73 pub done_criteria: Option<String>,
74}
75
76fn joined_or_fallback(items: &[String], fallback: &str) -> String {
77 if items.is_empty() {
78 fallback.to_string()
79 } else {
80 items.join(", ")
81 }
82}
83
84fn default_test_commands(seed: Option<&SpecsSeed>) -> Vec<String> {
85 let mut commands = Vec::new();
86 let langs = seed.map(|s| s.primary_languages.as_slice()).unwrap_or(&[]);
87 let surfaces = seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]);
88
89 if langs.iter().any(|l| l.contains("rust")) {
90 commands.push("cargo test".to_string());
91 }
92 if surfaces.iter().any(|s| s == "npm")
93 || langs
94 .iter()
95 .any(|l| l.contains("typescript") || l.contains("javascript"))
96 {
97 commands.push("npm test".to_string());
98 }
99 if langs.iter().any(|l| l == "python") {
100 commands.push("pytest".to_string());
101 }
102 if langs.iter().any(|l| l == "go") {
103 commands.push("go test ./...".to_string());
104 }
105 commands
106}
107
108fn has_language(seed: Option<&SpecsSeed>, needle: &str) -> bool {
109 let needle = needle.to_ascii_lowercase();
110 seed.map(|s| {
111 s.primary_languages
112 .iter()
113 .any(|l| l.to_ascii_lowercase().contains(&needle))
114 })
115 .unwrap_or(false)
116}
117
118fn primary_language_name(seed: Option<&SpecsSeed>) -> String {
119 seed.and_then(|s| s.primary_languages.first().cloned())
120 .unwrap_or_else(|| "not detected yet".to_string())
121}
122
123fn language_specific_test_criteria(seed: Option<&SpecsSeed>) -> Vec<String> {
124 if has_language(seed, "rust") {
125 return vec![
126 "`cargo test` passes for unit/integration coverage".to_string(),
127 "`cargo clippy -- -D warnings` passes with no denied lints".to_string(),
128 "`cargo fmt --check` passes on the repo".to_string(),
129 ];
130 }
131 if has_language(seed, "python") {
132 return vec![
133 "`pytest -q` passes for unit/integration scenarios".to_string(),
134 "`ruff check .` passes for lint quality".to_string(),
135 "`mypy .` passes for typed modules in production paths".to_string(),
136 ];
137 }
138 if has_language(seed, "go") {
139 return vec![
140 "`go test ./...` passes for all packages".to_string(),
141 "`go vet ./...` passes with no diagnostics".to_string(),
142 "`gofmt -l .` returns no files".to_string(),
143 ];
144 }
145 if has_language(seed, "typescript") || has_language(seed, "javascript") {
146 return vec![
147 "`npm test` (or `pnpm test`) passes for unit/integration suites".to_string(),
148 "`npm run lint` passes".to_string(),
149 "`npm run typecheck` passes for strict TS projects".to_string(),
150 ];
151 }
152 vec!["Repository test/lint/typecheck commands are defined and wired into CI.".to_string()]
153}
154
155fn language_specific_error_example(seed: Option<&SpecsSeed>) -> String {
156 if has_language(seed, "rust") {
157 return r#"```rust
158#[derive(Debug, thiserror::Error)]
159pub enum ApiError {
160 #[error("validation failed: {0}")]
161 Validation(String),
162 #[error("upstream timeout")]
163 UpstreamTimeout,
164 #[error("conflict: {0}")]
165 Conflict(String),
166}
167```"#
168 .to_string();
169 }
170 if has_language(seed, "python") {
171 return r#"```python
172class ApiError(Exception):
173 def __init__(self, code: str, message: str) -> None:
174 self.code = code
175 self.message = message
176 super().__init__(f"{code}: {message}")
177```"#
178 .to_string();
179 }
180 if has_language(seed, "go") {
181 return r#"```go
182var (
183 ErrValidation = errors.New("validation_failed")
184 ErrTimeout = errors.New("upstream_timeout")
185 ErrConflict = errors.New("conflict")
186)
187```"#
188 .to_string();
189 }
190 r#"```ts
191export enum ApiErrorCode {
192 Validation = "validation_failed",
193 UpstreamTimeout = "upstream_timeout",
194 Conflict = "conflict"
195}
196```"#
197 .to_string()
198}
199
200fn language_specific_supply_chain_tools(seed: Option<&SpecsSeed>) -> Vec<String> {
201 if has_language(seed, "rust") {
202 return vec!["cargo audit", "cargo deny", "cargo vet"]
203 .into_iter()
204 .map(str::to_string)
205 .collect();
206 }
207 if has_language(seed, "python") {
208 return vec!["pip-audit", "safety", "bandit"]
209 .into_iter()
210 .map(str::to_string)
211 .collect();
212 }
213 if has_language(seed, "go") {
214 return vec!["govulncheck", "gosec", "nancy"]
215 .into_iter()
216 .map(str::to_string)
217 .collect();
218 }
219 vec!["npm audit", "osv-scanner", "snyk"]
220 .into_iter()
221 .map(str::to_string)
222 .collect()
223}
224
225fn language_specific_logging_hint(seed: Option<&SpecsSeed>) -> String {
226 if has_language(seed, "rust") {
227 return "Use `tracing` + `tracing-subscriber` with structured JSON output and request correlation ids.".to_string();
228 }
229 if has_language(seed, "python") {
230 return "Use `structlog` (or stdlib logging JSON formatter) with request_id, task_id, and outcome fields.".to_string();
231 }
232 if has_language(seed, "go") {
233 return "Use `zap` or `zerolog` with structured fields and propagated context ids."
234 .to_string();
235 }
236 "Use structured logging (pino/winston) with request_id, actor, latency_ms, and error_code fields.".to_string()
237}
238
239fn adaptive_topology_diagram(style: DiagramStyle, seed: Option<&SpecsSeed>) -> String {
240 let product_type = seed
241 .and_then(|s| s.product_type.as_deref())
242 .unwrap_or("service");
243 match style {
244 DiagramStyle::Ascii => {
245 if product_type.contains("cli") {
246 r#"```text
247User -> CLI Entrypoint -> Command Router -> Core Engine -> Local Store
248 \-> External API / Filesystem
249```"#
250 .to_string()
251 } else if product_type.contains("frontend") {
252 r#"```text
253Browser -> UI Shell -> API Client -> Backend Gateway -> Datastores / Events
254```"#
255 .to_string()
256 } else if product_type.contains("library") {
257 r#"```text
258Host Application -> Library API -> Domain Core -> Adapters (Store / Network)
259```"#
260 .to_string()
261 } else {
262 r#"```text
263Client -> API Gateway -> Service Core -> Worker Queue -> Datastores
264```"#
265 .to_string()
266 }
267 }
268 DiagramStyle::Mermaid => {
269 if product_type.contains("cli") {
270 r#"```mermaid
271flowchart LR
272 U[User] --> C[CLI Entrypoint]
273 C --> R[Command Router]
274 R --> E[Core Engine]
275 E --> S[(Local Store)]
276 E --> X[External APIs / Filesystem]
277```"#
278 .to_string()
279 } else if product_type.contains("frontend") {
280 r#"```mermaid
281flowchart LR
282 B[Browser] --> UI[UI Shell]
283 UI --> A[API Client]
284 A --> G[Backend Gateway]
285 G --> DB[(Datastore)]
286 G --> Q[(Event Bus)]
287```"#
288 .to_string()
289 } else if product_type.contains("library") {
290 r#"```mermaid
291flowchart LR
292 H[Host Application] --> L[Library API]
293 L --> D[Domain Core]
294 D --> AD[Adapter Layer]
295 AD --> DB[(Store)]
296 AD --> N[Network]
297```"#
298 .to_string()
299 } else {
300 r#"```mermaid
301flowchart LR
302 C[Client] --> G[API Gateway]
303 G --> S[Service Core]
304 S --> W[Workers]
305 S --> DB[(Primary Datastore)]
306 W --> Q[(Queue)]
307```"#
308 .to_string()
309 }
310 }
311 }
312}
313
314fn adaptive_happy_path_sequence(style: DiagramStyle, seed: Option<&SpecsSeed>) -> String {
315 let product_type = seed
316 .and_then(|s| s.product_type.as_deref())
317 .unwrap_or("service");
318 match style {
319 DiagramStyle::Ascii => {
320 if product_type.contains("cli") {
321 r#"```text
322User invokes command -> CLI parses args -> Core executes action -> state persists -> result printed
323```"#
324 .to_string()
325 } else if product_type.contains("frontend") {
326 r#"```text
327User action -> UI validates input -> API request -> backend persists -> UI renders success
328```"#
329 .to_string()
330 } else {
331 r#"```text
332Client request -> API validation -> domain execution -> persistence -> response with trace id
333```"#
334 .to_string()
335 }
336 }
337 DiagramStyle::Mermaid => {
338 if product_type.contains("cli") {
339 r#"```mermaid
340sequenceDiagram
341 participant U as User
342 participant C as CLI
343 participant E as Core Engine
344 participant S as Store
345 U->>C: Run command
346 C->>E: Parse + validate
347 E->>S: Persist mutation
348 S-->>E: Ack
349 E-->>C: Result
350 C-->>U: Structured output
351```"#
352 .to_string()
353 } else if product_type.contains("frontend") {
354 r#"```mermaid
355sequenceDiagram
356 participant U as User
357 participant UI as Frontend
358 participant API as Backend API
359 participant DB as Datastore
360 U->>UI: Submit action
361 UI->>API: Authenticated request
362 API->>DB: Write transaction
363 DB-->>API: Commit ok
364 API-->>UI: 200 + payload
365 UI-->>U: Updated view
366```"#
367 .to_string()
368 } else {
369 r#"```mermaid
370sequenceDiagram
371 participant C as Client
372 participant G as API
373 participant D as Domain
374 participant DB as Datastore
375 C->>G: Request
376 G->>D: Validate + execute
377 D->>DB: Commit transaction
378 DB-->>D: Commit ok
379 D-->>G: Domain result
380 G-->>C: Response + trace_id
381```"#
382 .to_string()
383 }
384 }
385 }
386}
387
388fn specs_readme_template(seed: Option<&SpecsSeed>) -> String {
389 let product = seed
390 .and_then(|s| s.product_name.as_deref())
391 .unwrap_or("this repository");
392 let summary = seed
393 .and_then(|s| s.product_summary.as_deref())
394 .unwrap_or("Define the intended user-visible outcome.");
395 let languages = joined_or_fallback(
396 seed.map(|s| s.primary_languages.as_slice()).unwrap_or(&[]),
397 "not detected yet",
398 );
399 let surfaces = joined_or_fallback(
400 seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]),
401 "not detected yet",
402 );
403
404 format!(
405 r#"# Project Specs
406
407Canonical path: `.decapod/generated/specs/`.
408These files are the project-local contract for humans and agents.
409
410## Snapshot
411- Project: {product}
412- Outcome: {summary}
413- Detected languages: {languages}
414- Detected surfaces: {surfaces}
415
416## How to use this folder
417- `INTENT.md`: what success means and what is explicitly out of scope.
418- `ARCHITECTURE.md`: topology, runtime model, data boundaries, and ADR trail.
419- `INTERFACES.md`: API/CLI/events/storage contracts and failure behavior.
420- `VALIDATION.md`: proof commands, quality gates, and evidence artifacts.
421- `SEMANTICS.md`: state machines, invariants, replay rules, and idempotency.
422- `OPERATIONS.md`: SLOs, monitoring, incident response, and rollout strategy.
423- `SECURITY.md`: threat model, trust boundaries, auth/authz, and supply-chain posture.
424
425## Canonical `.decapod/` Layout
426- `.decapod/data/`: canonical control-plane state (SQLite + ledgers).
427- `.decapod/generated/specs/`: living project specs for humans and agents.
428- `.decapod/generated/context/`: deterministic context capsules.
429- `.decapod/generated/policy/context_capsule_policy.json`: repo-native JIT context policy contract.
430- `.decapod/generated/artifacts/provenance/`: promotion manifests and convergence checklist.
431- `.decapod/generated/artifacts/inventory/`: deterministic release inventory.
432- `.decapod/generated/artifacts/diagnostics/`: opt-in diagnostics artifacts.
433- `.decapod/workspaces/`: isolated todo-scoped git worktrees.
434
435## Day-0 Onboarding Checklist
436- [ ] Replace all placeholders in all 8 spec files.
437- [ ] Confirm primary user outcome and acceptance criteria in `INTENT.md`.
438- [ ] Confirm topology and runtime model in `ARCHITECTURE.md`.
439- [ ] Document all inbound/outbound contracts in `INTERFACES.md`.
440- [ ] Define validation gates and CI proof surfaces in `VALIDATION.md`.
441- [ ] Define state machines and invariants in `SEMANTICS.md`.
442- [ ] Define SLOs, alerting, and incident process in `OPERATIONS.md`.
443- [ ] Define threat model and auth/authz decisions in `SECURITY.md`.
444- [ ] Ensure architecture diagram, docs, changelog, and tests are mapped to promotion gates.
445- [ ] Run all validation/test commands and attach evidence artifacts.
446
447## Agent Directive
448- Treat these files as executable governance surfaces. Before implementation: resolve ambiguity and update specs. After implementation: refresh drifted sections, rerun proof gates, and attach evidence.
449"#
450 )
451}
452
453fn specs_intent_template(seed: Option<&SpecsSeed>) -> String {
454 let product_outcome = seed
455 .and_then(|s| s.product_summary.as_deref())
456 .unwrap_or("Define the user-visible outcome in one paragraph.");
457 let done_criteria = seed
458 .and_then(|s| s.done_criteria.as_deref())
459 .unwrap_or("Functional behavior is demonstrably correct.");
460 let product_name = seed
461 .and_then(|s| s.product_name.as_deref())
462 .unwrap_or("this repository");
463 let product_type = seed
464 .and_then(|s| s.product_type.as_deref())
465 .unwrap_or("not classified yet");
466 let languages = joined_or_fallback(
467 seed.map(|s| s.primary_languages.as_slice()).unwrap_or(&[]),
468 "not detected yet",
469 );
470 let surfaces = joined_or_fallback(
471 seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]),
472 "not detected yet",
473 );
474 let language_criteria = language_specific_test_criteria(seed)
475 .into_iter()
476 .map(|s| format!("- [ ] {}", s))
477 .collect::<Vec<_>>()
478 .join("\n");
479
480 format!(
481 r#"# Intent
482
483## Product Outcome
484- {product_outcome}
485
486## Product View
487```mermaid
488flowchart LR
489 U[Primary User] --> P[{product_name}]
490 P --> O[User-visible Outcome]
491 P --> G[Proof Gates]
492 G --> E[Evidence Artifacts]
493```
494
495## Inferred Baseline
496- Repository: {product_name}
497- Product type: {product_type}
498- Primary languages: {languages}
499- Detected surfaces: {surfaces}
500
501## Scope
502| Area | In Scope | Proof Surface |
503|---|---|---|
504| Core workflow | Define a concrete user-visible workflow | Acceptance criteria + tests |
505| Data contracts | Document canonical inputs/outputs | `INTERFACES.md` and schema checks |
506| Delivery quality | Block promotion on broken proof surfaces | `VALIDATION.md` blocking gates |
507
508## Non-Goals (Falsifiable)
509| Non-goal | How to falsify |
510|---|---|
511| Feature creep beyond the primary outcome | Any PR adds capability not tied to outcome criteria |
512| Shipping without evidence | Missing validation artifacts for promoted changes |
513| Ambiguous ownership boundaries | Missing owner/system-of-record in interfaces |
514
515## Constraints
516- Technical: runtime, dependency, and topology boundaries are explicit.
517- Operational: deployment, rollback, and incident ownership are defined.
518- Security/compliance: sensitive data handling and authz are mandatory.
519
520## Acceptance Criteria (must be objectively testable)
521- [ ] {done_criteria}
522- [ ] Non-functional targets are met (latency, reliability, cost, etc.).
523- [ ] Validation gates pass and artifacts are attached.
524{language_criteria}
525
526## Tradeoffs Register
527| Decision | Benefit | Cost | Review Trigger |
528|---|---|---|---|
529| Simplicity vs extensibility | Faster iteration | Potential rework | Feature set expands |
530| Strict gates vs dev speed | Higher confidence | More upfront discipline | Lead time regressions |
531
532## First Implementation Slice
533- [ ] Define the smallest user-visible workflow to ship first.
534- [ ] Define required data/contracts for that workflow.
535- [ ] Define what is intentionally postponed until v2.
536
537## Open Questions (with decision deadlines)
538| Question | Owner | Deadline | Decision |
539|---|---|---|---|
540| Which interfaces are versioned at launch? | TBD | YYYY-MM-DD | |
541| Which non-functional target is hardest to hit? | TBD | YYYY-MM-DD | |
542"#
543 )
544}
545
546fn specs_architecture_template(style: DiagramStyle, seed: Option<&SpecsSeed>) -> String {
547 let summary = seed
548 .and_then(|s| s.architecture_direction.as_deref())
549 .unwrap_or(
550 "Describe architecture in deployment-reality terms: runtime boundaries, operational ownership, and failure containment.",
551 );
552 let runtime_langs = seed
553 .map(|s| s.primary_languages.join(", "))
554 .filter(|s| !s.trim().is_empty())
555 .unwrap_or_else(|| "to be confirmed".to_string());
556 let surfaces = seed
557 .map(|s| s.detected_surfaces.join(", "))
558 .filter(|s| !s.trim().is_empty())
559 .unwrap_or_else(|| "to be confirmed".to_string());
560 let product_type = seed
561 .and_then(|s| s.product_type.as_deref())
562 .unwrap_or("to be confirmed");
563
564 format!(
565 r#"# Architecture
566
567## Direction
568{summary}
569
570## Current Facts
571- Runtime/languages: {runtime_langs}
572- Detected surfaces/framework hints: {surfaces}
573- Product type: {product_type}
574
575## Topology
576{topology}
577
578## Store Boundaries
579```mermaid
580flowchart LR
581 I[Inbound Requests] --> C[Core Runtime]
582 C --> W[(Write Store)]
583 C --> R[(Read Store)]
584 C --> E[External Dependency]
585 E --> DLQ[(DLQ / Retry Queue)]
586```
587
588## Happy Path Sequence
589{happy_path}
590
591## Error Path
592```mermaid
593sequenceDiagram
594 participant Client
595 participant API
596 participant Upstream
597 Client->>API: Request
598 API->>Upstream: Call with timeout budget
599 Upstream--xAPI: Timeout / failure
600 API-->>Client: Typed error + retry guidance + trace_id
601```
602
603## Execution Path
604- Ingress parse + validation:
605- Policy/interlock checks:
606- Core execution + persistence:
607- Verification and artifact emission:
608
609## Concurrency and Runtime Model
610- Execution model:
611- Isolation boundaries:
612- Backpressure strategy:
613- Shared state synchronization:
614
615## Deployment Topology
616- Runtime units:
617- Region/zone model:
618- Rollout strategy (blue/green/canary):
619- Rollback trigger and blast-radius scope:
620
621## Data and Contracts
622- Inbound contracts (CLI/API/events):
623- Outbound dependencies (datastores/queues/external APIs):
624- Data ownership boundaries:
625- Schema evolution + migration policy:
626
627## ADR Register
628| ADR | Title | Status | Rationale | Date |
629|---|---|---|---|---|
630| ADR-001 | Initial topology choice | Proposed | Define first stable architecture | YYYY-MM-DD |
631
632## Delivery Plan (first 3 slices)
633- Slice 1 (ship first):
634- Slice 2:
635- Slice 3:
636
637## Risks and Mitigations
638| Risk | Likelihood | Impact | Mitigation |
639|---|---|---|---|
640| Contract drift across components | Medium | High | Spec + schema checks in CI |
641| Runtime saturation under peak load | Medium | High | Capacity model + load tests |
642"#,
643 topology = adaptive_topology_diagram(style, seed),
644 happy_path = adaptive_happy_path_sequence(style, seed),
645 )
646}
647
648fn specs_interfaces_template(seed: Option<&SpecsSeed>) -> String {
649 let surfaces = joined_or_fallback(
650 seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]),
651 "not detected yet",
652 );
653 let product_type = seed
654 .and_then(|s| s.product_type.as_deref())
655 .unwrap_or("not classified yet");
656 let error_example = language_specific_error_example(seed);
657
658 format!(
659 r#"# Interfaces
660
661## Contract Principles
662- Prefer explicit schemas over implicit behavior.
663- Every mutating interface defines idempotency semantics.
664- Every failure path maps to a typed, documented error code.
665
666## API / RPC Contracts
667| Interface | Method | Request Schema | Response Schema | Errors | Idempotency |
668|---|---|---|---|---|---|
669| `TODO` | `TODO` | `TODO` | `TODO` | `TODO` | `TODO` |
670
671## Event Consumers
672| Consumer | Event | Ordering Requirement | Retry Policy | DLQ Policy |
673|---|---|---|---|---|
674| `TODO` | `TODO` | `TODO` | `TODO` | `TODO` |
675
676## Outbound Dependencies
677| Dependency | Purpose | SLA | Timeout | Circuit-Breaker |
678|---|---|---|---|---|
679| `TODO` | `TODO` | `TODO` | `TODO` | `TODO` |
680
681## Inbound Contracts
682- API / RPC entrypoints:
683- CLI surfaces:
684- Event/webhook consumers:
685- Repository-detected surfaces: {surfaces}
686
687## Data Ownership
688- Source-of-truth tables/collections:
689- Cross-boundary read models:
690- Consistency expectations:
691
692## Error Taxonomy Example ({product_type})
693{error_example}
694
695## Failure Semantics
696| Failure Class | Retry/Backoff | Client Contract | Observability |
697|---|---|---|---|
698| Validation | No retry | 4xx typed error | warn log + metric |
699| Dependency timeout | Exponential backoff | 503 with retryable code | error log + alert |
700| Conflict | Conditional retry | 409 with conflict detail | info log + metric |
701
702## Timeout Budget
703| Hop | Budget (ms) | Notes |
704|---|---|---|
705| Client -> Edge/API | 500 | Includes auth + routing |
706| API -> Domain | 300 | Includes validation |
707| Domain -> Store/Dependency | 200 | Includes retry overhead |
708
709## Interface Versioning
710- Version strategy (`v1`, date-based, semver):
711- Backward-compatibility guarantees:
712- Deprecation window and removal policy:
713"#
714 )
715}
716
717fn specs_validation_template(seed: Option<&SpecsSeed>) -> String {
718 let commands = default_test_commands(seed);
719 let test_commands = if commands.is_empty() {
720 "- Add repository-specific test command(s) here.".to_string()
721 } else {
722 commands
723 .into_iter()
724 .map(|c| format!("- `{}`", c))
725 .collect::<Vec<_>>()
726 .join("\n")
727 };
728 format!(
729 r#"# Validation
730
731## Validation Philosophy
732> Validation is a release gate, not documentation theater.
733
734## Validation Decision Tree
735```mermaid
736flowchart TD
737 S[Start] --> W{{Workspace valid?}}
738 W -->|No| F1[Fail: workspace gate]
739 W -->|Yes| T{{Tests pass?}}
740 T -->|No| F2[Fail: test gate]
741 T -->|Yes| D{{Docs + diagrams + changelog updated?}}
742 D -->|No| F3[Fail: docs gate]
743 D -->|Yes| V[Run decapod validate]
744 V --> P{{All blocking gates pass?}}
745 P -->|No| F4[Fail: promotion blocked]
746 P -->|Yes| E[Emit promotion evidence]
747```
748
749## Promotion Flow
750```mermaid
751flowchart LR
752 A[Plan] --> B[Implement]
753 B --> C[Test]
754 C --> D[Validate]
755 D --> E[Assemble Evidence]
756 E --> F[Promote]
757```
758
759## Proof Surfaces
760- `decapod validate`
761- Required test commands:
762{test_commands}
763- Required integration/e2e commands:
764
765## Promotion Gates
766
767## Blocking Gates
768| Gate | Command | Evidence |
769|---|---|---|
770| Architecture + interface drift check | `decapod validate` | Gate output |
771| Tests pass | project test command | CI + local logs |
772| Docs + changelog current | repo docs checks | PR diff |
773| Security critical checks pass | security scanner suite | scanner reports |
774
775## Warning Gates
776| Gate | Trigger | Follow-up SLA |
777|---|---|---|
778| Coverage regression warning | Coverage drops below target | 48h |
779| Non-blocking perf drift | P95 regression below hard threshold | 72h |
780
781## Evidence Artifacts
782| Artifact | Path | Required For |
783|---|---|---|
784| Validation report | `.decapod/generated/artifacts/provenance/*` | Promotion |
785| Test logs | CI artifact store | Promotion |
786| Architecture diagram snapshot | `ARCHITECTURE.md` | Promotion |
787| Changelog entry | `CHANGELOG.md` | Promotion |
788
789## Regression Guardrails
790- Baseline references:
791- Statistical thresholds (if non-deterministic):
792- Rollback criteria:
793
794## Bounded Execution
795| Operation | Timeout | Failure Mode |
796|---|---|---|
797| Validation | 30s | timeout or lock |
798| Unit test suite | project-defined | non-zero exit |
799| Integration suite | project-defined | non-zero exit |
800
801## Coverage Checklist
802- [ ] Unit tests cover critical branches.
803- [ ] Integration tests cover key user flows.
804- [ ] Failure-path tests cover retries/timeouts.
805- [ ] Docs/diagram/changelog updates included.
806"#
807 )
808}
809
810fn specs_semantics_template(seed: Option<&SpecsSeed>) -> String {
811 let lang = primary_language_name(seed);
812 format!(
813 r#"# Semantics
814
815## State Machines
816```mermaid
817stateDiagram-v2
818 [*] --> Draft
819 Draft --> InProgress
820 InProgress --> Verified
821 InProgress --> Blocked
822 Blocked --> InProgress
823 Verified --> [*]
824```
825
826| From | Event | To | Guard | Side Effect |
827|---|---|---|---|---|
828| Draft | start | InProgress | owner assigned | emit state change |
829| InProgress | validate_pass | Verified | all blocking gates pass | persist receipt hash |
830| InProgress | dependency_blocked | Blocked | external dependency unavailable | emit alert event |
831
832## Invariants
833| Invariant | Type | Validation |
834|---|---|---|
835| No promoted change without proof | System | validation gate |
836| Canonical source-of-truth per entity | Data | interface/spec review |
837| Mutation events are replayable | Data | deterministic replay |
838
839## Event Sourcing Schema
840| Field | Type | Description |
841|---|---|---|
842| event_id | string | globally unique event id |
843| aggregate_id | string | entity/workflow id |
844| event_type | string | semantic transition |
845| payload | object | transition data |
846| recorded_at | timestamp | append time |
847
848## Replay Semantics
849- Replay order:
850- Conflict resolution:
851- Snapshot cadence:
852- Determinism proof strategy:
853
854## Error Code Semantics
855- Namespace:
856- Stable compatibility window:
857- Mapping to retry/degrade behavior:
858
859## Domain Rules
860- Business rule 1:
861- Business rule 2:
862- Business rule 3:
863
864## Idempotency Contracts
865| Operation | Idempotency Key | Duplicate Behavior |
866|---|---|---|
867| create/update mutation | request_id | return original result |
868| async enqueue | event_id | ignore duplicate enqueue |
869
870## Language Note
871- Primary language inferred: {lang}
872"#
873 )
874}
875
876fn specs_operations_template(seed: Option<&SpecsSeed>) -> String {
877 let logging_hint = language_specific_logging_hint(seed);
878 format!(
879 r#"# Operations
880
881## Operational Readiness Checklist
882- [ ] On-call ownership defined.
883- [ ] SLOs and alert thresholds defined.
884- [ ] Dashboards for latency/errors/throughput are live.
885- [ ] Runbooks linked for all Sev1/Sev2 alerts.
886- [ ] Rollback plan validated.
887- [ ] Capacity guardrails documented.
888
889## Service Level Objectives
890| SLI | SLO Target | Measurement Window | Owner |
891|---|---|---|---|
892| Availability | 99.9% | 30d | TBD |
893| P95 latency | TBD | 7d | TBD |
894| Error rate | < 1% | 7d | TBD |
895
896## Monitoring
897| Signal | Metric | Threshold | Alert |
898|---|---|---|---|
899| Traffic | requests/sec | baseline drift | warn |
900| Latency | p95/p99 | threshold breach | page |
901| Reliability | error ratio | threshold breach | page |
902| Saturation | cpu/memory/queue depth | sustained high | page |
903
904## Health Checks
905- Liveness:
906- Readiness:
907- Dependency health:
908- Synthetic transaction:
909
910## Alerting and Runbooks
911| Alert | Severity | Runbook Link | Escalation |
912|---|---|---|---|
913| API error rate spike | Sev2 | TBD | App on-call |
914| Persistent dependency timeout | Sev1 | TBD | App + platform |
915| Validation gate outage | Sev2 | TBD | Maintainers |
916
917## Incident Response
918- Incident commander model:
919- Communication channels:
920- Postmortem SLA:
921- Corrective action tracking:
922
923## Structured Logging
924- {logging_hint}
925
926## Severity Definitions
927| Severity | Definition | Response Time |
928|---|---|---|
929| Sev1 | Production outage or data integrity risk | Immediate |
930| Sev2 | Major functionality impaired | 30 minutes |
931| Sev3 | Minor degradation | Next business day |
932
933## Deployment Strategy
934- Primary strategy:
935- Change validation process:
936- Rollback and forward-fix policy:
937
938## Environment Configuration
939| Variable | Purpose | Default | Secret |
940|---|---|---|---|
941| APP_ENV | runtime environment | dev | no |
942| LOG_LEVEL | observability verbosity | info | no |
943| API_TOKEN | external auth | none | yes |
944
945## Capacity Planning
946- Peak request assumption:
947- Storage growth model:
948- Queue/worker headroom:
949"#
950 )
951}
952
953fn specs_security_template(seed: Option<&SpecsSeed>) -> String {
954 let scanners = language_specific_supply_chain_tools(seed)
955 .into_iter()
956 .map(|tool| format!("`{}`", tool))
957 .collect::<Vec<_>>()
958 .join(", ");
959 format!(
960 r#"# Security
961
962## Threat Model
963```mermaid
964flowchart LR
965 U[User/Client] --> A[Application Boundary]
966 A --> D[(Data Stores)]
967 A --> X[External Dependencies]
968 I[Identity Provider] --> A
969 A --> L[Audit Logs]
970```
971
972## STRIDE Table
973| Threat | Surface | Mitigation | Verification |
974|---|---|---|---|
975| Spoofing | Auth boundary | strong auth + token validation | auth tests |
976| Tampering | State mutation APIs | integrity checks + RBAC | integration tests |
977| Repudiation | Critical actions | immutable audit logs | log review |
978| Information disclosure | Data at rest/in transit | encryption + classification | security scans |
979| Denial of service | Hot paths | rate limit + backpressure | load tests |
980| Elevation of privilege | Admin interfaces | least privilege + policy checks | authz tests |
981
982## Authentication
983- Identity source:
984- Token/session lifetime:
985- Rotation and revocation:
986
987## Authorization
988- Role model:
989- Resource-level policy:
990- Privilege escalation controls:
991
992## Data Classification
993| Data Class | Examples | Storage Rules | Access Rules |
994|---|---|---|---|
995| Public | docs, non-sensitive metadata | standard | unrestricted |
996| Internal | operational telemetry | controlled | team access |
997| Sensitive | tokens, PII, secrets | encrypted | least privilege |
998
999## Sensitive Data Handling
1000- Encryption at rest:
1001- Encryption in transit:
1002- Redaction in logs:
1003- Retention + deletion policy:
1004
1005## Supply Chain Security
1006- Recommended scanners: {scanners}
1007- Dependency update cadence:
1008- Signed artifact/provenance strategy:
1009
1010## Secrets Management
1011| Secret | Source | Rotation | Consumer |
1012|---|---|---|---|
1013| API credentials | secret manager/env | periodic | runtime services |
1014| Signing keys | HSM/KMS/local secure store | periodic | release pipeline |
1015
1016## Security Testing
1017| Test Type | Cadence | Tooling |
1018|---|---|---|
1019| SAST | each PR | language linters/scanners |
1020| Dependency scan | each PR + weekly | supply-chain tools |
1021| DAST/pentest | scheduled | external/internal |
1022
1023## Compliance and Audit
1024- Regulatory scope:
1025- Audit evidence location:
1026- Exception process:
1027
1028## Pre-Promotion Security Checklist
1029- [ ] Threat model updated for changed surfaces.
1030- [ ] Auth/authz tests pass.
1031- [ ] Dependency vulnerability scan reviewed.
1032- [ ] No unresolved critical/high security findings.
1033"#
1034 )
1035}
1036
1037pub const DECAPOD_GITIGNORE_RULES: &[&str] = &[
1042 ".decapod/data",
1043 ".decapod/data/*",
1044 ".decapod/.stfolder",
1045 ".decapod/workspaces",
1046 ".decapod/generated/*",
1047 "!.decapod/data/",
1048 "!.decapod/data/knowledge.promotions.jsonl",
1049 "!.decapod/generated/Dockerfile",
1050 "!.decapod/generated/context/",
1051 "!.decapod/generated/context/*.json",
1052 "!.decapod/generated/policy/",
1053 "!.decapod/generated/policy/context_capsule_policy.json",
1054 "!.decapod/generated/artifacts/",
1055 "!.decapod/generated/artifacts/provenance/",
1056 "!.decapod/generated/artifacts/provenance/*.json",
1057 "!.decapod/generated/artifacts/provenance/kcr_trend.jsonl",
1058 "!.decapod/generated/specs/",
1059 "!.decapod/generated/specs/*.md",
1060 "!.decapod/generated/specs/.manifest.json",
1061];
1062
1063fn ensure_gitignore_entry(target_dir: &Path, entry: &str) -> Result<(), error::DecapodError> {
1066 let gitignore_path = target_dir.join(".gitignore");
1067 let content = fs::read_to_string(&gitignore_path).unwrap_or_default();
1068
1069 if content.lines().any(|line| line.trim() == entry) {
1071 return Ok(());
1072 }
1073
1074 let mut new_content = content;
1075 if !new_content.is_empty() && !new_content.ends_with('\n') {
1076 new_content.push('\n');
1077 }
1078 new_content.push_str(entry);
1079 new_content.push('\n');
1080 fs::write(&gitignore_path, new_content).map_err(error::DecapodError::IoError)?;
1081 Ok(())
1082}
1083
1084fn ensure_parent(path: &Path) -> Result<(), error::DecapodError> {
1085 if let Some(p) = path.parent() {
1086 fs::create_dir_all(p).map_err(error::DecapodError::IoError)?;
1087 }
1088 Ok(())
1089}
1090
1091#[derive(Clone, Copy, Debug)]
1092pub enum FileAction {
1093 Created,
1094 Unchanged,
1095 Preserved,
1096}
1097
1098fn write_file(
1099 opts: &ScaffoldOptions,
1100 rel_path: &str,
1101 content: &str,
1102) -> Result<FileAction, error::DecapodError> {
1103 use sha2::{Digest, Sha256};
1104
1105 let dest = opts.target_dir.join(rel_path);
1106
1107 if dest.exists() {
1108 if let Ok(existing_content) = fs::read_to_string(&dest) {
1109 let mut template_hasher = Sha256::new();
1110 template_hasher.update(content.as_bytes());
1111 let template_hash = format!("{:x}", template_hasher.finalize());
1112
1113 let mut existing_hasher = Sha256::new();
1114 existing_hasher.update(existing_content.as_bytes());
1115 let existing_hash = format!("{:x}", existing_hasher.finalize());
1116
1117 if template_hash == existing_hash {
1118 return Ok(FileAction::Unchanged);
1119 }
1120 }
1121
1122 if !opts.force {
1123 if opts.dry_run {
1124 return Ok(FileAction::Unchanged);
1125 }
1126 return Err(error::DecapodError::ValidationError(format!(
1127 "Refusing to overwrite existing path without --force: {}",
1128 dest.display()
1129 )));
1130 }
1131 }
1132
1133 if opts.dry_run {
1134 return Ok(FileAction::Created);
1135 }
1136
1137 ensure_parent(&dest)?;
1138 fs::write(&dest, content).map_err(error::DecapodError::IoError)?;
1139
1140 Ok(FileAction::Created)
1141}
1142
1143pub fn scaffold_project_entrypoints(
1144 opts: &ScaffoldOptions,
1145) -> Result<ScaffoldSummary, error::DecapodError> {
1146 let data_dir_rel = ".decapod/data";
1147
1148 fs::create_dir_all(opts.target_dir.join(data_dir_rel)).map_err(error::DecapodError::IoError)?;
1150
1151 if !opts.dry_run {
1153 for rule in DECAPOD_GITIGNORE_RULES {
1154 ensure_gitignore_entry(&opts.target_dir, rule)?;
1155 }
1156 }
1157
1158 let files_to_generate = if opts.all || opts.agent_files.is_empty() {
1163 vec!["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"]
1164 } else {
1165 opts.agent_files.iter().map(|s| s.as_str()).collect()
1166 };
1167
1168 let readme_md = assets::get_template("README.md").expect("Missing template: README.md");
1170 let override_md = assets::get_template("OVERRIDE.md").expect("Missing template: OVERRIDE.md");
1171
1172 let mut ep_created = 0usize;
1174 let mut ep_unchanged = 0usize;
1175 let mut ep_preserved = 0usize;
1176 for file in files_to_generate {
1177 let content =
1178 assets::get_template(file).unwrap_or_else(|| panic!("Missing template: {}", file));
1179 match write_file(opts, file, &content)? {
1180 FileAction::Created => ep_created += 1,
1181 FileAction::Unchanged => ep_unchanged += 1,
1182 FileAction::Preserved => ep_preserved += 1,
1183 }
1184 }
1185
1186 let mut cfg_created = 0usize;
1187 let mut cfg_unchanged = 0usize;
1188 let mut cfg_preserved = 0usize;
1189
1190 match write_file(opts, ".decapod/README.md", &readme_md)? {
1191 FileAction::Created => cfg_created += 1,
1192 FileAction::Unchanged => cfg_unchanged += 1,
1193 FileAction::Preserved => cfg_preserved += 1,
1194 }
1195
1196 let override_path = opts.target_dir.join(".decapod/OVERRIDE.md");
1198 if override_path.exists() {
1199 cfg_preserved += 1;
1200 } else {
1201 match write_file(opts, ".decapod/OVERRIDE.md", &override_md)? {
1202 FileAction::Created => cfg_created += 1,
1203 FileAction::Unchanged => cfg_unchanged += 1,
1204 FileAction::Preserved => cfg_preserved += 1,
1205 }
1206 }
1207
1208 if !opts.dry_run {
1210 blend_legacy_entrypoints(&opts.target_dir)?;
1211 }
1212
1213 let generated_dir = opts.target_dir.join(".decapod/generated");
1215 fs::create_dir_all(&generated_dir).map_err(error::DecapodError::IoError)?;
1216 fs::create_dir_all(generated_dir.join("context")).map_err(error::DecapodError::IoError)?;
1217 fs::create_dir_all(generated_dir.join("policy")).map_err(error::DecapodError::IoError)?;
1218 fs::create_dir_all(generated_dir.join("artifacts").join("provenance"))
1219 .map_err(error::DecapodError::IoError)?;
1220 fs::create_dir_all(generated_dir.join("artifacts").join("inventory"))
1221 .map_err(error::DecapodError::IoError)?;
1222 fs::create_dir_all(
1223 generated_dir
1224 .join("artifacts")
1225 .join("diagnostics")
1226 .join("validate"),
1227 )
1228 .map_err(error::DecapodError::IoError)?;
1229 fs::create_dir_all(generated_dir.join("migrations")).map_err(error::DecapodError::IoError)?;
1230 let dockerfile_path = generated_dir.join("Dockerfile");
1231 if !dockerfile_path.exists() {
1232 let dockerfile_content = container::generated_dockerfile_for_repo(&opts.target_dir);
1233 fs::write(&dockerfile_path, dockerfile_content).map_err(error::DecapodError::IoError)?;
1234 }
1235 let version_counter_path = generated_dir.join("version_counter.json");
1236 if !version_counter_path.exists() {
1237 let now = crate::core::time::now_epoch_z();
1238 let version_counter = serde_json::json!({
1239 "schema_version": "1.0.0",
1240 "version_count": 1,
1241 "initialized_with_version": env!("CARGO_PKG_VERSION"),
1242 "last_seen_version": env!("CARGO_PKG_VERSION"),
1243 "updated_at": now,
1244 });
1245 let body = serde_json::to_string_pretty(&version_counter).map_err(|e| {
1246 error::DecapodError::ValidationError(format!(
1247 "Failed to serialize version counter: {}",
1248 e
1249 ))
1250 })?;
1251 fs::write(version_counter_path, body).map_err(error::DecapodError::IoError)?;
1252 }
1253
1254 let generated_policy_path = opts.target_dir.join(GENERATED_POLICY_REL_PATH);
1255 if !generated_policy_path.exists() {
1256 let policy_body = default_policy_json_pretty()?;
1257 fs::write(generated_policy_path, policy_body).map_err(error::DecapodError::IoError)?;
1258 }
1259
1260 let (specs_created, specs_unchanged, specs_preserved) = if opts.generate_specs {
1261 let mut created = 0usize;
1262 let mut unchanged = 0usize;
1263 let mut preserved = 0usize;
1264 let mut manifest_entries: Vec<ProjectSpecManifestEntry> = Vec::new();
1265
1266 let seed = opts.specs_seed.as_ref();
1267 let mut specs_files: Vec<(&str, String)> = Vec::new();
1268 for spec in LOCAL_PROJECT_SPECS {
1269 let content = match spec.path {
1270 LOCAL_PROJECT_SPECS_README => specs_readme_template(seed),
1271 LOCAL_PROJECT_SPECS_INTENT => specs_intent_template(seed),
1272 LOCAL_PROJECT_SPECS_ARCHITECTURE => {
1273 specs_architecture_template(opts.diagram_style, seed)
1274 }
1275 LOCAL_PROJECT_SPECS_INTERFACES => specs_interfaces_template(seed),
1276 LOCAL_PROJECT_SPECS_VALIDATION => specs_validation_template(seed),
1277 LOCAL_PROJECT_SPECS_SEMANTICS => specs_semantics_template(seed),
1278 LOCAL_PROJECT_SPECS_OPERATIONS => specs_operations_template(seed),
1279 LOCAL_PROJECT_SPECS_SECURITY => specs_security_template(seed),
1280 _ => continue,
1281 };
1282 specs_files.push((spec.path, content));
1283 }
1284
1285 for (rel_path, content) in specs_files {
1286 let template_hash = hash_text(&content);
1287 match write_file(opts, rel_path, &content)? {
1288 FileAction::Created => created += 1,
1289 FileAction::Unchanged => unchanged += 1,
1290 FileAction::Preserved => preserved += 1,
1291 }
1292 manifest_entries.push(ProjectSpecManifestEntry {
1293 path: rel_path.to_string(),
1294 template_hash: template_hash.clone(),
1295 content_hash: template_hash,
1296 });
1297 }
1298
1299 if !opts.dry_run {
1300 let manifest = ProjectSpecsManifest {
1301 schema_version: LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA.to_string(),
1302 template_version: "scaffold-v2".to_string(),
1303 generated_at: crate::core::time::now_epoch_z(),
1304 repo_signal_fingerprint: repo_signal_fingerprint(&opts.target_dir)?,
1305 files: manifest_entries,
1306 };
1307 let manifest_path = opts.target_dir.join(LOCAL_PROJECT_SPECS_MANIFEST);
1308 ensure_parent(&manifest_path)?;
1309 let manifest_body = serde_json::to_string_pretty(&manifest).map_err(|e| {
1310 error::DecapodError::ValidationError(format!(
1311 "Failed to serialize specs manifest: {}",
1312 e
1313 ))
1314 })?;
1315 fs::write(manifest_path, manifest_body).map_err(error::DecapodError::IoError)?;
1316 }
1317 (created, unchanged, preserved)
1318 } else {
1319 (0usize, 0usize, 0usize)
1320 };
1321
1322 Ok(ScaffoldSummary {
1323 entrypoints_created: ep_created,
1324 entrypoints_unchanged: ep_unchanged,
1325 entrypoints_preserved: ep_preserved,
1326 config_created: cfg_created,
1327 config_unchanged: cfg_unchanged,
1328 config_preserved: cfg_preserved,
1329 specs_created,
1330 specs_unchanged,
1331 specs_preserved,
1332 })
1333}
1334
1335pub fn blend_legacy_entrypoints(target_dir: &Path) -> Result<(), error::DecapodError> {
1338 let override_path = target_dir.join(".decapod/OVERRIDE.md");
1339 let mut overrides_added = false;
1340 let mut content_to_add = String::new();
1341
1342 for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
1343 let bak_path = target_dir.join(format!("{}.bak", file));
1344 if bak_path.exists() {
1345 if let Ok(bak_content) = fs::read_to_string(&bak_path) {
1346 let trimmed = bak_content.trim();
1348 if !trimmed.is_empty() {
1349 content_to_add.push_str(&format!(
1350 "\n\n### Blended from Legacy {} Entrypoint\n\n{}\n",
1351 file.replace(".md", ""),
1352 trimmed
1353 ));
1354 overrides_added = true;
1355 }
1356 }
1357 let _ = fs::remove_file(&bak_path);
1359 }
1360 }
1361
1362 if overrides_added && override_path.exists() {
1363 let mut existing = fs::read_to_string(&override_path).unwrap_or_default();
1364 existing.push_str(&content_to_add);
1365 fs::write(&override_path, existing).map_err(error::DecapodError::IoError)?;
1366 }
1367
1368 Ok(())
1369}