Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d882cf0c6e | |||
| 564d4a4ca3 | |||
| c524ad731e | |||
| 8bcf7296d5 | |||
| 81c7a22ddd | |||
| 2c08c778a9 | |||
| 6ffcbe8283 | |||
| 5775a97388 | |||
| b3c75ccec1 | |||
| e891496163 | |||
| 382944c055 | |||
| 71b6a4fa91 |
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"phase": 0,
|
"phase": 0,
|
||||||
"stage": "plan",
|
"stage": "complete",
|
||||||
"milestone": "v1.18",
|
"milestone": "v1.18",
|
||||||
"phase_role": "pre_execution",
|
"phase_role": "pre_execution",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-08-06T00:25:00Z",
|
"updated_at": "2026-08-06T00:35:00Z",
|
||||||
"milestone_complete": false,
|
"milestone_complete": false,
|
||||||
"notes": "v1.18 PLAN complete. 8 phases, 6 waves, 15 requirements. Sequential execution."
|
"tag": "v1.17.0",
|
||||||
|
"release_id": 522,
|
||||||
|
"notes": "v1.18 P0 complete. 5 pre-execution stages done. Tag v1.17.0, release 522."
|
||||||
}
|
}
|
||||||
+98
-1
@@ -58,6 +58,103 @@ traceable to a human attestation and an immutable evidence stream.
|
|||||||
boundary. The platform validates, enriches with operational standards,
|
boundary. The platform validates, enriches with operational standards,
|
||||||
and reconciles the target state.
|
and reconciles the target state.
|
||||||
|
|
||||||
|
## Scope: Nova is Downstream of PDLC
|
||||||
|
|
||||||
|
> **Promoted from Core Tenet #2 + Anti-Goal #1 (v1.18, REQ-216).** This
|
||||||
|
> is the unmissable scope statement — the PDLC is upstream, Nova is
|
||||||
|
> downstream.
|
||||||
|
|
||||||
|
The **Product Development Lifecycle (PDLC)** — product backlog, code
|
||||||
|
authorship, IDE workflows, sprint planning, application business logic —
|
||||||
|
is **upstream** of Nova. Nova never penetrates the PDLC. Nova's domain is
|
||||||
|
**infrastructure + delivery only**: environment progression, cloud
|
||||||
|
resource lifecycle, operational security/observability NFRs, policy
|
||||||
|
enforcement, immutable audit lineage, and the two consumer surfaces
|
||||||
|
(technical developer + agentic).
|
||||||
|
|
||||||
|
Integration between the PDLC and Nova is **only** through the validated,
|
||||||
|
published contract boundary (`schemas/contract.schema.json` +
|
||||||
|
`schemas/submission-readiness.schema.json`). The citizen developer's AI
|
||||||
|
coding agent, an upstream agentic SDLC platform, or any upstream
|
||||||
|
development platform may all produce submissions — the source does not
|
||||||
|
matter because all are subject to the same compliance standards (the
|
||||||
|
submission-readiness gate, D-133). Nova validates, enriches with
|
||||||
|
operational standards, and reconciles the target state. Nova never
|
||||||
|
authors application code, manages product backlogs, or provides IDE
|
||||||
|
workflows.
|
||||||
|
|
||||||
|
```
|
||||||
|
PDLC (upstream) Nova (downstream)
|
||||||
|
───────────────── ─────────────────
|
||||||
|
product backlog contract ingestion
|
||||||
|
code authorship (AI agent / IDE / SDLC) → submission-readiness gate
|
||||||
|
sprint planning → policy enforcement
|
||||||
|
application business logic → cloud resource lifecycle
|
||||||
|
→ environment progression (dev→qa→prod→dr)
|
||||||
|
→ immutable audit + attestation
|
||||||
|
```
|
||||||
|
|
||||||
|
## RACI Matrix
|
||||||
|
|
||||||
|
> **Source of truth (v1.18, REQ-215, D-139).** Three roles clarify who
|
||||||
|
> owns what across the Nova delivery lifecycle. The matrix is the
|
||||||
|
> authoritative version; `docs/raci.md` is the citizen-developer-facing
|
||||||
|
> copy.
|
||||||
|
|
||||||
|
### Roles
|
||||||
|
|
||||||
|
- **Citizen Developer (CD)** — the consumer (technical developer L3A or
|
||||||
|
non-technical L3B). Responsible for all **Functional Requirements (FRs)**
|
||||||
|
and **User Acceptance Testing (UAT)**. The FRs + UAT are produced via
|
||||||
|
the citizen developer's AI coding agent, an upstream agentic SDLC, or
|
||||||
|
an upstream development platform — **the source does not matter as all
|
||||||
|
are subject to the same compliance standards** (the submission-readiness
|
||||||
|
gate, D-133).
|
||||||
|
- **Platform** — Nova. Responsible for all **Non-Functional Requirements
|
||||||
|
(NFRs)**, **Infrastructure** (cloud resource lifecycle, state, IAM),
|
||||||
|
**QA** (the platform-side quality checks: policy, confidence, schema),
|
||||||
|
and **Production deployments to cloud** (the apply path, the pipeline,
|
||||||
|
the release).
|
||||||
|
- **Release Management (RM)** — **co-owned**. QA + SRE attestations are
|
||||||
|
required by the actual release. The attestations are performed
|
||||||
|
agentically (the platform runs the checks), but the release is
|
||||||
|
**overseen and triggered by the Citizen Developer** — the human
|
||||||
|
attestation at the stage gate (D-042, hitl_gates.py). The platform
|
||||||
|
performs; the citizen developer authorizes.
|
||||||
|
|
||||||
|
### Matrix
|
||||||
|
|
||||||
|
| Work Category | Citizen Developer | Platform | Release Management |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Functional Requirements (FRs)** | **R/A** | C | I |
|
||||||
|
| **User Acceptance Testing (UAT)** | **R/A** | C | I |
|
||||||
|
| **Non-Functional Requirements (NFRs)** | I | **R/A** | C |
|
||||||
|
| **Infrastructure (cloud, state, IAM)** | I | **R/A** | C |
|
||||||
|
| **QA (policy, confidence, schema checks)** | C | **R/A** | I |
|
||||||
|
| **Production deployment to cloud** | I | **R/A** | C |
|
||||||
|
| **Release attestation (QA + SRE sign-off)** | **A** | R | **R** |
|
||||||
|
|
||||||
|
**Key: R** = Responsible (does the work) · **A** = Accountable (owns the
|
||||||
|
outcome, sign-off) · **C** = Consulted · **I** = Informed.
|
||||||
|
|
||||||
|
**Compliance-standard equivalence note:** the citizen developer's FRs +
|
||||||
|
UAT may originate from any upstream source — an AI coding agent, an
|
||||||
|
agentic SDLC platform, or a traditional development platform. All are
|
||||||
|
subject to the same compliance standards: the submission-readiness gate
|
||||||
|
(`schemas/submission-readiness.schema.json`), the contract schema, the
|
||||||
|
policy envelope, and the immutable audit stream. The platform does not
|
||||||
|
differentiate by upstream source; it validates the submission, not the
|
||||||
|
author.
|
||||||
|
|
||||||
|
**Co-ownership of Release Management:** the release is co-owned. The
|
||||||
|
platform performs the QA + SRE attestations agentically (confidence signal,
|
||||||
|
policy checks, separation-of-duties). The citizen developer oversees and
|
||||||
|
triggers the actual release — the human attestation at the stage gate is
|
||||||
|
the citizen developer's authorization, recorded with approver identity
|
||||||
|
(D-042). The platform runs the checks; the citizen developer authorizes
|
||||||
|
the promotion. This is the "autonomy in operations, human at stage gates"
|
||||||
|
model from the NORTH_STAR.
|
||||||
|
|
||||||
## Capability Status (Re-Verified 2026-07-27)
|
## Capability Status (Re-Verified 2026-07-27)
|
||||||
|
|
||||||
> Source of truth: `.ciagent/CAPABILITY_INVENTORY.md` (Phase 54, D-093).
|
> Source of truth: `.ciagent/CAPABILITY_INVENTORY.md` (Phase 54, D-093).
|
||||||
@@ -883,7 +980,7 @@ or user-directed scope). New v1.7 decisions:
|
|||||||
| W1.A | AI-refinement trigger | **Accept recommendation.** Joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. |
|
| W1.A | AI-refinement trigger | **Accept recommendation.** Joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops unilateral override. |
|
||||||
| W1.B | Multi-stack edge case rule | **Accept recommendation.** Permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. |
|
| W1.B | Multi-stack edge case rule | **Accept recommendation.** Permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30d, (c) explicit Infra & Ops approval with `multiStack.justification`. |
|
||||||
| W2.A | Tag mutability for prod | **Accept recommendation (Path B).** Tag for dev/qa, SHA for prod. Platform CLI resolves tag→SHA for prod-bound workflows. Justified by the "Audit truth lives outside the repository" bet. |
|
| W2.A | Tag mutability for prod | **Accept recommendation (Path B).** Tag for dev/qa, SHA for prod. Platform CLI resolves tag→SHA for prod-bound workflows. Justified by the "Audit truth lives outside the repository" bet. |
|
||||||
| BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case. |
|
| BA.A | Initial L3B skill catalog | **Accept recommendation.** 5 skills: web API, worker, scheduled job, static asset, basic observability bootstrap. Addition criteria: (a) reviewable for sensitive data, (b) expressible as a single contract submission, (c) documented use case. **Extended v1.18 (REQ-221/222):** the BA.A 5-skill catalog is extended with 9 Atelier-derived production-grade engineering skills under `skills/` (api, security, data, testing, observability, errors, devops, infrastructure-as-code, compliance), indexed by `docs/skills.md`. The Atelier skills extend, not replace, the BA.A catalog. |
|
||||||
| W3.D | L1/L2 standard versioning | **Decided.** Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (same as the v1.0 demo D-rule, lifted to the real platform). Pin model: L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible. Evolution: MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window. |
|
| W3.D | L1/L2 standard versioning | **Decided.** Semver: interface → MAJOR, behavior → MINOR, lifecycle → PATCH (same as the v1.0 demo D-rule, lifted to the real platform). Pin model: L2 contracts pin L1 by `name@semver`; the resolver picks the highest compatible. Evolution: MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window. |
|
||||||
| W3.E | Schema mandatory vs optional inputs | **Decided.** Per-env mandatory table: dev requires `stack` + `environment`; qa adds `validation.e2eSuite` + `validation.loadTest`; prod adds `runbook` + `dashboard` + `oncall`; dr adds `drDrillRef`. `inputs` map is always optional. `profile: agentic` fields (`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`) optional everywhere. |
|
| W3.E | Schema mandatory vs optional inputs | **Decided.** Per-env mandatory table: dev requires `stack` + `environment`; qa adds `validation.e2eSuite` + `validation.loadTest`; prod adds `runbook` + `dashboard` + `oncall`; dr adds `drDrillRef`. `inputs` map is always optional. `profile: agentic` fields (`naturalLanguageIntent`, `confidenceAtSubmission`, `agentTrace`) optional everywhere. |
|
||||||
| BA.B | Confidence threshold tuning | **Decided.** Starting thresholds frozen for v1. Tuning begins in v1.2: track FP/FN per environment quarterly; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream. |
|
| BA.B | Confidence threshold tuning | **Decided.** Starting thresholds frozen for v1. Tuning begins in v1.2: track FP/FN per environment quarterly; override authority = Infra & Ops + SRE joint sign-off; any override is itself a confidence-event in the audit stream. |
|
||||||
|
|||||||
@@ -499,4 +499,23 @@ def lambda_handler(event, context):
|
|||||||
return {"statusCode": 401, "body": json.dumps({"error": str(e)})}
|
return {"statusCode": 401, "body": json.dumps({"error": str(e)})}
|
||||||
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
|
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
|
||||||
except Exception as e: # pragma: no cover - defensive top-level guard
|
except Exception as e: # pragma: no cover - defensive top-level guard
|
||||||
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||||
|
|
||||||
|
|
||||||
|
# --- CLI: --check-readiness (D-133, REQ-218) ---------------------------
|
||||||
|
# Invoked as: python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>
|
||||||
|
# Delegates to core.submission_readiness.check_readiness() and prints the
|
||||||
|
# structured ReadinessResult. Exits 0 if ready, 1 if not.
|
||||||
|
if __name__ == "__main__": # pragma: no cover - CLI entry
|
||||||
|
import sys
|
||||||
|
if "--check-readiness" in sys.argv:
|
||||||
|
sys.path.insert(
|
||||||
|
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
)
|
||||||
|
from core.submission_readiness import cli_main
|
||||||
|
|
||||||
|
# Strip the --check-readiness flag; pass the file path.
|
||||||
|
rest = [a for a in sys.argv[1:] if a != "--check-readiness"]
|
||||||
|
sys.exit(cli_main(["check-readiness"] + rest))
|
||||||
|
else:
|
||||||
|
print("Usage: python3 -m core.lambda.contract_ingestor --check-readiness <submission.json>")
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
"""core/submission_readiness.py — Nova submission-readiness validator (REQ-218).
|
||||||
|
|
||||||
|
Defines what is acceptable to start — a superset gate ABOVE
|
||||||
|
contract.schema.json validity. Invoked as
|
||||||
|
``contract_ingestor.py --check-readiness`` (D-133). Returns a structured
|
||||||
|
ReadinessResult (pass/fail per check, with reason codes). On fail → the
|
||||||
|
ingestor rejects with a citizen-developer-facing error (not a stack
|
||||||
|
trace). On pass → proceeds to existing contract ingestion.
|
||||||
|
|
||||||
|
The validator calls contract.schema.json validation first (the shape),
|
||||||
|
then the readiness checks (the gate): tags, env mandatory, policy
|
||||||
|
preconditions, profile:agentic markers, appSource.
|
||||||
|
|
||||||
|
Reason codes:
|
||||||
|
MISSING_TAGS — one or more required Nova tags are absent
|
||||||
|
ENV_MISSING_MANDATORY:<env>:<field> — a per-env mandatory field is missing
|
||||||
|
AGENTIC_MISSING_INTENT — profile=agentic but naturalLanguageIntent absent
|
||||||
|
MISSING_APP_SOURCE — appSource (repo + ref) is missing
|
||||||
|
POLICY_PRECONDITION_MISSING — a declared policy precondition is absent
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_SCHEMA_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "schemas"
|
||||||
|
)
|
||||||
|
|
||||||
|
REQUIRED_TAGS = [
|
||||||
|
"nova:owner",
|
||||||
|
"nova:contract",
|
||||||
|
"nova:environment",
|
||||||
|
"nova:cost-center",
|
||||||
|
"nova:ref",
|
||||||
|
]
|
||||||
|
|
||||||
|
ENV_MANDATORY: dict[str, list[str]] = {
|
||||||
|
"dev": [], # dev requires only the base contract shape (id+environment+infrastructure)
|
||||||
|
"qa": ["validation.e2eSuite", "validation.loadTest"],
|
||||||
|
"prod": ["runbook", "dashboard", "oncall"],
|
||||||
|
"dr": ["drDrillRef"],
|
||||||
|
}
|
||||||
|
|
||||||
|
AGENTIC_REQUIRED = ["naturalLanguageIntent", "confidenceAtSubmission", "agentTrace"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReadinessResult:
|
||||||
|
"""Structured result of the submission-readiness gate."""
|
||||||
|
|
||||||
|
ready: bool
|
||||||
|
reason_codes: list[str] = field(default_factory=list)
|
||||||
|
contract_id: str | None = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"ready": self.ready,
|
||||||
|
"reason_codes": self.reason_codes,
|
||||||
|
"contractId": self.contract_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.ready:
|
||||||
|
return f"READY — contract {self.contract_id} passes submission-readiness gate"
|
||||||
|
codes = "; ".join(self.reason_codes) if self.reason_codes else "unknown"
|
||||||
|
return f"NOT READY — contract {self.contract_id}: {codes}"
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_contract_schema(contract: dict[str, Any]) -> list[str]:
|
||||||
|
"""Validate the contract against contract.schema.json (the shape).
|
||||||
|
Returns a list of reason codes (empty if valid). Falls back to no-op
|
||||||
|
if jsonschema or the schema file is unavailable (the contract is
|
||||||
|
validated upstream by run_platform.sh in the normal path).
|
||||||
|
"""
|
||||||
|
codes: list[str] = []
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
schema_path = os.path.join(_SCHEMA_DIR, "contract.schema.json")
|
||||||
|
with open(schema_path) as f:
|
||||||
|
schema = json.load(f)
|
||||||
|
jsonschema.validate(instance=contract, schema=schema)
|
||||||
|
except (OSError, ImportError):
|
||||||
|
pass
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
codes.append(f"CONTRACT_SCHEMA_INVALID:{e.message}")
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nested(data: dict[str, Any], dotted_key: str) -> Any:
|
||||||
|
parts = dotted_key.split(".")
|
||||||
|
val: Any = data
|
||||||
|
for p in parts:
|
||||||
|
if not isinstance(val, dict) or p not in val:
|
||||||
|
return None
|
||||||
|
val = val[p]
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def check_readiness(submission: dict[str, Any]) -> ReadinessResult:
|
||||||
|
"""Run the full submission-readiness gate.
|
||||||
|
|
||||||
|
1. Validate the contract shape (contract.schema.json).
|
||||||
|
2. Validate the readiness schema (submission-readiness.schema.json).
|
||||||
|
3. Run the semantic readiness checks (tags, env mandatory, agentic, appSource, policy).
|
||||||
|
|
||||||
|
Returns a ReadinessResult. Never raises — all failures are reason codes.
|
||||||
|
"""
|
||||||
|
contract_id = submission.get("contractId") or submission.get("id", "unknown")
|
||||||
|
codes: list[str] = []
|
||||||
|
|
||||||
|
# Step 1: contract shape validation
|
||||||
|
contract_shape = {k: v for k, v in submission.items() if k in ("id", "name", "environment", "infrastructure")}
|
||||||
|
if contract_shape:
|
||||||
|
codes.extend(_validate_contract_schema(contract_shape))
|
||||||
|
|
||||||
|
# Step 2: readiness schema validation
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
schema_path = os.path.join(_SCHEMA_DIR, "submission-readiness.schema.json")
|
||||||
|
with open(schema_path) as f:
|
||||||
|
readiness_schema = json.load(f)
|
||||||
|
jsonschema.validate(instance=submission, schema=readiness_schema)
|
||||||
|
except (OSError, ImportError):
|
||||||
|
pass
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
codes.append(f"READINESS_SCHEMA_INVALID:{e.message}")
|
||||||
|
|
||||||
|
# Step 3: semantic checks (reason codes for citizen-developer-facing errors)
|
||||||
|
|
||||||
|
# 3a: tags
|
||||||
|
tags = submission.get("tags", {})
|
||||||
|
missing_tags = [t for t in REQUIRED_TAGS if t not in tags or not tags[t]]
|
||||||
|
if missing_tags:
|
||||||
|
codes.append(f"MISSING_TAGS:{','.join(missing_tags)}")
|
||||||
|
|
||||||
|
# 3b: env mandatory (W3.E per-env table)
|
||||||
|
env = submission.get("environment")
|
||||||
|
if env and env in ENV_MANDATORY:
|
||||||
|
for field_key in ENV_MANDATORY[env]:
|
||||||
|
val = _get_nested(submission, field_key)
|
||||||
|
if val is None:
|
||||||
|
codes.append(f"ENV_MISSING_MANDATORY:{env}:{field_key}")
|
||||||
|
|
||||||
|
# 3c: agentic profile markers
|
||||||
|
if submission.get("profile") == "agentic":
|
||||||
|
for marker in AGENTIC_REQUIRED:
|
||||||
|
if not submission.get(marker):
|
||||||
|
codes.append(f"AGENTIC_MISSING_INTENT:{marker}")
|
||||||
|
|
||||||
|
# 3d: appSource
|
||||||
|
app_source = submission.get("appSource")
|
||||||
|
if not app_source or not app_source.get("repo") or not app_source.get("ref"):
|
||||||
|
codes.append("MISSING_APP_SOURCE")
|
||||||
|
|
||||||
|
# 3e: policy preconditions (warn if declared but not enforced this milestone)
|
||||||
|
policy = submission.get("policyPreconditions", {})
|
||||||
|
if not policy:
|
||||||
|
codes.append("POLICY_PRECONDITION_MISSING")
|
||||||
|
|
||||||
|
ready = len(codes) == 0
|
||||||
|
return ReadinessResult(ready=ready, reason_codes=codes, contract_id=contract_id)
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv: list[str]) -> int:
|
||||||
|
"""CLI entry: python3 -m core.submission_readiness <contract.json>
|
||||||
|
|
||||||
|
Also invoked via contract_ingestor.py --check-readiness (D-133).
|
||||||
|
Prints the ReadinessResult to stdout; exits 0 if ready, 1 if not.
|
||||||
|
"""
|
||||||
|
if len(argv) < 2:
|
||||||
|
print("Usage: submission_readiness <contract.json>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
path = argv[1]
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
submission = json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
|
print(f"ERROR: cannot read {path}: {e}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
result = check_readiness(submission)
|
||||||
|
print(result)
|
||||||
|
print(json.dumps(result.to_dict(), indent=2))
|
||||||
|
return 0 if result.ready else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(cli_main(sys.argv))
|
||||||
@@ -100,9 +100,11 @@ CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `--allow-local-files` flag is **required** for PPTX export so the local
|
The `--allow-local-files` flag is **required** for PPTX export so the local
|
||||||
PNG diagrams are embedded in the file. PPTX files are not committed to the
|
PNG diagrams are embedded in the file. As of v1.18 (REQ-228, D-141), PPTX
|
||||||
repo (binary, no meaningful diffs) — they are uploaded to the Gitea release
|
files **are committed to the repo** as first-class binary artifacts (no LFS)
|
||||||
as downloadable attachments.
|
and are also attached to the phase's Gitea release via
|
||||||
|
`scripts/attach_release_asset.py`. The render + commit + attach pipeline is
|
||||||
|
automated by `scripts/render_deck.sh`.
|
||||||
|
|
||||||
### Step 4 — Talking points (presenter cues)
|
### Step 4 — Talking points (presenter cues)
|
||||||
|
|
||||||
@@ -341,7 +343,13 @@ attachments to the Gitea release.
|
|||||||
|
|
||||||
## Current decks
|
## Current decks
|
||||||
|
|
||||||
| Deck | Source of truth (Step 1) | Marp deck (Step 2) | Rendered HTML (Step 3) | Talking points (Step 4) | Slides | Audience |
|
| Deck | Source of truth (Step 1) | Marp deck (Step 2) | Rendered HTML + PPTX (Step 3) | Talking points (Step 4) | Slides | Audience |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| How the Platform Works | `how-the-platform-works.md` | `how-the-platform-works-marp.md` | `how-the-platform-works.html` | `how-the-platform-works-talking-points.md` | 11 main + TOC + 8 appendix (20) | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
| Nova — The No-Humans Infrastructure Platform | `nova-no-humans-platform.md` | `nova-no-humans-platform-marp.md` | `nova-no-humans-platform.html` + `.pptx` (committed + release-attached) | `nova-no-humans-platform-talking-points.md` | 19 main + 2 appendix (21) | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
||||||
| The Developer Experience | `the-developer-experience.md` | `the-developer-experience-marp.md` | `the-developer-experience.html` | `the-developer-experience-talking-points.md` | 11 main + TOC + 7 appendix (19) | CTO, Head of Cloud, Head of Infra, Head of DevOps |
|
|
||||||
|
> **v1.18 (D-130):** the two legacy decks (How the Platform Works + The
|
||||||
|
> Developer Experience) were consolidated into a single unified narrative
|
||||||
|
> deck with a 5-act arc (Problem → Vision → How → Proof → Roadmap). v1.18
|
||||||
|
> (REQ-226) adds 3 slides (17 Scope, 18 RACI, 19 Atelier) → 21 total. The
|
||||||
|
> S&P Global Energy theme is restored (REQ-214, P1). PPTX is committed to
|
||||||
|
> git + attached to the Gitea release (REQ-228, D-141).
|
||||||
@@ -6,13 +6,25 @@ size: 16x9
|
|||||||
header: 'Nova — The No-Humans Infrastructure Platform'
|
header: 'Nova — The No-Humans Infrastructure Platform'
|
||||||
footer: 'Act %{page}/5 — v1.17'
|
footer: 'Act %{page}/5 — v1.17'
|
||||||
style: |
|
style: |
|
||||||
section { font-size: 0.85em; }
|
section {
|
||||||
h1 { color: #1a1a2e; }
|
font-family: "Akkurat Pro", "Helvetica Neue", "Arial", sans-serif;
|
||||||
h2 { color: #16213e; }
|
font-size: 22px;
|
||||||
table { font-size: 0.75em; }
|
color: #1B1B1B;
|
||||||
.badge { padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
|
}
|
||||||
.badge.planned { background: #fff3cd; color: #856404; }
|
h1 { color: #D6002A; font-size: 34px; margin-bottom: 0.3em; }
|
||||||
section.title { background: #1a1a2e; color: white; }
|
h2 { color: #D6002A; font-size: 26px; margin-bottom: 0.2em; }
|
||||||
|
section.title { background: #1B1B1B; color: #fff; border-top: 8px solid #D6002A; }
|
||||||
|
section.title h1 { color: #fff; }
|
||||||
|
table { font-size: 18px; width: 100%; }
|
||||||
|
th { background: #F0F0F0; }
|
||||||
|
blockquote { border-left: 4px solid #D6002A; color: #2E2E2E; font-size: 20px; }
|
||||||
|
img { display: block; margin: 0 auto; max-height: 320px; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||||
|
font-size: 14px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge.today { background: #c6f6d5; color: #22543d; }
|
||||||
|
.badge.planned { background: #fef3c7; color: #78350f; }
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- _class: title -->
|
<!-- _class: title -->
|
||||||
@@ -22,7 +34,7 @@ style: |
|
|||||||
|
|
||||||
**Shifting from Operational Overhead to Strategic Value**
|
**Shifting from Operational Overhead to Strategic Value**
|
||||||
|
|
||||||
v1.17 — Strategic Direction, Leadership Metrics & Unified Story
|
v1.18 — Citizen Developer & Production-Grade Guidance
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,7 +49,7 @@ v1.17 — Strategic Direction, Leadership Metrics & Unified Story
|
|||||||
2. **Vision** — Nova's strategic direction (NORTH_STAR)
|
2. **Vision** — Nova's strategic direction (NORTH_STAR)
|
||||||
3. **How** — the pipeline, Decision Ledger, attestation gates
|
3. **How** — the pipeline, Decision Ledger, attestation gates
|
||||||
4. **Proof** — grounded metrics that make the claim defensible
|
4. **Proof** — grounded metrics that make the claim defensible
|
||||||
5. **Roadmap** — deferred metrics with unblock paths + the ask
|
5. **Roadmap** — deferred metrics with unblock paths + the ask + scope + RACI
|
||||||
|
|
||||||
**Benefit:** you leave knowing which claims are proven today, which are pipeline-ready, and which are deferred with a documented unblock path — no marketing, just grounded evidence.
|
**Benefit:** you leave knowing which claims are proven today, which are pipeline-ready, and which are deferred with a documented unblock path — no marketing, just grounded evidence.
|
||||||
|
|
||||||
@@ -289,6 +301,56 @@ From `docs/METRICS_DEFERRED_ROADMAP.md`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Slide 17 — Scope: Downstream of PDLC
|
||||||
|
|
||||||
|
**Nova governs infrastructure + delivery. The PDLC (product backlog, code authorship, IDE) is upstream — Nova never penetrates it.**
|
||||||
|
|
||||||
|
- **The PDLC is upstream:** product backlog, code authorship (AI agent / IDE / agentic SDLC), sprint planning, application business logic
|
||||||
|
- **Nova is downstream:** contract ingestion → submission-readiness gate → policy → cloud lifecycle → environment progression → audit + attestation
|
||||||
|
- **Integration is only through the contract boundary:** the citizen developer's AI coding agent, an upstream agentic SDLC, or any dev platform may all produce submissions — the source does not matter as all are subject to the same compliance standards
|
||||||
|
- Nova validates the submission, not the author
|
||||||
|
- Cites `docs/scope.md` + `PROJECT.md` § Scope
|
||||||
|
|
||||||
|
**Benefit:** you now know the scope boundary — Nova is purpose-built for infrastructure operations, not product development; integration is through one validated contract.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 18 — RACI: Who Owns What
|
||||||
|
|
||||||
|
**Three roles, one matrix — the citizen developer owns FRs + UAT, the platform owns NFRs + infra + QA + prod deploy, release management is co-owned.**
|
||||||
|
|
||||||
|
| Work Category | Citizen Dev | Platform | Release Mgmt |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Functional Requirements (FRs) | **R/A** | C | I |
|
||||||
|
| User Acceptance Testing (UAT) | **R/A** | C | I |
|
||||||
|
| Non-Functional Requirements (NFRs) | I | **R/A** | C |
|
||||||
|
| Infrastructure (cloud, state, IAM) | I | **R/A** | C |
|
||||||
|
| QA (policy, confidence, schema) | C | **R/A** | I |
|
||||||
|
| Production deployment to cloud | I | **R/A** | C |
|
||||||
|
| Release attestation (QA + SRE) | **A** | R | **R** |
|
||||||
|
|
||||||
|
- **Compliance-standard equivalence:** FRs + UAT may come from any upstream source (AI agent, agentic SDLC, dev platform) — all pass the same submission-readiness gate
|
||||||
|
- **Release co-ownership:** the platform runs the attestations agentically; the citizen developer oversees and triggers the actual release (human at the stage gate)
|
||||||
|
- Cites `docs/raci.md` + `PROJECT.md` § RACI Matrix
|
||||||
|
|
||||||
|
**Benefit:** you now know exactly what you bring (FRs + UAT), what Nova provides (NFRs + infra + QA + prod deploy), and what you co-own (the release attestation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 19 — Production-Grade Guidance via Atelier
|
||||||
|
|
||||||
|
**Nova instructs the citizen developer's AI agent on production-grade engineering — skills + an MCP server with agentic validation beyond deterministic scanners.**
|
||||||
|
|
||||||
|
- **Skills (9):** markdown files under `skills/` keyed to Atelier domain paths (api, security, data, testing, observability, errors, devops, infrastructure-as-code, compliance) — extending the BA.A 5-skill catalog
|
||||||
|
- **MCP server:** `mcp/atelier/server.py` (plugin-registry, stdio) — 4 tools: `lookup_principle`, `list_domains`, `matrix_lookup`, `validate_against_principles`
|
||||||
|
- **Agentic validation:** catches C1 correctness + C2 clarity + C7 observability gaps that Wiz/Checkmarx/Mend cannot — deterministic tools check policy/secrets; the MCP server checks engineering discipline
|
||||||
|
- **Vendored Atelier** (pinned tag v0.3.6): audit reproducibility — a validation result is replayable against the exact principles that produced it
|
||||||
|
- Cites `docs/skills.md` + `mcp/atelier/README.md`
|
||||||
|
|
||||||
|
**Benefit:** you now know the citizen developer is not unguided — Nova provides production-grade engineering principles via skills + an MCP server, so the AI agent's submissions meet the same standards regardless of upstream source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<!-- _class: title -->
|
<!-- _class: title -->
|
||||||
<!-- _paginate: false -->
|
<!-- _paginate: false -->
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,27 @@
|
|||||||
- "Pipeline-ready" → "production-proven" is the value proposition
|
- "Pipeline-ready" → "production-proven" is the value proposition
|
||||||
- **Key takeaway:** approve a pilot + the ledger build-out to move from pipeline-ready to production-proven
|
- **Key takeaway:** approve a pilot + the ledger build-out to move from pipeline-ready to production-proven
|
||||||
|
|
||||||
|
### Slide 17 — Scope: Downstream of PDLC
|
||||||
|
- Nova governs infra + delivery only; the PDLC (product backlog, code authorship, IDE) is upstream
|
||||||
|
- Integration is only through the validated contract boundary
|
||||||
|
- Any upstream source (AI agent, agentic SDLC, dev platform) may produce submissions — all subject to the same compliance standards
|
||||||
|
- Nova validates the submission, not the author
|
||||||
|
- **Key takeaway:** Nova is purpose-built for infrastructure operations, not product development; the scope boundary is clean
|
||||||
|
|
||||||
|
### Slide 18 — RACI: Who Owns What
|
||||||
|
- Citizen Developer owns FRs + UAT (via any upstream source — AI agent, SDLC, dev platform — all pass the same gate)
|
||||||
|
- Platform owns NFRs + infra + QA + prod deploy
|
||||||
|
- Release Management is co-owned: platform runs attestations agentically, citizen developer oversees + triggers the release (human at stage gate)
|
||||||
|
- The compliance-standard equivalence is the key: the source does not matter; the submission does
|
||||||
|
- **Key takeaway:** you bring FRs + UAT; Nova provides NFRs + infra + QA + prod deploy; the release is co-owned with you at the stage gate
|
||||||
|
|
||||||
|
### Slide 19 — Production-Grade Guidance via Atelier
|
||||||
|
- Nova instructs the citizen developer's AI agent via skills (9 markdown files) + an MCP server (4 tools, plugin-registry, stdio)
|
||||||
|
- The MCP server provides agentic validation beyond deterministic scanners — catches correctness, clarity, observability gaps that Wiz/Checkmarx/Mend cannot
|
||||||
|
- Atelier is vendored (pinned tag) for audit reproducibility — a validation result is replayable
|
||||||
|
- This is how Nova ensures the citizen developer's submissions meet production-grade standards regardless of upstream source
|
||||||
|
- **Key takeaway:** the citizen developer is not unguided — Nova provides engineering principles via skills + MCP, so every submission meets the same standards
|
||||||
|
|
||||||
### Appendix A1 — Metrics Glossary
|
### Appendix A1 — Metrics Glossary
|
||||||
- Reference for every metric mentioned in the deck
|
- Reference for every metric mentioned in the deck
|
||||||
- Use if the audience asks "what does X mean?"
|
- Use if the audience asks "what does X mean?"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,85 @@
|
|||||||
|
# RACI — Who Owns What
|
||||||
|
|
||||||
|
> **Source of truth:** `.ciagent/PROJECT.md` § RACI Matrix (v1.18, REQ-215,
|
||||||
|
> D-139). This page is the citizen-developer-facing copy.
|
||||||
|
|
||||||
|
Nova's delivery lifecycle has three roles. This page clarifies who owns
|
||||||
|
what — so the citizen developer knows what they bring, what the platform
|
||||||
|
provides, and what is co-owned.
|
||||||
|
|
||||||
|
## The Three Roles
|
||||||
|
|
||||||
|
### Citizen Developer (CD)
|
||||||
|
|
||||||
|
That's you — the consumer (technical developer L3A or non-technical L3B).
|
||||||
|
You are **Responsible** for all **Functional Requirements (FRs)** and
|
||||||
|
**User Acceptance Testing (UAT)**. You produce the FRs + UAT via your AI
|
||||||
|
coding agent, an upstream agentic SDLC platform, or any upstream
|
||||||
|
development platform. **The source does not matter** — all are subject
|
||||||
|
to the same compliance standards (the submission-readiness gate, the
|
||||||
|
contract schema, the policy envelope, the immutable audit stream). Nova
|
||||||
|
validates the submission, not the author.
|
||||||
|
|
||||||
|
### Platform (Nova)
|
||||||
|
|
||||||
|
Nova is **Responsible** for all **Non-Functional Requirements (NFRs)**,
|
||||||
|
**Infrastructure** (cloud resource lifecycle, state, IAM), **QA** (the
|
||||||
|
platform-side quality checks: policy enforcement, confidence scoring,
|
||||||
|
schema validation), and **Production deployments to cloud** (the apply
|
||||||
|
path, the pipeline, the release mechanics).
|
||||||
|
|
||||||
|
### Release Management (RM) — co-owned
|
||||||
|
|
||||||
|
The release is **co-owned**. The platform performs the QA + SRE
|
||||||
|
attestations agentically (it runs the confidence signal, the policy
|
||||||
|
checks, the separation-of-duties). The citizen developer **oversees and
|
||||||
|
triggers** the actual release — the human attestation at the stage gate
|
||||||
|
is your authorization. The platform runs the checks; you authorize the
|
||||||
|
promotion. This is the "autonomy in operations, human at stage gates"
|
||||||
|
model.
|
||||||
|
|
||||||
|
## The Matrix
|
||||||
|
|
||||||
|
| Work Category | Citizen Developer | Platform | Release Management |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Functional Requirements (FRs)** | **R/A** | C | I |
|
||||||
|
| **User Acceptance Testing (UAT)** | **R/A** | C | I |
|
||||||
|
| **Non-Functional Requirements (NFRs)** | I | **R/A** | C |
|
||||||
|
| **Infrastructure (cloud, state, IAM)** | I | **R/A** | C |
|
||||||
|
| **QA (policy, confidence, schema checks)** | C | **R/A** | I |
|
||||||
|
| **Production deployment to cloud** | I | **R/A** | C |
|
||||||
|
| **Release attestation (QA + SRE sign-off)** | **A** | R | **R** |
|
||||||
|
|
||||||
|
**Key:** **R** = Responsible (does the work) · **A** = Accountable (owns
|
||||||
|
the outcome, sign-off) · **C** = Consulted · **I** = Informed.
|
||||||
|
|
||||||
|
## What This Means in Practice
|
||||||
|
|
||||||
|
**You (Citizen Developer) bring:**
|
||||||
|
- Your application code + a contract that declares intent.
|
||||||
|
- Your FRs (what the application does).
|
||||||
|
- Your UAT (you accept the deployment when it meets your FRs).
|
||||||
|
|
||||||
|
**Nova (Platform) provides:**
|
||||||
|
- The NFRs (security, observability, compliance — baked into the
|
||||||
|
pipeline, not your concern).
|
||||||
|
- The infrastructure (cloud resources, state management, IAM scoping).
|
||||||
|
- The QA (policy enforcement, confidence scoring, schema validation).
|
||||||
|
- The production deployment (the apply path, the pipeline, the release).
|
||||||
|
|
||||||
|
**You co-own the release:**
|
||||||
|
- Nova runs the attestations (QA confidence, SRE operational readiness).
|
||||||
|
- You authorize the promotion at the stage gate. No promotion happens
|
||||||
|
without your recorded attestation.
|
||||||
|
|
||||||
|
## Compliance Standards Apply Equally
|
||||||
|
|
||||||
|
Your FRs + UAT may come from any source — an AI coding agent, an
|
||||||
|
agentic SDLC platform, or a traditional IDE. Nova does not
|
||||||
|
differentiate. All submissions pass through the same gate
|
||||||
|
(`schemas/submission-readiness.schema.json`): tags, environment
|
||||||
|
metadata, policy preconditions, profile markers. The compliance
|
||||||
|
standards are the same regardless of how the code was authored. This
|
||||||
|
is by design: the audit trail is the same, the policy envelope is the
|
||||||
|
same, the evidence stream is the same. The source does not matter; the
|
||||||
|
submission does.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Scope — Nova is Downstream of PDLC
|
||||||
|
|
||||||
|
> **Source of truth:** `.ciagent/PROJECT.md` § Scope (v1.18, REQ-216).
|
||||||
|
> This page is the citizen-developer-facing copy.
|
||||||
|
|
||||||
|
## The Boundary
|
||||||
|
|
||||||
|
The **Product Development Lifecycle (PDLC)** is **upstream** of Nova. The
|
||||||
|
PDLC includes:
|
||||||
|
|
||||||
|
- Product backlog / roadmap planning
|
||||||
|
- Code authorship (via AI coding agent, IDE, or agentic SDLC platform)
|
||||||
|
- Sprint planning / issue tracking
|
||||||
|
- Application business logic
|
||||||
|
- IDE workflows / developer experience
|
||||||
|
|
||||||
|
Nova never penetrates the PDLC. Nova's domain is **infrastructure +
|
||||||
|
delivery only**.
|
||||||
|
|
||||||
|
## What Nova Does
|
||||||
|
|
||||||
|
Nova governs the downstream half:
|
||||||
|
|
||||||
|
- **Contract ingestion** — the validated entry point
|
||||||
|
- **Submission-readiness gate** — what is acceptable to start
|
||||||
|
(`schemas/submission-readiness.schema.json`)
|
||||||
|
- **Policy enforcement** — the confidence signal, Checkov, tagging
|
||||||
|
- **Cloud resource lifecycle** — Terraform plan/apply, state, IAM
|
||||||
|
- **Environment progression** — dev (autonomous) → qa (QA attestation) →
|
||||||
|
prod (SRE attestation) → dr (SRE attestation)
|
||||||
|
- **Immutable audit + attestation** — the Decision Ledger, the evidence
|
||||||
|
stream, the HITL gates
|
||||||
|
|
||||||
|
## The Integration Point
|
||||||
|
|
||||||
|
Integration between the PDLC and Nova is **only** through the validated,
|
||||||
|
published contract boundary:
|
||||||
|
|
||||||
|
```
|
||||||
|
PDLC (upstream) Nova (downstream)
|
||||||
|
───────────────── ─────────────────
|
||||||
|
product backlog contract ingestion
|
||||||
|
code authorship (AI agent / IDE / SDLC) → submission-readiness gate
|
||||||
|
sprint planning → policy enforcement
|
||||||
|
application business logic → cloud resource lifecycle
|
||||||
|
→ environment progression (dev→qa→prod→dr)
|
||||||
|
→ immutable audit + attestation
|
||||||
|
```
|
||||||
|
|
||||||
|
The citizen developer's AI coding agent, an upstream agentic SDLC
|
||||||
|
platform, or any upstream development platform may all produce
|
||||||
|
submissions. **The source does not matter** — all are subject to the
|
||||||
|
same compliance standards. Nova validates the submission, not the
|
||||||
|
author.
|
||||||
|
|
||||||
|
## What Nova is Not
|
||||||
|
|
||||||
|
- Not an upstream development platform (no product backlogs, IDE, code
|
||||||
|
authorship).
|
||||||
|
- Not a general-purpose AI agent platform (autonomy is narrow, bounded
|
||||||
|
by policy envelopes).
|
||||||
|
- Not a legacy infrastructure bridge (no VMs/bare metal/OS).
|
||||||
|
- Not a permissive delivery highway (no escape hatches past confidence
|
||||||
|
or HITL).
|
||||||
|
- Not a mutable audit log (VCS history ≠ regulatory evidence).
|
||||||
|
|
||||||
|
These anti-goals (from `docs/vision.md` §7 and Core Tenet #2) are
|
||||||
|
promoted here from buried tenets to an unmissable scope statement.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Skills — Production-Grade Guidance for the Citizen Developer
|
||||||
|
|
||||||
|
> **Source of truth (v1.18, REQ-221, REQ-222).** The Nova skill catalog
|
||||||
|
> extends the BA.A 5-skill catalog (web API, worker, scheduled job, static
|
||||||
|
> asset, basic observability bootstrap) with Atelier-derived production-
|
||||||
|
> grade engineering principles. Each skill is a markdown file under
|
||||||
|
> `skills/` keyed to an Atelier domain path.
|
||||||
|
|
||||||
|
## How the Citizen Developer's AI Agent Consumes Skills
|
||||||
|
|
||||||
|
1. **Before completing a task**, read the relevant skill file(s) that
|
||||||
|
match the task's domain.
|
||||||
|
2. **Run `review/agent-checklist.md`** (from Atelier) before finishing —
|
||||||
|
the checklist items are the gate between "the code is written" and
|
||||||
|
"the task is done."
|
||||||
|
3. **Use the Atelier MCP server** (`mcp/atelier/server.py`, P5) for
|
||||||
|
agentic validation — the `atelier.validate_against_principles` tool
|
||||||
|
catches correctness/clarity/simplicity/observability gaps that
|
||||||
|
deterministic scanners (Wiz, Checkmarx, Mend) cannot.
|
||||||
|
|
||||||
|
## The 9 Skills
|
||||||
|
|
||||||
|
| Skill | Atelier Source | Core Principles | BA.A Mapping |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [`api.md`](../skills/api.md) | `domains/api/` | C1, C2, C6 | web API |
|
||||||
|
| [`security.md`](../skills/security.md) | `domains/security/` | C1 | cross-cutting (all 5) |
|
||||||
|
| [`data.md`](../skills/data.md) | `domains/data/` | C1, C4, C6 | web API, worker, scheduled job |
|
||||||
|
| [`testing.md`](../skills/testing.md) | `domains/testing/` | C1, C5 | UAT (citizen-dev RACI) |
|
||||||
|
| [`observability.md`](../skills/observability.md) | `domains/observability/` | C7 | basic observability bootstrap |
|
||||||
|
| [`errors.md`](../skills/errors.md) | `domains/errors/` | C1, C7 | web API, worker, scheduled job |
|
||||||
|
| [`devops.md`](../skills/devops.md) | `domains/devops/` | C5, C7, C8 | scheduled job, worker |
|
||||||
|
| [`infrastructure-as-code.md`](../skills/infrastructure-as-code.md) | `domains/infrastructure-as-code/` | C1, C5, C8 | static asset |
|
||||||
|
| [`compliance.md`](../skills/compliance.md) | `domains/compliance/` | C1, C5 | cross-cutting (all 5) |
|
||||||
|
|
||||||
|
## Atelier Provenance
|
||||||
|
|
||||||
|
The skills are derived from [Atelier](https://git.cloudinit.dev/coreci/atelier)
|
||||||
|
— a first-principles docs-as-code engineering framework with 8 core
|
||||||
|
principles (C1–C8) and 19 domains, each with 10 derived P-rules. The
|
||||||
|
skills distill the citizen-developer-relevant subset of each domain's
|
||||||
|
first-principles, link to the agent-checklist triggers, and map to the
|
||||||
|
existing BA.A catalog.
|
||||||
|
|
||||||
|
Atelier is vendored under `mcp/atelier/vendor/` (pinned tag, D-136) for
|
||||||
|
audit reproducibility — an agentic validation result is replayable
|
||||||
|
against the exact principles that produced it.
|
||||||
|
|
||||||
|
## The 8 Core Principles (from Atelier)
|
||||||
|
|
||||||
|
| # | Principle | One-line |
|
||||||
|
|---|---|---|
|
||||||
|
| C1 | Correctness | The system does what it is supposed to do, and nothing else. |
|
||||||
|
| C2 | Clarity | The intent of the code is obvious to its reader. |
|
||||||
|
| C3 | Simplicity | The solution is as simple as possible, and no simpler. |
|
||||||
|
| C4 | Locality | Decisions and their consequences live near each other. |
|
||||||
|
| C5 | Reversibility | Every decision can be undone, and the cost of undoing is known. |
|
||||||
|
| C6 | Composability | Parts combine into wholes, and the parts are reusable. |
|
||||||
|
| C7 | Observability | The system's behavior is visible to those who must understand it. |
|
||||||
|
| C8 | Economy | The system uses no more resources than the task requires. |
|
||||||
|
|
||||||
|
Precedence: C1 > C2 > C3 > C4 > C5 > C6 > C7 > C8. Correctness is never
|
||||||
|
sacrificed.
|
||||||
|
|
||||||
|
## Reference-Only Domains (cited inside skills, not elevated to skill files)
|
||||||
|
|
||||||
|
These 4 Atelier domains are relevant to a citizen developer but are cited
|
||||||
|
inside the 9 skills above rather than getting their own skill file:
|
||||||
|
|
||||||
|
- **Performance** (`domains/performance/`) — cited in `observability.md` +
|
||||||
|
`devops.md` (bounded operations, timeouts, N+1)
|
||||||
|
- **Documentation** (`domains/documentation/`) — the runbook requirement
|
||||||
|
(W3.E prod mandatory) is the documentation skill in practice
|
||||||
|
- **Concurrency** (`domains/concurrency/`) — cited in `errors.md` +
|
||||||
|
`devops.md` (bounded queues, cancellation, timeout)
|
||||||
|
- **AI/ML** (`domains/ai-ml/`) — scope: engineering discipline (data
|
||||||
|
versioning, evaluation, serving, drift), not algorithm design
|
||||||
|
|
||||||
|
## Excluded Domains (not relevant to Nova citizen developer)
|
||||||
|
|
||||||
|
6 Atelier domains are excluded from the Nova skill catalog (not relevant
|
||||||
|
to a citizen developer building on Nova's infrastructure platform):
|
||||||
|
|
||||||
|
- UI/UX — Nova has no frontend (frontend-engineer deactivated, PERSONAS.md)
|
||||||
|
- Kubernetes — Nova is AWS-only this milestone (NORTH_STAR Non-Goal #7)
|
||||||
|
- GitOps + Operators — future roadmap (no GitOps reconciler today)
|
||||||
|
- Edge — not in scope (Nova is cloud, not edge)
|
||||||
|
- Messaging — not in scope (Nova deploys infra, not message brokers)
|
||||||
|
- i18n — application-level concern, not infrastructure
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Submission Readiness — What is Acceptable to Start
|
||||||
|
|
||||||
|
> **Source of truth:** `schemas/submission-readiness.schema.json` (v1.18,
|
||||||
|
> REQ-217). The validator is `core/submission_readiness.py` (REQ-218),
|
||||||
|
> invoked as `python3 -m core.lambda.contract_ingestor --check-readiness
|
||||||
|
> <submission.json>` (D-133).
|
||||||
|
|
||||||
|
Nova's submission-readiness gate defines what is **acceptable to start**.
|
||||||
|
It is a superset gate *above* contract-schema validity: the contract schema
|
||||||
|
(`schemas/contract.schema.json`) defines the **shape** (id / name /
|
||||||
|
environment / infrastructure); the readiness schema defines the **gate**
|
||||||
|
(tags, per-env mandatory metadata, policy preconditions, profile markers,
|
||||||
|
appSource). Both must pass before ingestion proceeds.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
citizen developer submits
|
||||||
|
↓
|
||||||
|
contract.schema.json validation (shape) ← the existing check
|
||||||
|
↓
|
||||||
|
submission-readiness.schema.json (gate) ← the new check
|
||||||
|
├── contractId present (non-empty)
|
||||||
|
├── environment valid (dev/qa/prod/dr)
|
||||||
|
├── tags: all 5 Nova tags present (D-054)
|
||||||
|
├── policyPreconditions declared
|
||||||
|
├── profile: developer or agentic
|
||||||
|
│ └── if agentic: naturalLanguageIntent + confidenceAtSubmission + agentTrace
|
||||||
|
├── appSource: repo + ref (for runtime fetch)
|
||||||
|
└── per-env mandatory (W3.E):
|
||||||
|
dev → stack + environment
|
||||||
|
qa → + validation.e2eSuite + validation.loadTest
|
||||||
|
prod → + runbook + dashboard + oncall
|
||||||
|
dr → + drDrillRef
|
||||||
|
↓
|
||||||
|
ready → proceed to contract ingestion
|
||||||
|
not ready → reject with citizen-developer-facing error (reason code)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reason Codes
|
||||||
|
|
||||||
|
When a submission is not ready, the validator returns one or more reason
|
||||||
|
codes. These are citizen-developer-facing — no stack traces.
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `MISSING_TAGS:<tag1>,<tag2>` | One or more required Nova tags are absent |
|
||||||
|
| `ENV_MISSING_MANDATORY:<env>:<field>` | A per-env mandatory field (W3.E) is missing |
|
||||||
|
| `AGENTIC_MISSING_INTENT:<marker>` | profile=agentic but a required marker is absent |
|
||||||
|
| `MISSING_APP_SOURCE` | appSource (repo + ref) is missing |
|
||||||
|
| `POLICY_PRECONDITION_MISSING` | No policy preconditions declared |
|
||||||
|
| `CONTRACT_SCHEMA_INVALID:<detail>` | The contract shape failed contract.schema.json |
|
||||||
|
| `READINESS_SCHEMA_INVALID:<detail>` | The submission failed the readiness schema |
|
||||||
|
|
||||||
|
## Good Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contractId": "uuid-1234",
|
||||||
|
"id": "webapi",
|
||||||
|
"name": "Customer Web API",
|
||||||
|
"environment": "dev",
|
||||||
|
"tags": {
|
||||||
|
"nova:owner": "consumer-repo",
|
||||||
|
"nova:contract": "uuid-1234",
|
||||||
|
"nova:environment": "dev",
|
||||||
|
"nova:cost-center": "nova-default",
|
||||||
|
"nova:ref": "CHG0678912"
|
||||||
|
},
|
||||||
|
"policyPreconditions": {
|
||||||
|
"public-ingress": false,
|
||||||
|
"encryption_enabled": true,
|
||||||
|
"deletion_protection": true
|
||||||
|
},
|
||||||
|
"profile": "developer",
|
||||||
|
"appSource": {
|
||||||
|
"repo": "consumer/web-api",
|
||||||
|
"ref": "main"
|
||||||
|
},
|
||||||
|
"infrastructure": {
|
||||||
|
"static-assets": {
|
||||||
|
"inputs": {
|
||||||
|
"bucket_name": "webapi-assets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: **READY** — passes the shape + the gate.
|
||||||
|
|
||||||
|
## Rejected Examples
|
||||||
|
|
||||||
|
### Missing Tags
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contractId": "uuid-1234",
|
||||||
|
"environment": "dev",
|
||||||
|
"tags": {
|
||||||
|
"nova:owner": "consumer-repo"
|
||||||
|
},
|
||||||
|
"policyPreconditions": {"public-ingress": false},
|
||||||
|
"profile": "developer",
|
||||||
|
"appSource": {"repo": "consumer/repo", "ref": "main"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: `NOT READY — MISSING_TAGS:nova:contract,nova:environment,nova:cost-center,nova:ref`
|
||||||
|
|
||||||
|
### Agentic Missing Intent
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contractId": "uuid-1234",
|
||||||
|
"environment": "qa",
|
||||||
|
"tags": { "nova:owner": "x", "nova:contract": "x", "nova:environment": "qa", "nova:cost-center": "x", "nova:ref": "x" },
|
||||||
|
"policyPreconditions": {"public-ingress": false},
|
||||||
|
"profile": "agentic",
|
||||||
|
"appSource": {"repo": "x", "ref": "x"},
|
||||||
|
"validation": {"e2eSuite": true, "loadTest": true}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: `NOT READY — AGENTIC_MISSING_INTENT:naturalLanguageIntent; AGENTIC_MISSING_INTENT:confidenceAtSubmission; AGENTIC_MISSING_INTENT:agentTrace`
|
||||||
|
|
||||||
|
### Env Missing Mandatory (prod without runbook)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contractId": "uuid-1234",
|
||||||
|
"environment": "prod",
|
||||||
|
"tags": { "nova:owner": "x", "nova:contract": "x", "nova:environment": "prod", "nova:cost-center": "x", "nova:ref": "x" },
|
||||||
|
"policyPreconditions": {"public-ingress": false},
|
||||||
|
"profile": "developer",
|
||||||
|
"appSource": {"repo": "x", "ref": "x"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: `NOT READY — ENV_MISSING_MANDATORY:prod:runbook; ENV_MISSING_MANDATORY:prod:dashboard; ENV_MISSING_MANDATORY:prod:oncall`
|
||||||
|
|
||||||
|
## Compliance-Standard Equivalence
|
||||||
|
|
||||||
|
The submission-readiness gate applies **equally** to all upstream sources.
|
||||||
|
Whether the citizen developer's submission originated from an AI coding
|
||||||
|
agent, an agentic SDLC platform, or a traditional development platform —
|
||||||
|
the same tags, the same env mandatory, the same policy preconditions, the
|
||||||
|
same profile markers are required. The source does not matter; the
|
||||||
|
submission does. This is the RACI compliance-standard equivalence note
|
||||||
|
(`docs/raci.md`) made machine-checkable.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# mcp/atelier — Nova Atelier MCP server package (v1.18)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Nova Atelier MCP Server
|
||||||
|
|
||||||
|
> **v1.18, REQ-223, REQ-224.** An MCP (Model Context Protocol) server that
|
||||||
|
> exposes Atelier engineering principles to the citizen developer's AI
|
||||||
|
> agent. Plugin-registry architecture (D-140); stdio transport (D-135);
|
||||||
|
> vendored Atelier (D-136) for audit reproducibility.
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
The server exposes 4 tools that let a citizen developer's AI coding agent
|
||||||
|
look up production-grade engineering principles and validate code against
|
||||||
|
them — agentic validation that goes **beyond deterministic scanners**
|
||||||
|
(Wiz, Checkmarx, Mend) by catching correctness, clarity, simplicity, and
|
||||||
|
observability gaps.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `atelier.lookup_principle(domain, principle_id)` | Look up a principle by domain + P-rule ID (e.g., `security`, `P4`). Returns the principle text + the core C-rule it derives from. |
|
||||||
|
| `atelier.list_domains()` | List the 19 Atelier domains with P-rule counts + Nova-relevance. |
|
||||||
|
| `atelier.matrix_lookup(domain)` | Look up the domain→core principle mapping for a given domain. |
|
||||||
|
| `atelier.validate_against_principles(snippet, domains?)` | Validate a code/diff snippet against the Atelier agent-checklist. Returns pass/fail per check item with the principle citation. |
|
||||||
|
|
||||||
|
## Architecture — Plugin Registry (D-140)
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp/atelier/
|
||||||
|
├── server.py # entrypoint: loads plugins, starts server
|
||||||
|
├── plugins/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── principles.py # lookup_principle, list_domains, matrix_lookup
|
||||||
|
│ └── validation.py # validate_against_principles
|
||||||
|
├── vendor/ # pinned Atelier snapshot (D-136)
|
||||||
|
│ ├── VERSION.md # pinned tag + upgrade instructions
|
||||||
|
│ ├── core/first-principles.md
|
||||||
|
│ ├── domains/security/first-principles.md
|
||||||
|
│ ├── review/agent-checklist.md
|
||||||
|
│ └── matrix/principles-matrix.md
|
||||||
|
└── README.md # this file
|
||||||
|
```
|
||||||
|
|
||||||
|
Each plugin module exposes `register(mcp) -> None` and calls `@mcp.tool()`
|
||||||
|
for its tools. `server.py` scans `plugins/` and calls `register` on each.
|
||||||
|
**Future capabilities drop in as a new plugin file — no `server.py` edits.**
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
### With the MCP Python SDK installed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install "mcp[cli]"
|
||||||
|
python3 -m mcp.atelier.server
|
||||||
|
```
|
||||||
|
|
||||||
|
The server runs over stdio. An MCP client (e.g., the citizen developer's
|
||||||
|
AI coding agent) spawns it as a subprocess and calls tools via JSON-RPC.
|
||||||
|
|
||||||
|
### Without the SDK (fallback / test mode)
|
||||||
|
|
||||||
|
The server degrades to a plain-Python tool registry. Tools are callable
|
||||||
|
directly — this is how tests run without the SDK installed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mcp.atelier.server import NovaAtelierServer
|
||||||
|
s = NovaAtelierServer()
|
||||||
|
s.load_plugins()
|
||||||
|
result = s.call_tool("atelier_lookup_principle", {"domain": "security", "principle_id": "P4"})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vendoring (D-136)
|
||||||
|
|
||||||
|
Atelier is vendored under `vendor/` at a pinned tag (`v0.3.6`, see
|
||||||
|
`vendor/VERSION.md`). An agentic validation result is only reproducible if
|
||||||
|
the principles that produced it are pinned. Live-fetch breaks replayability
|
||||||
|
(Atelier `main` drifts). To upgrade:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/update_atelier_vendor.sh <new-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extensibility
|
||||||
|
|
||||||
|
To add a new tool (e.g., a cost-estimation tool, a policy-as-code
|
||||||
|
evaluator): create `plugins/<name>.py`, expose `register(mcp)`, and call
|
||||||
|
`@mcp.tool()` on your function. The server picks it up automatically. No
|
||||||
|
`server.py` edit. This is the extensibility insurance for future
|
||||||
|
capabilities.
|
||||||
|
|
||||||
|
## Transport
|
||||||
|
|
||||||
|
- **Now:** stdio (local agent consumption — the citizen developer's AI
|
||||||
|
agent spawns the server as a subprocess).
|
||||||
|
- **Future:** Streamable HTTP (the MCP SDK supports it on the same
|
||||||
|
`MCPServer` object; adding it is a transport-only change in `server.py`,
|
||||||
|
not a rewrite).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# mcp/atelier package
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# mcp/atelier/plugins package
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""mcp/atelier/plugins/principles.py — principle lookup, domain listing, matrix lookup.
|
||||||
|
|
||||||
|
Implements 3 MCP tools (REQ-223):
|
||||||
|
- atelier.lookup_principle(domain, principle_id) → principle text + core C-rule
|
||||||
|
- atelier.list_domains() → 19 domains with P-rule counts + Nova-relevance
|
||||||
|
- atelier.matrix_lookup(domain) → domain→core principle mapping
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_VENDOR = Path(__file__).resolve().parent.parent / "vendor"
|
||||||
|
|
||||||
|
DOMAINS = [
|
||||||
|
{"domain": "api", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "security", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "data", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "testing", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "performance", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "observability", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "errors", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "documentation", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "concurrency", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "devops", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "infrastructure-as-code", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "kubernetes", "p_rules": 10, "nova_relevant": False},
|
||||||
|
{"domain": "gitops-operators", "p_rules": 10, "nova_relevant": False},
|
||||||
|
{"domain": "ai-ml", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "i18n", "p_rules": 10, "nova_relevant": False},
|
||||||
|
{"domain": "compliance", "p_rules": 10, "nova_relevant": True},
|
||||||
|
{"domain": "edge", "p_rules": 10, "nova_relevant": False},
|
||||||
|
{"domain": "messaging", "p_rules": 10, "nova_relevant": False},
|
||||||
|
{"domain": "ui-ux", "p_rules": 10, "nova_relevant": False},
|
||||||
|
]
|
||||||
|
|
||||||
|
_MATRIX = {
|
||||||
|
"security": [
|
||||||
|
{"p": "P1", "core": "C1", "title": "Boundary Validation"},
|
||||||
|
{"p": "P2", "core": "C1, C8", "title": "Least Privilege"},
|
||||||
|
{"p": "P3", "core": "C1", "title": "Defense in Depth"},
|
||||||
|
{"p": "P4", "core": "C1, C7", "title": "Secrets Never Exposed"},
|
||||||
|
{"p": "P5", "core": "C1", "title": "Authenticated by Default"},
|
||||||
|
{"p": "P6", "core": "C1", "title": "Encrypted in Transit and at Rest"},
|
||||||
|
{"p": "P7", "core": "C1, C7", "title": "Auditable Actions"},
|
||||||
|
{"p": "P8", "core": "C1, C8", "title": "Patched Dependencies"},
|
||||||
|
{"p": "P9", "core": "C1, C6", "title": "Isolated Blast Radius"},
|
||||||
|
{"p": "P10", "core": "C1", "title": "Secure by Default"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(mcp: Any) -> None:
|
||||||
|
"""Register the principles tools with the MCP server (or fallback registry)."""
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def atelier_lookup_principle(domain: str, principle_id: str) -> dict[str, Any]:
|
||||||
|
"""Look up an Atelier principle by domain + P-rule ID (e.g., 'security', 'P4').
|
||||||
|
|
||||||
|
Returns the principle title, text, and the core C-rule(s) it derives from.
|
||||||
|
"""
|
||||||
|
fp = _VENDOR / "domains" / domain / "first-principles.md"
|
||||||
|
if not fp.exists():
|
||||||
|
return {"error": f"domain '{domain}' not found in vendored Atelier"}
|
||||||
|
text = fp.read_text()
|
||||||
|
# Parse the P-rule section
|
||||||
|
pattern = rf"## ({principle_id}\s*—\s*.+?)\n(.+?)(?=\n## |\Z)"
|
||||||
|
match = re.search(pattern, text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return {"error": f"principle '{principle_id}' not found in domain '{domain}'"}
|
||||||
|
title = match.group(1).strip()
|
||||||
|
body = match.group(2).strip()
|
||||||
|
# Find core C-rule from matrix
|
||||||
|
matrix_entry = next(
|
||||||
|
(e for e in _MATRIX.get(domain, []) if e["p"] == principle_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
core = matrix_entry["core"] if matrix_entry else "unknown"
|
||||||
|
return {
|
||||||
|
"domain": domain,
|
||||||
|
"principle_id": principle_id,
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"core_c_rule": core,
|
||||||
|
}
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def atelier_list_domains() -> list[dict[str, Any]]:
|
||||||
|
"""List the 19 Atelier domains with P-rule counts + Nova-relevance."""
|
||||||
|
return DOMAINS
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def atelier_matrix_lookup(domain: str) -> dict[str, Any]:
|
||||||
|
"""Look up the domain→core principle mapping for a given domain."""
|
||||||
|
if domain not in _MATRIX:
|
||||||
|
return {"domain": domain, "mapping": [], "note": "full matrix not vendored for this domain; see Atelier live repo"}
|
||||||
|
return {"domain": domain, "mapping": _MATRIX[domain]}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""mcp/atelier/plugins/validation.py — agentic validation against Atelier principles.
|
||||||
|
|
||||||
|
Implements 1 MCP tool (REQ-223):
|
||||||
|
- atelier.validate_against_principles(snippet, domains) → pass/fail per
|
||||||
|
checklist item with the principle citation. This is the agentic
|
||||||
|
validation BEYOND deterministic scanners (Wiz/Checkmarx/Mend) — it
|
||||||
|
catches correctness/clarity/simplicity/observability gaps that
|
||||||
|
deterministic tools cannot.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Condensed checklist: core C1-C8 + security domain. Each item is a
|
||||||
|
# (check_id, description, heuristic_pattern, principle_citation).
|
||||||
|
_CHECKLIST = [
|
||||||
|
# C1 Correctness
|
||||||
|
{"id": "C1.1", "desc": "Does the code do what the task asked, completely?", "heuristic": r"TODO|FIXME|pass\s*$", "principle": "C1 Correctness", "neg": True},
|
||||||
|
{"id": "C1.2", "desc": "Does it handle failure cases? (errors, timeouts)", "heuristic": r"except\s*:?\s*pass", "principle": "C1 Correctness", "neg": True},
|
||||||
|
{"id": "C1.3", "desc": "Is there a test that would fail if the code were wrong?", "heuristic": r"def test_|describe\(", "principle": "C1 Correctness", "neg": False, "optional": True},
|
||||||
|
# C2 Clarity
|
||||||
|
{"id": "C2.1", "desc": "Are names intent-revealing? (no 'data', 'temp', 'x')", "heuristic": r"\b(data|temp|x|foo|bar|doStuff)\b", "principle": "C2 Clarity", "neg": True},
|
||||||
|
# C3 Simplicity
|
||||||
|
{"id": "C3.1", "desc": "Is there dead code? (unreachable branches)", "heuristic": r"return\s+\w+\s*$.*return", "principle": "C3 Simplicity", "neg": True, "multiline": True},
|
||||||
|
# C7 Observability
|
||||||
|
{"id": "C7.1", "desc": "Are there logs for significant events?", "heuristic": r"log(ger|ging)?|print\(|console\.", "principle": "C7 Observability", "neg": False, "optional": True},
|
||||||
|
{"id": "C7.2", "desc": "Are there secrets in logs?", "heuristic": r"password|secret|token|api_key", "principle": "C7 Observability + Security P4", "neg": True},
|
||||||
|
# Security
|
||||||
|
{"id": "SEC.1", "desc": "No secrets in code/logs/URLs", "heuristic": r"(password|secret|token|api_key)\s*=\s*['\"]", "principle": "Security P4 Secrets Never Exposed", "neg": True},
|
||||||
|
{"id": "SEC.2", "desc": "Input validated at the boundary", "heuristic": r"validate|schema|assert", "principle": "Security P1 Boundary Validation", "neg": False, "optional": True},
|
||||||
|
{"id": "SEC.3", "desc": "Authorization checked, not assumed", "heuristic": r"auth|permission|rbac|authorize", "principle": "Security P5 Authenticated by Default", "neg": False, "optional": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register(mcp: Any) -> None:
|
||||||
|
"""Register the validation tools with the MCP server (or fallback registry)."""
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def atelier_validate_against_principles(snippet: str, domains: list[str] | None = None) -> dict[str, Any]:
|
||||||
|
"""Validate a code/diff snippet against Atelier principles.
|
||||||
|
|
||||||
|
Runs the agent-checklist items against the snippet and returns
|
||||||
|
pass/fail per item with the principle citation. This is the
|
||||||
|
agentic validation BEYOND deterministic scanners (Wiz/Checkmarx/
|
||||||
|
Mend) — it catches correctness/clarity/simplicity/observability
|
||||||
|
gaps that deterministic tools cannot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
snippet: The code or diff text to validate.
|
||||||
|
domains: Optional list of domains to include (default: core + security).
|
||||||
|
"""
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
for check in _CHECKLIST:
|
||||||
|
pattern = check["heuristic"]
|
||||||
|
flags = re.DOTALL if check.get("multiline") else 0
|
||||||
|
found = bool(re.search(pattern, snippet, flags))
|
||||||
|
# neg=True means finding the pattern is a FAIL; neg=False means finding is a PASS
|
||||||
|
if check.get("neg"):
|
||||||
|
status = "FAIL" if found else "PASS"
|
||||||
|
else:
|
||||||
|
if check.get("optional"):
|
||||||
|
status = "PASS" if found else "WARN"
|
||||||
|
else:
|
||||||
|
status = "PASS" if found else "WARN"
|
||||||
|
results.append({
|
||||||
|
"check_id": check["id"],
|
||||||
|
"description": check["desc"],
|
||||||
|
"status": status,
|
||||||
|
"principle": check["principle"],
|
||||||
|
})
|
||||||
|
all_pass = all(r["status"] == "PASS" for r in results)
|
||||||
|
return {
|
||||||
|
"overall": "PASS" if all_pass else "FAIL",
|
||||||
|
"results": results,
|
||||||
|
"domains_checked": domains or ["core", "security"],
|
||||||
|
"note": "Agentic validation beyond Wiz/Checkmarx/Mend — catches correctness, clarity, simplicity, observability gaps.",
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""mcp/atelier/server.py — Nova Atelier MCP server (REQ-223, D-135, D-137, D-140).
|
||||||
|
|
||||||
|
Plugin-registry architecture (D-140): plugins/<name>.py modules each expose
|
||||||
|
``register(mcp) -> None`` and call ``@mcp.tool()`` for their tools. This file
|
||||||
|
scans ``plugins/`` and calls ``register`` on each. Future capabilities drop
|
||||||
|
in as new plugin files — no server.py edits.
|
||||||
|
|
||||||
|
Transport: stdio (D-135). The MCP Python SDK v2 (``modelcontextprotocol/
|
||||||
|
python-sdk``, D-137) is the target. If the SDK is not installed, the server
|
||||||
|
degrades to a plain-Python tool registry that can be tested directly — the
|
||||||
|
tools are callable without MCP. This makes the server testable in CI
|
||||||
|
without the SDK installed.
|
||||||
|
|
||||||
|
Usage (with SDK):
|
||||||
|
python3 -m mcp.atelier.server
|
||||||
|
|
||||||
|
Usage (without SDK, for testing):
|
||||||
|
from mcp.atelier.server import NovaAtelierServer
|
||||||
|
s = NovaAtelierServer()
|
||||||
|
s.load_plugins()
|
||||||
|
result = s.call_tool("atelier.lookup_principle", {"domain": "security", "principle_id": "P4"})
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
_PLUGIN_DIR = pathlib.Path(__file__).parent / "plugins"
|
||||||
|
_VENDOR_DIR = pathlib.Path(__file__).parent / "vendor"
|
||||||
|
|
||||||
|
|
||||||
|
class _ToolRegistry:
|
||||||
|
"""A minimal tool registry that mimics the MCP ``@mcp.tool()`` decorator.
|
||||||
|
|
||||||
|
When the MCP SDK is available, ``NovaAtelierServer`` wraps a real
|
||||||
|
``MCPServer`` and the decorator registers tools with the SDK. When the
|
||||||
|
SDK is absent, this registry is the fallback — tools are callable via
|
||||||
|
``call_tool()`` for testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._tools: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def tool(self, name: str | None = None, description: str | None = None) -> Callable:
|
||||||
|
def decorator(fn: Callable) -> Callable:
|
||||||
|
tool_name = name or fn.__name__
|
||||||
|
self._tools[tool_name] = {
|
||||||
|
"fn": fn,
|
||||||
|
"description": description or fn.__doc__ or "",
|
||||||
|
"name": tool_name,
|
||||||
|
}
|
||||||
|
return fn
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def list_tools(self) -> list[dict[str, str]]:
|
||||||
|
return [{"name": t["name"], "description": t["description"]} for t in self._tools.values()]
|
||||||
|
|
||||||
|
def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
||||||
|
if name not in self._tools:
|
||||||
|
raise KeyError(f"Unknown tool: {name}")
|
||||||
|
return self._tools[name]["fn"](**arguments)
|
||||||
|
|
||||||
|
|
||||||
|
class NovaAtelierServer:
|
||||||
|
"""The Nova Atelier MCP server.
|
||||||
|
|
||||||
|
Wraps an MCP SDK ``MCPServer`` if available; otherwise uses the
|
||||||
|
``_ToolRegistry`` fallback. Plugins are loaded from ``plugins/``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.registry = _ToolRegistry()
|
||||||
|
self._mcp = None
|
||||||
|
try:
|
||||||
|
from mcp.server import MCPServer # type: ignore[import-not-found]
|
||||||
|
self._mcp = MCPServer("atelier")
|
||||||
|
except ImportError:
|
||||||
|
pass # SDK not installed — fallback to _ToolRegistry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mcp(self) -> Any:
|
||||||
|
"""The object plugins register tools on (real MCPServer or fallback)."""
|
||||||
|
return self._mcp if self._mcp is not None else self.registry
|
||||||
|
|
||||||
|
def load_plugins(self) -> list[str]:
|
||||||
|
"""Scan plugins/ and call ``register(mcp)`` on each. Returns loaded names."""
|
||||||
|
loaded: list[str] = []
|
||||||
|
for p in sorted(_PLUGIN_DIR.glob("*.py")):
|
||||||
|
if p.stem == "__init__":
|
||||||
|
continue
|
||||||
|
mod_name = f"mcp.atelier.plugins.{p.stem}"
|
||||||
|
mod = importlib.import_module(mod_name)
|
||||||
|
if hasattr(mod, "register"):
|
||||||
|
mod.register(self.mcp if self._mcp else self.registry)
|
||||||
|
loaded.append(p.stem)
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
def list_tools(self) -> list[dict[str, str]]:
|
||||||
|
if self._mcp is not None:
|
||||||
|
return [{"name": t.name, "description": t.description} for t in self._mcp._tools.values()] # type: ignore[attr-defined]
|
||||||
|
return self.registry.list_tools()
|
||||||
|
|
||||||
|
def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
||||||
|
if self._mcp is not None:
|
||||||
|
raise RuntimeError("MCP SDK call_tool not supported in fallback mode — use the MCP client")
|
||||||
|
return self.registry.call_tool(name, arguments)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""Run the server over stdio (requires the MCP SDK)."""
|
||||||
|
if self._mcp is None:
|
||||||
|
raise RuntimeError("MCP SDK not installed — cannot run server. Install: pip install mcp")
|
||||||
|
self._mcp.run()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_plugin_compat_decorator(registry_or_mcp: Any) -> Callable:
|
||||||
|
"""Return a ``tool()`` decorator that works for both the fallback
|
||||||
|
registry and the real MCP SDK."""
|
||||||
|
if hasattr(registry_or_mcp, "tool"):
|
||||||
|
return registry_or_mcp.tool
|
||||||
|
# Fallback: wrap registry.tool() as a decorator factory
|
||||||
|
return registry_or_mcp.tool
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
server = NovaAtelierServer()
|
||||||
|
loaded = server.load_plugins()
|
||||||
|
print(f"Atelier MCP server — {len(loaded)} plugins loaded: {', '.join(loaded)}", file=sys.stderr)
|
||||||
|
if server._mcp is None:
|
||||||
|
print("MCP SDK not installed — server is in fallback (test) mode.", file=sys.stderr)
|
||||||
|
print("Tools: " + ", ".join(t["name"] for t in server.list_tools()), file=sys.stderr)
|
||||||
|
else:
|
||||||
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Vendored Atelier — Version Pin
|
||||||
|
|
||||||
|
> **Pinned tag:** `v0.3.6` (the v0.4 milestone release, 2026-08-05)
|
||||||
|
> **Commit:** `666b137dbb3c00e81f8740d18b639bc67587d29f`
|
||||||
|
> **P-rule count:** 190 (19 domains × 10 P-rules)
|
||||||
|
> **Vendor date:** 2026-08-06
|
||||||
|
> **Vendor reason:** audit reproducibility (D-136) — an agentic validation
|
||||||
|
> result is only replayable if the principles that produced it are pinned.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
To bump the vendored Atelier to a new tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/update_atelier_vendor.sh <new-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
The script fetches the Atelier repo at the given tag, replaces
|
||||||
|
`mcp/atelier/vendor/`, updates this VERSION.md, and commits the change.
|
||||||
|
Upgrades are **intentional** — never automatic. Atelier `main` is a
|
||||||
|
moving target; pinning is required for audit reproducibility.
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Core First Principles
|
||||||
|
|
||||||
|
The 8 universal axioms. Every domain principle derives from one or more
|
||||||
|
of these. Precedence: C1 > C2 > C3 > C4 > C5 > C6 > C7 > C8.
|
||||||
|
|
||||||
|
## C1 — Correctness
|
||||||
|
The system does what it is supposed to do, and nothing else.
|
||||||
|
|
||||||
|
## C2 — Clarity
|
||||||
|
The intent of the code is obvious to its reader.
|
||||||
|
|
||||||
|
## C3 — Simplicity
|
||||||
|
The solution is as simple as possible, and no simpler.
|
||||||
|
|
||||||
|
## C4 — Locality
|
||||||
|
Decisions and their consequences live near each other.
|
||||||
|
|
||||||
|
## C5 — Reversibility
|
||||||
|
Every decision can be undone, and the cost of undoing is known.
|
||||||
|
|
||||||
|
## C6 — Composability
|
||||||
|
Parts combine into wholes, and the parts are reusable.
|
||||||
|
|
||||||
|
## C7 — Observability
|
||||||
|
The system's behavior is visible to those who must understand it.
|
||||||
|
|
||||||
|
## C8 — Economy
|
||||||
|
The system uses no more resources than the task requires.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Security — First Principles
|
||||||
|
|
||||||
|
## P1 — Boundary Validation
|
||||||
|
All input is validated at the trust boundary. (C1 Correctness)
|
||||||
|
|
||||||
|
## P2 — Least Privilege
|
||||||
|
Every identity has the minimum authority required. (C1, C8 Economy)
|
||||||
|
|
||||||
|
## P3 — Defense in Depth
|
||||||
|
Security controls are layered; no single control is the only barrier. (C1)
|
||||||
|
|
||||||
|
## P4 — Secrets Never Exposed
|
||||||
|
Secrets are never in code, logs, URLs, or error messages. (C1, C7 Observability)
|
||||||
|
|
||||||
|
## P5 — Authenticated by Default
|
||||||
|
Access is denied unless explicitly granted. (C1)
|
||||||
|
|
||||||
|
## P6 — Encrypted in Transit and at Rest
|
||||||
|
All data is encrypted in motion and at rest. (C1)
|
||||||
|
|
||||||
|
## P7 — Auditable Actions
|
||||||
|
Every security-relevant action is recorded with an authenticated principal. (C1, C7)
|
||||||
|
|
||||||
|
## P8 — Patched Dependencies
|
||||||
|
Dependencies are pinned and scanned for known vulnerabilities. (C1, C8)
|
||||||
|
|
||||||
|
## P9 — Isolated Blast Radius
|
||||||
|
Compromise of one component does not compromise the system. (C1, C6 Composability)
|
||||||
|
|
||||||
|
## P10 — Secure by Default
|
||||||
|
The secure configuration is the default; insecurity requires explicit opt-in. (C1)
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Principles Matrix (Vendored Stub)
|
||||||
|
|
||||||
|
Maps every domain P-rule back to the core C-rule(s) it derives from.
|
||||||
|
Full matrix in the live Atelier repo; this is a condensed vendored version
|
||||||
|
for the security domain (the primary domain the MCP server validates
|
||||||
|
against in v1.18).
|
||||||
|
|
||||||
|
| Domain | P-rule | Core C-rule(s) |
|
||||||
|
|---|---|---|
|
||||||
|
| security | P1 Boundary Validation | C1 Correctness |
|
||||||
|
| security | P2 Least Privilege | C1, C8 Economy |
|
||||||
|
| security | P3 Defense in Depth | C1 |
|
||||||
|
| security | P4 Secrets Never Exposed | C1, C7 Observability |
|
||||||
|
| security | P5 Authenticated by Default | C1 |
|
||||||
|
| security | P6 Encrypted in Transit and at Rest | C1 |
|
||||||
|
| security | P7 Auditable Actions | C1, C7 |
|
||||||
|
| security | P8 Patched Dependencies | C1, C8 |
|
||||||
|
| security | P9 Isolated Blast Radius | C1, C6 Composability |
|
||||||
|
| security | P10 Secure by Default | C1 |
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Agent Pre-Completion Checklist (Vendored)
|
||||||
|
|
||||||
|
Every AI agent runs this checklist before completing a task.
|
||||||
|
|
||||||
|
## Core Principles Checklist (C1–C8)
|
||||||
|
|
||||||
|
### C1 Correctness
|
||||||
|
- Does the code do what the task asked, completely?
|
||||||
|
- Does it handle the specified edge cases? (nulls, empties, max, min)
|
||||||
|
- Does it handle the failure cases? (errors, timeouts, invalid input)
|
||||||
|
- Is there a test that would fail if the code were wrong?
|
||||||
|
|
||||||
|
### C2 Clarity
|
||||||
|
- Can a stranger read this and understand it without asking you?
|
||||||
|
- Are names intent-revealing? (No `data`, `temp`, `x`, `doStuff`)
|
||||||
|
- Do comments explain *why*, not *what*?
|
||||||
|
|
||||||
|
### C3 Simplicity
|
||||||
|
- Is this the simplest solution that is complete?
|
||||||
|
- Is there dead code? (Unreachable branches, unused variables)
|
||||||
|
- Is there premature abstraction? (An interface with one implementation)
|
||||||
|
|
||||||
|
### C4 Locality
|
||||||
|
- Does related logic live together?
|
||||||
|
- Are side effects near their causes?
|
||||||
|
|
||||||
|
### C5 Reversibility
|
||||||
|
- Is this change undoable? (migration has a `down`, deploy has a rollback)
|
||||||
|
- Did I avoid irreversible actions without explicit confirmation?
|
||||||
|
|
||||||
|
### C6 Composability
|
||||||
|
- Does this component/function do one thing?
|
||||||
|
- Is the boundary (props/args/return) explicit and typed?
|
||||||
|
|
||||||
|
### C7 Observability
|
||||||
|
- Are there logs for significant events?
|
||||||
|
- Do errors carry enough context to debug? (request ID, user, action)
|
||||||
|
- Are there no secrets in logs?
|
||||||
|
|
||||||
|
### C8 Economy
|
||||||
|
- Is memory bounded? (No unbounded growth, no loading everything)
|
||||||
|
- Is time bounded? (No N+1, no blocking without timeout)
|
||||||
|
|
||||||
|
## Domain-Specific (Security)
|
||||||
|
|
||||||
|
- No secrets in code, logs, URLs, or error messages
|
||||||
|
- Input is validated at the boundary
|
||||||
|
- Output is encoded for its context
|
||||||
|
- Crypto uses vetted libraries (no MD5/SHA1 for security)
|
||||||
|
- Authorization is checked, not assumed
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nova.dev/schemas/submission-readiness.schema.json",
|
||||||
|
"title": "Nova Submission-Readiness Gate",
|
||||||
|
"description": "Defines what is acceptable to start — a superset gate ABOVE contract.schema.json validity. The contract schema defines the SHAPE (id/name/environment/infrastructure); this schema defines the READINESS gate: required Nova tags, per-env mandatory metadata (W3.E), declared policy preconditions, profile:agentic markers, and the appSource pointer. The validator (core/submission_readiness.py, REQ-218) calls contract.schema.json validation first, then these readiness checks. On fail → citizen-developer-facing error (not a stack trace); on pass → proceeds to existing contract ingestion.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["contractId", "environment", "tags", "policyPreconditions", "profile", "appSource"],
|
||||||
|
"properties": {
|
||||||
|
"contractId": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "The contract identifier (UUID or operational id). Non-empty."
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["dev", "qa", "prod", "dr"],
|
||||||
|
"description": "Target environment. Determines the per-env mandatory fields (allOf below)."
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The 5 required Nova tags (D-054). References schemas/tagging-standard.json.",
|
||||||
|
"required": ["nova:owner", "nova:contract", "nova:environment", "nova:cost-center", "nova:ref"],
|
||||||
|
"properties": {
|
||||||
|
"nova:owner": {"type": "string", "minLength": 1},
|
||||||
|
"nova:contract": {"type": "string", "minLength": 1},
|
||||||
|
"nova:environment": {"type": "string", "enum": ["dev", "qa", "prod", "dr"]},
|
||||||
|
"nova:cost-center": {"type": "string", "minLength": 1},
|
||||||
|
"nova:ref": {"type": "string", "minLength": 1}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"policyPreconditions": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Declared policy expectations the platform will enforce. The citizen developer states what the platform should check; the platform enforces it at apply time. Missing a declared precondition is POLICY_PRECONDITION_MISSING.",
|
||||||
|
"properties": {
|
||||||
|
"public-ingress": {"type": "boolean", "default": false},
|
||||||
|
"encryption_enabled": {"type": "boolean", "default": true},
|
||||||
|
"deletion_protection": {"type": "boolean", "default": true}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["developer", "agentic"],
|
||||||
|
"description": "developer = L3A (technical); agentic = L3B (non-technical, requires naturalLanguageIntent + confidenceAtSubmission + agentTrace per REQ-22 / W3.E)."
|
||||||
|
},
|
||||||
|
"appSource": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Pointer to the consumer application code so the platform can fetch at run time.",
|
||||||
|
"required": ["repo", "ref"],
|
||||||
|
"properties": {
|
||||||
|
"repo": {"type": "string", "minLength": 1, "description": "Repository URL or owner/repo shorthand."},
|
||||||
|
"ref": {"type": "string", "minLength": 1, "description": "Git ref (branch, tag, or SHA)."}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"naturalLanguageIntent": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Required when profile=agentic (L3B). The citizen developer's plain-language intent."
|
||||||
|
},
|
||||||
|
"confidenceAtSubmission": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1,
|
||||||
|
"description": "Required when profile=agentic (L3B). The submitter's self-assessed confidence."
|
||||||
|
},
|
||||||
|
"agentTrace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Required when profile=agentic (L3B). The agent's trace/reasoning for the submission."
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Per-env mandatory metadata (W3.E). qa requires e2eSuite + loadTest; prod requires runbook + dashboard + oncall; dr requires drDrillRef.",
|
||||||
|
"properties": {
|
||||||
|
"e2eSuite": {"type": "boolean"},
|
||||||
|
"loadTest": {"type": "boolean"}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"runbook": {"type": "string", "description": "Required when environment=prod (W3.E)."},
|
||||||
|
"dashboard": {"type": "string", "description": "Required when environment=prod (W3.E)."},
|
||||||
|
"oncall": {"type": "string", "description": "Required when environment=prod (W3.E)."},
|
||||||
|
"drDrillRef": {"type": "string", "description": "Required when environment=dr (W3.E)."}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {"properties": {"environment": {"const": "qa"}}},
|
||||||
|
"then": {"required": ["validation"], "properties": {"validation": {"required": ["e2eSuite", "loadTest"]}}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {"properties": {"environment": {"const": "prod"}}},
|
||||||
|
"then": {"required": ["runbook", "dashboard", "oncall"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {"properties": {"environment": {"const": "dr"}}},
|
||||||
|
"then": {"required": ["drDrillRef"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {"properties": {"profile": {"const": "agentic"}}},
|
||||||
|
"then": {"required": ["naturalLanguageIntent", "confidenceAtSubmission", "agentTrace"]}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""scripts/attach_release_asset.py — upload a file as a Gitea release attachment.
|
||||||
|
|
||||||
|
REQ-228 (v1.18): PPTX (and any deck artifact) is attached to the phase's
|
||||||
|
Gitea release. Uses the Gitea API:
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/releases/{id}/assets
|
||||||
|
multipart form: name=<filename>, attachment=<file bytes>
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/attach_release_asset.py <file-path> <release-id>
|
||||||
|
python3 scripts/attach_release_asset.py docs/presentations/nova-no-humans-platform.pptx 522
|
||||||
|
|
||||||
|
Token resolution: reads NOVA_GITEA_TOKEN (or ACDL_GITEA_TOKEN) from .env.secrets
|
||||||
|
/ .env, matching the ship_phase.sh pattern. Never uses shell env tokens.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
GITEA_BASE = "https://git.cloudinit.dev"
|
||||||
|
OWNER = "continuous-intelligence"
|
||||||
|
REPO = "acdl"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_token() -> str:
|
||||||
|
for fn in (".env.secrets", ".env"):
|
||||||
|
try:
|
||||||
|
for line in Path(fn).read_text().splitlines():
|
||||||
|
if line.startswith("NOVA_GITEA_TOKEN=") or line.startswith("ACDL_GITEA_TOKEN="):
|
||||||
|
return line.split("=", 1)[1].strip()
|
||||||
|
except (FileNotFoundError, PermissionError):
|
||||||
|
continue
|
||||||
|
raise RuntimeError("No Gitea token found in .env.secrets or .env (NOVA_GITEA_TOKEN/ACDL_GITEA_TOKEN)")
|
||||||
|
|
||||||
|
|
||||||
|
def attach_asset(file_path: str, release_id: str) -> dict:
|
||||||
|
token = resolve_token()
|
||||||
|
p = Path(file_path)
|
||||||
|
if not p.is_file():
|
||||||
|
raise FileNotFoundError(f"Asset file not found: {file_path}")
|
||||||
|
|
||||||
|
url = f"{GITEA_BASE}/api/v1/repos/{OWNER}/{REPO}/releases/{release_id}/assets"
|
||||||
|
filename = p.name
|
||||||
|
|
||||||
|
boundary = "----NovaBoundary7MAgYbk"
|
||||||
|
body = (
|
||||||
|
f"--{boundary}\r\n"
|
||||||
|
f'Content-Disposition: form-data; name="name"\r\n\r\n'
|
||||||
|
f"{filename}\r\n"
|
||||||
|
f"--{boundary}\r\n"
|
||||||
|
f'Content-Disposition: form-data; name="attachment"; filename="{filename}"\r\n'
|
||||||
|
f"Content-Type: application/octet-stream\r\n\r\n"
|
||||||
|
).encode() + p.read_bytes() + f"\r\n--{boundary}--\r\n".encode()
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=body,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = urllib.request.urlopen(req, timeout=60)
|
||||||
|
return json.loads(resp.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
err = e.read().decode()[:300]
|
||||||
|
raise RuntimeError(f"HTTP {e.code} attaching {filename} to release {release_id}: {err}") from e
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: attach_release_asset.py <file-path> <release-id>")
|
||||||
|
sys.exit(1)
|
||||||
|
result = attach_asset(sys.argv[1], sys.argv[2])
|
||||||
|
print(f"Attached: {result.get('name')} → release {sys.argv[2]} (asset id {result.get('id')})")
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/render_deck.sh — render a Marp deck to HTML + PPTX, commit both to git.
|
||||||
|
# REQ-228 (v1.18): PPTX is now a first-class committed artifact + release attachment.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/render_deck.sh <deck-name>
|
||||||
|
# bash scripts/render_deck.sh nova-no-humans-platform
|
||||||
|
#
|
||||||
|
# Renders:
|
||||||
|
# docs/presentations/<deck-name>-marp.md → docs/presentations/<deck-name>.html (committed)
|
||||||
|
# → docs/presentations/<deck-name>.pptx (committed, binary)
|
||||||
|
#
|
||||||
|
# The PPTX is also attached to the current phase's Gitea release via
|
||||||
|
# scripts/attach_release_asset.py (call separately after ship, or this script
|
||||||
|
# will invoke it if NOVA_GITEA_RELEASE_ID is set).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DECK="${1:?Usage: render_deck.sh <deck-name>}"
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
SRC="docs/presentations/${DECK}-marp.md"
|
||||||
|
HTML="docs/presentations/${DECK}.html"
|
||||||
|
PPTX="docs/presentations/${DECK}.pptx"
|
||||||
|
|
||||||
|
if [ ! -f "$SRC" ]; then
|
||||||
|
echo "ERROR: source deck $SRC not found" >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHROME=""
|
||||||
|
for c in \
|
||||||
|
/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
|
||||||
|
/usr/bin/chromium \
|
||||||
|
/usr/bin/chromium-browser \
|
||||||
|
/usr/bin/google-chrome; do
|
||||||
|
if [ -x "$c" ]; then CHROME="$c"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$CHROME" ]; then
|
||||||
|
echo "WARNING: no Chrome/Chromium found — skipping render (HTML/PPTX will need manual re-render)" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
export CHROME_PATH="$CHROME"
|
||||||
|
|
||||||
|
echo "Rendering HTML → $HTML"
|
||||||
|
npx --yes @marp-team/marp-cli@latest --allow-local-files "$SRC" -o "$HTML" 2>&1 | tail -3
|
||||||
|
|
||||||
|
echo "Rendering PPTX → $PPTX"
|
||||||
|
npx --yes @marp-team/marp-cli@latest --allow-local-files "$SRC" -o "$PPTX" 2>&1 | tail -3
|
||||||
|
|
||||||
|
git add "$HTML" "$PPTX"
|
||||||
|
echo "Staged $HTML + $PPTX for commit."
|
||||||
|
|
||||||
|
if [ -n "${NOVA_GITEA_RELEASE_ID:-}" ]; then
|
||||||
|
echo "Attaching PPTX to Gitea release $NOVA_GITEA_RELEASE_ID..."
|
||||||
|
python3 scripts/attach_release_asset.py "$PPTX" "$NOVA_GITEA_RELEASE_ID" || \
|
||||||
|
echo "WARNING: attach failed — PPTX is still committed; attach manually."
|
||||||
|
fi
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/update_atelier_vendor.sh — intentionally upgrade the vendored Atelier snapshot.
|
||||||
|
# Usage: bash scripts/update_atelier_vendor.sh <new-tag>
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${1:?Usage: update_atelier_vendor.sh <new-tag>}"
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
VENDOR_DIR="mcp/atelier/vendor"
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
echo "Fetching Atelier at tag ${TAG}..."
|
||||||
|
git clone --depth 1 --branch "${TAG}" https://git.cloudinit.dev/coreci/atelier.git "${TEMP_DIR}/atelier" 2>&1 | tail -3
|
||||||
|
|
||||||
|
echo "Replacing vendored snapshot..."
|
||||||
|
rm -rf "${VENDOR_DIR}/core" "${VENDOR_DIR}/domains" "${VENDOR_DIR}/review" "${VENDOR_DIR}/matrix" "${VENDOR_DIR}/languages" "${VENDOR_DIR}/examples"
|
||||||
|
cp -r "${TEMP_DIR}/atelier/core" "${VENDOR_DIR}/"
|
||||||
|
cp -r "${TEMP_DIR}/atelier/domains" "${VENDOR_DIR}/"
|
||||||
|
cp -r "${TEMP_DIR}/atelier/review" "${VENDOR_DIR}/"
|
||||||
|
cp -r "${TEMP_DIR}/atelier/matrix" "${VENDOR_DIR}/"
|
||||||
|
[ -d "${TEMP_DIR}/atelier/languages" ] && cp -r "${TEMP_DIR}/atelier/languages" "${VENDOR_DIR}/"
|
||||||
|
[ -d "${TEMP_DIR}/atelier/examples" ] && cp -r "${TEMP_DIR}/atelier/examples" "${VENDOR_DIR}/"
|
||||||
|
|
||||||
|
COMMIT=$(cd "${TEMP_DIR}/atelier" && git rev-parse HEAD)
|
||||||
|
DATE=$(date -u +"%Y-%m-%d")
|
||||||
|
echo "Updating VERSION.md..."
|
||||||
|
cat > "${VENDOR_DIR}/VERSION.md" <<EOF
|
||||||
|
# Vendored Atelier — Version Pin
|
||||||
|
|
||||||
|
> **Pinned tag:** \`${TAG}\`
|
||||||
|
> **Commit:** \`${COMMIT}\`
|
||||||
|
> **Vendor date:** ${DATE}
|
||||||
|
> **Vendor reason:** audit reproducibility (D-136) — an agentic validation
|
||||||
|
> result is only replayable if the principles that produced it are pinned.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
To bump the vendored Atelier to a new tag:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
bash scripts/update_atelier_vendor.sh <new-tag>
|
||||||
|
\`\`\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rm -rf "${TEMP_DIR}"
|
||||||
|
echo "Vendored Atelier updated to ${TAG}. Review the diff and commit."
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Skill: API Design
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/api/` (first-principles + rest, graphql,
|
||||||
|
> versioning, error-responses, pagination)
|
||||||
|
> **Core principles:** C1 Correctness, C2 Clarity, C6 Composability
|
||||||
|
> **BA.A mapping:** web API skill
|
||||||
|
> **Consumer:** read this before authoring an API service contract.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Endpoints are nouns, plural, lowercase-hyphenated.** (`/customers`,
|
||||||
|
not `/getCustomer`)
|
||||||
|
- **Status codes are correct.** 200/201/204/4xx/5xx per semantics.
|
||||||
|
- **Errors are structured.** Every error response carries `code`,
|
||||||
|
`message`, `request_id` — not a stack trace.
|
||||||
|
- **Input is validated against a schema.** The contract's
|
||||||
|
`infrastructure` map is validated at resolution time; the API must
|
||||||
|
validate its own request bodies.
|
||||||
|
- **Auth is required by default.** No unauthenticated endpoints unless
|
||||||
|
explicitly declared in `policyPreconditions`.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers
|
||||||
|
|
||||||
|
Before completing an API task, run these (from
|
||||||
|
`review/agent-checklist.md` § API):
|
||||||
|
|
||||||
|
- Endpoints are nouns, plural, lowercase-hyphenated
|
||||||
|
- Status codes are correct per semantics
|
||||||
|
- Errors are structured (code, message, request_id)
|
||||||
|
- Input is validated against a schema
|
||||||
|
- Auth is required by default
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
The submission-readiness gate (`schemas/submission-readiness.schema.json`)
|
||||||
|
checks that your contract declares `policyPreconditions`. The API skill
|
||||||
|
tells you what the platform expects your application to enforce on its
|
||||||
|
own surface (request validation, structured errors, auth). Nova does
|
||||||
|
not author your API; it deploys it. The API skill ensures the
|
||||||
|
application you deploy meets production-grade standards.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Skill: Compliance
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/compliance/` (first-principles + audit-logs,
|
||||||
|
> data-retention, policy-as-code, evidence)
|
||||||
|
> **Core principles:** C1 Correctness, C5 Reversibility
|
||||||
|
> **BA.A mapping:** cross-cutting (all 5 skills)
|
||||||
|
> **Consumer:** read this before any regulated-environment submission.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Audit records are immutable once written.** Deletion/mutation is
|
||||||
|
itself an auditable incident. Nova's Decision Ledger (SQLite
|
||||||
|
hash-chain, v1.17; S3 Object Lock + JWS future) enforces this.
|
||||||
|
- **The set of auditable actions is defined a priori.** "We forgot to log
|
||||||
|
it" is a violation. The submission-readiness gate's
|
||||||
|
`policyPreconditions` declare what the platform will audit.
|
||||||
|
- **Policy violations block before the action.** Checkov runs pre-apply;
|
||||||
|
the confidence signal gates; the HITL gate stops. Compliance is
|
||||||
|
admission-time, not audit-time.
|
||||||
|
- **Evidence gathered as a byproduct of operation.** Not assembled
|
||||||
|
manually at audit time. Every pipeline run emits events into the
|
||||||
|
Decision Ledger + the evidence stream.
|
||||||
|
- **Every logged action traces to an authenticated principal.** No
|
||||||
|
shared/generic identities. The HITL approver identity (D-042) is
|
||||||
|
recorded with every prod/dr promotion.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Compliance)
|
||||||
|
|
||||||
|
- Audit records are immutable once written; deletion/mutation is itself
|
||||||
|
auditable (P1)
|
||||||
|
- The set of auditable actions is defined a priori (P2)
|
||||||
|
- Policy violations block before the action (admission/CI/CD-time) (P5)
|
||||||
|
- Evidence gathered as a byproduct of operation, not assembled manually
|
||||||
|
(P6)
|
||||||
|
- Every logged action traces to an authenticated principal; no
|
||||||
|
shared/generic identities (P7)
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
Nova's compliance posture is framework-agnostic (D-024 in Atelier; the
|
||||||
|
platform lists GDPR, SOX, SOC2, DORA — not any single framework). The
|
||||||
|
compliance skill tells you what the platform enforces (immutable audit,
|
||||||
|
pre-apply policy, evidence byproduct, authenticated principals) and what
|
||||||
|
your application must enforce (the same standards on its own surface).
|
||||||
|
The submission-readiness gate ensures your contract declares
|
||||||
|
`policyPreconditions`; the compliance skill ensures your application
|
||||||
|
respects them. This is the RACI compliance-standard equivalence made
|
||||||
|
concrete: regardless of upstream source (AI agent, SDLC, dev platform),
|
||||||
|
the same compliance standards apply to every submission.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Skill: Data
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/data/` (first-principles + schema-design,
|
||||||
|
> migrations, indexing)
|
||||||
|
> **Core principles:** C1 Correctness, C4 Locality, C6 Composability
|
||||||
|
> **BA.A mapping:** web API, worker, scheduled job
|
||||||
|
> **Consumer:** read this before authoring a service with a database.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Schema reflects the domain, not the application.** Tables model
|
||||||
|
real-world entities, not ORM classes.
|
||||||
|
- **Constraints are in the schema.** NOT NULL, UNIQUE, FK — the database
|
||||||
|
enforces integrity, not the application.
|
||||||
|
- **Migration has an `up` and a `down`.** Every migration is reversible.
|
||||||
|
- **Types are domain-accurate.** UUID for IDs, TIMESTAMPTZ for timestamps,
|
||||||
|
DECIMAL for money — not string/integer/everything.
|
||||||
|
- **No `SELECT *`; no N+1.** Explicit columns; eager-load relations.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Data)
|
||||||
|
|
||||||
|
- Schema reflects the domain (not the application)
|
||||||
|
- Constraints are in the schema (NOT NULL, UNIQUE, FK)
|
||||||
|
- Migration has an `up` and a `down`
|
||||||
|
- Types are domain-accurate (UUID, TIMESTAMPTZ, DECIMAL for money)
|
||||||
|
- No `SELECT *`; no N+1
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
Nova deploys your infrastructure (RDS, DynamoDB) but does not author your
|
||||||
|
schema. The data skill ensures the schema you bring meets production-grade
|
||||||
|
standards. The submission-readiness gate checks that your contract declares
|
||||||
|
the infrastructure; the data skill checks that the application running on
|
||||||
|
that infrastructure uses the database correctly.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Skill: DevOps
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/devops/` (first-principles + ci-cd,
|
||||||
|
> environments)
|
||||||
|
> **Core principles:** C5 Reversibility, C7 Observability, C8 Economy
|
||||||
|
> **BA.A mapping:** scheduled job, worker
|
||||||
|
> **Consumer:** read this before any deployment.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **The pipeline is the process.** No manual steps. Every change flows
|
||||||
|
through the same pipeline: contract → resolver → plan → policy →
|
||||||
|
confidence → (HITL gate for qa/prod/dr) → apply → evidence.
|
||||||
|
- **Rollback path is known.** Every deployment has a documented rollback.
|
||||||
|
Terraform state is the rollback mechanism; the pipeline replans to the
|
||||||
|
prior state.
|
||||||
|
- **Config is in code, not on the server.** Environment variables, SSM
|
||||||
|
parameters, secrets — all declared, versioned, and reviewable. No
|
||||||
|
hand-configured server state.
|
||||||
|
- **Environments are parity.** dev = prod modulo data. The same contract
|
||||||
|
deploys to all environments; only the environment field changes.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ DevOps)
|
||||||
|
|
||||||
|
- The pipeline is the process (no manual steps)
|
||||||
|
- Rollback path is known
|
||||||
|
- Config is in code, not on the server
|
||||||
|
- Environments are parity (dev = prod modulo data)
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
Nova IS the pipeline. The citizen developer's contract declares intent;
|
||||||
|
Nova provides the process. The DevOps skill tells you what the platform
|
||||||
|
expects from your submission: no manual steps (everything flows through
|
||||||
|
the contract), a known rollback (Terraform state), config in code (SSM
|
||||||
|
SecureString, not hand-configured servers), and environment parity (one
|
||||||
|
contract, four environments).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Skill: Errors
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/errors/` (first-principles + patterns)
|
||||||
|
> **Core principles:** C1 Correctness, C7 Observability
|
||||||
|
> **BA.A mapping:** web API, worker, scheduled job
|
||||||
|
> **Consumer:** read this before authoring error handling.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Errors are not swallowed silently.** A bare `except: pass` is a bug.
|
||||||
|
Every caught error is either handled, re-raised, or logged with context.
|
||||||
|
- **Errors are specific.** Not `raise Exception("something went wrong")`
|
||||||
|
— a named exception with the what/where/why.
|
||||||
|
- **Errors preserve context.** The error carries the request ID, the
|
||||||
|
user, the action — enough to debug without reproducing.
|
||||||
|
- **Recovery is attempted when possible; fail fast when not.** Retry
|
||||||
|
transient errors with backoff; fail fast on invariant violations.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Errors)
|
||||||
|
|
||||||
|
- Errors are not swallowed silently
|
||||||
|
- Errors are specific (not generic "something went wrong")
|
||||||
|
- Errors preserve context (where, when, why, what)
|
||||||
|
- Recovery is attempted when possible; fail fast when not
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
Nova's confidence signal (D-040) uses error events as one of its 6 inputs.
|
||||||
|
The error skill ensures your application's errors are structured enough to
|
||||||
|
feed the signal: specific error types, preserved context, no silent
|
||||||
|
swallows. The platform's `report_error` Lambda action (D-055) creates a
|
||||||
|
GitHub issue on the platform repo when the pipeline fails — your
|
||||||
|
application errors should be structured enough to flow through the same
|
||||||
|
path.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Skill: Infrastructure as Code
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/infrastructure-as-code/` (first-principles +
|
||||||
|
> terraform, opentofu, state, modules)
|
||||||
|
> **Core principles:** C1 Correctness, C5 Reversibility, C8 Economy
|
||||||
|
> **BA.A mapping:** static asset
|
||||||
|
> **Consumer:** read this before authoring a contract that declares
|
||||||
|
> infrastructure.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Configuration is declarative, not scripted.** The contract declares
|
||||||
|
what; Terraform reconciles how. No imperative scripts in the contract.
|
||||||
|
- **Provider versions are pinned, never `latest`.** The contract's
|
||||||
|
infrastructure map may pin module versions (semver); the platform pins
|
||||||
|
provider versions.
|
||||||
|
- **State is remote with locking; never committed.** Nova manages state
|
||||||
|
in S3 + DynamoDB; the citizen developer never touches state files.
|
||||||
|
- **`plan` is reviewed before every `apply`.** The confidence signal
|
||||||
|
gates the apply; the HITL gate (qa/prod/dr) requires human attestation
|
||||||
|
before the apply proceeds.
|
||||||
|
- **No secrets in HCL; secrets via providers/stores.** Secrets live in
|
||||||
|
SSM SecureString / Secrets Manager, not in the contract or HCL.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Infrastructure as Code)
|
||||||
|
|
||||||
|
- Configuration is declarative, not scripted (P1)
|
||||||
|
- Provider versions are pinned, never `latest` (P5)
|
||||||
|
- State is remote with locking; never committed (P3, P8)
|
||||||
|
- `plan` is reviewed before every `apply` (P4)
|
||||||
|
- No secrets in HCL; secrets via providers/stores (P10)
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
Nova IS the infrastructure-as-code platform. The citizen developer
|
||||||
|
declares intent in the contract; Nova's adapter (stateless assembler,
|
||||||
|
v1.11) translates to Terraform modules; the pipeline runs plan → policy →
|
||||||
|
confidence → (HITL) → apply. The IaC skill tells you what the platform
|
||||||
|
expects from your contract: declarative inputs (not scripts), pinned
|
||||||
|
versions (not `latest`), no secrets in the contract (secrets via SSM),
|
||||||
|
and acceptance that the platform owns state + the apply path.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Skill: Observability
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/observability/` (first-principles + logging,
|
||||||
|
> metrics, tracing)
|
||||||
|
> **Core principles:** C7 Observability
|
||||||
|
> **BA.A mapping:** basic observability bootstrap
|
||||||
|
> **Consumer:** read this before any production submission (W3.E requires
|
||||||
|
> `dashboard` + `oncall` for prod).
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Logs are structured.** JSON with fields, not free-form text. Every
|
||||||
|
log line carries a timestamp, level, message, and context fields.
|
||||||
|
- **Every request has a correlation ID.** A request ID propagates from
|
||||||
|
ingress through every downstream call. Logs, metrics, and traces share
|
||||||
|
the same ID.
|
||||||
|
- **No high-cardinality labels in metrics.** User IDs, request IDs, and
|
||||||
|
other unbounded values go in logs/traces, not metric labels.
|
||||||
|
- **Alerts have runbooks.** Every alert links to a runbook
|
||||||
|
(`runbook` field in the submission, W3.E prod mandatory) that explains
|
||||||
|
what to do when it fires.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Observability)
|
||||||
|
|
||||||
|
- Logs are structured (JSON, fields)
|
||||||
|
- Every request has a correlation ID
|
||||||
|
- No high-cardinality labels in metrics
|
||||||
|
- Alerts have runbooks
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
The W3.E per-env mandatory table requires `runbook` + `dashboard` +
|
||||||
|
`oncall` for `prod` submissions — the submission-readiness gate enforces
|
||||||
|
this. The observability skill tells you what those artifacts must contain:
|
||||||
|
structured logs, correlation IDs, bounded metric labels, and runbook-linked
|
||||||
|
alerts. Nova provides the infrastructure (CloudWatch, the uptime
|
||||||
|
monitor); you provide the application-level observability (structured
|
||||||
|
logs, dashboards, runbooks).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Skill: Security
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/security/` (first-principles +
|
||||||
|
> authentication, authorization, input-validation, secrets, supply-chain)
|
||||||
|
> **Core principles:** C1 Correctness (security is correctness)
|
||||||
|
> **BA.A mapping:** cross-cutting (all 5 skills)
|
||||||
|
> **Consumer:** read this before any production submission.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **No secrets in code, logs, URLs, or error messages.** Secrets live in
|
||||||
|
the platform's secret store (SSM SecureString, Secrets Manager), not
|
||||||
|
your application repo.
|
||||||
|
- **Input is validated at the boundary.** Every external input (HTTP
|
||||||
|
body, query, header, file) is validated against a schema before
|
||||||
|
processing.
|
||||||
|
- **Output is encoded for its context.** HTML escaping, URL encoding,
|
||||||
|
SQL parameterization — context-appropriate, not a blanket escape.
|
||||||
|
- **Crypto uses vetted libraries.** No MD5/SHA1 for security. Use
|
||||||
|
bcrypt/argon2 for passwords, AES-GCM for encryption.
|
||||||
|
- **Authorization is checked, not assumed.** Every request verifies the
|
||||||
|
caller's authority to perform the action.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Security)
|
||||||
|
|
||||||
|
- No secrets in code, logs, URLs, or error messages
|
||||||
|
- Input is validated at the boundary
|
||||||
|
- Output is encoded for its context
|
||||||
|
- Crypto uses vetted libraries (no MD5/SHA1 for security)
|
||||||
|
- Authorization is checked, not assumed
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
The submission-readiness gate checks `policyPreconditions` (e.g.,
|
||||||
|
`public-ingress: false`, `encryption_enabled: true`). The security skill
|
||||||
|
tells you what the platform enforces and what your application must
|
||||||
|
enforce on its own surface. The platform enforces infrastructure-level
|
||||||
|
security (IAM scoping, ABAC, encryption-at-rest, policy-as-code via
|
||||||
|
Checkov); you enforce application-level security (input validation, output
|
||||||
|
encoding, auth checks). The Atelier MCP server (`mcp/atelier/server.py`,
|
||||||
|
P5) can validate your code against these principles agenticly — beyond
|
||||||
|
what deterministic scanners like Wiz/Checkmarx/Mend catch.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Skill: Testing
|
||||||
|
|
||||||
|
> **Atelier source:** `domains/testing/` (first-principles + pyramid,
|
||||||
|
> fixtures)
|
||||||
|
> **Core principles:** C1 Correctness, C5 Reversibility
|
||||||
|
> **BA.A mapping:** UAT is the citizen developer's RACI responsibility
|
||||||
|
> **Consumer:** read this before submitting for UAT.
|
||||||
|
|
||||||
|
## First Principles (citizen-developer-relevant subset)
|
||||||
|
|
||||||
|
- **Tests are independent.** Order doesn't matter; one test's setup
|
||||||
|
doesn't break another's.
|
||||||
|
- **Tests are deterministic.** No `Date.now()`, no `random()`, no
|
||||||
|
network calls in unit tests.
|
||||||
|
- **Edge cases are covered.** Empty, single, max, invalid — not just
|
||||||
|
the happy path.
|
||||||
|
- **A failing test names the problem.** The assertion message explains
|
||||||
|
what failed and why, not just "assertion failed".
|
||||||
|
- **The pyramid: unit → integration → e2e.** Most tests are unit; few
|
||||||
|
are e2e; the middle is integration. Don't invert the pyramid.
|
||||||
|
|
||||||
|
## Agent-Checklist Triggers (§ Testing)
|
||||||
|
|
||||||
|
- Tests are independent (order doesn't matter)
|
||||||
|
- Tests are deterministic (no `Date.now()`, no `random()`)
|
||||||
|
- Edge cases are covered (empty, single, max, invalid)
|
||||||
|
- A failing test names the problem specifically
|
||||||
|
|
||||||
|
## How Nova Uses This
|
||||||
|
|
||||||
|
Per the RACI matrix (`docs/raci.md`), the **Citizen Developer is
|
||||||
|
Responsible for User Acceptance Testing (UAT)**. The platform provides
|
||||||
|
the QA checks (policy, confidence, schema); you provide the UAT. The
|
||||||
|
testing skill ensures your UAT meets production-grade standards. The
|
||||||
|
W3.E per-env mandatory table requires `validation.e2eSuite` +
|
||||||
|
`validation.loadTest` for `qa` environment submissions — the
|
||||||
|
submission-readiness gate enforces this.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"""tests/test_atelier_mcp.py — REQ-225.
|
||||||
|
|
||||||
|
Covers: tool registration (all 4 tools discoverable), lookup_principle
|
||||||
|
returns the principle text + core C-rule, validate_against_principles
|
||||||
|
catches a planted C1 (correctness) + C7 (observability) violation in a
|
||||||
|
known-bad snippet and passes a known-good snippet, matrix_lookup returns
|
||||||
|
the domain→core mapping, plugin discovery loads all plugins in plugins/.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from mcp.atelier.server import NovaAtelierServer
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluginDiscovery(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.server = NovaAtelierServer()
|
||||||
|
self.loaded = self.server.load_plugins()
|
||||||
|
|
||||||
|
def test_both_plugins_loaded(self):
|
||||||
|
self.assertIn("principles", self.loaded)
|
||||||
|
self.assertIn("validation", self.loaded)
|
||||||
|
|
||||||
|
def test_four_tools_registered(self):
|
||||||
|
tools = self.server.list_tools()
|
||||||
|
names = {t["name"] for t in tools}
|
||||||
|
self.assertIn("atelier_lookup_principle", names)
|
||||||
|
self.assertIn("atelier_list_domains", names)
|
||||||
|
self.assertIn("atelier_matrix_lookup", names)
|
||||||
|
self.assertIn("atelier_validate_against_principles", names)
|
||||||
|
self.assertEqual(len(names), 4)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLookupPrinciple(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.server = NovaAtelierServer()
|
||||||
|
self.server.load_plugins()
|
||||||
|
|
||||||
|
def test_lookup_security_p4(self):
|
||||||
|
result = self.server.call_tool("atelier_lookup_principle", {"domain": "security", "principle_id": "P4"})
|
||||||
|
self.assertNotIn("error", result)
|
||||||
|
self.assertEqual(result["domain"], "security")
|
||||||
|
self.assertEqual(result["principle_id"], "P4")
|
||||||
|
self.assertIn("Secrets", result["title"])
|
||||||
|
self.assertIn("C1", result["core_c_rule"])
|
||||||
|
self.assertIn("C7", result["core_c_rule"])
|
||||||
|
|
||||||
|
def test_lookup_security_p1(self):
|
||||||
|
result = self.server.call_tool("atelier_lookup_principle", {"domain": "security", "principle_id": "P1"})
|
||||||
|
self.assertNotIn("error", result)
|
||||||
|
self.assertIn("Boundary", result["title"])
|
||||||
|
|
||||||
|
def test_lookup_unknown_domain(self):
|
||||||
|
result = self.server.call_tool("atelier_lookup_principle", {"domain": "nonexistent", "principle_id": "P1"})
|
||||||
|
self.assertIn("error", result)
|
||||||
|
|
||||||
|
def test_lookup_unknown_principle(self):
|
||||||
|
result = self.server.call_tool("atelier_lookup_principle", {"domain": "security", "principle_id": "P99"})
|
||||||
|
self.assertIn("error", result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestListDomains(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.server = NovaAtelierServer()
|
||||||
|
self.server.load_plugins()
|
||||||
|
|
||||||
|
def test_returns_19_domains(self):
|
||||||
|
result = self.server.call_tool("atelier_list_domains", {})
|
||||||
|
self.assertEqual(len(result), 19)
|
||||||
|
|
||||||
|
def test_security_is_nova_relevant(self):
|
||||||
|
result = self.server.call_tool("atelier_list_domains", {})
|
||||||
|
sec = next(d for d in result if d["domain"] == "security")
|
||||||
|
self.assertTrue(sec["nova_relevant"])
|
||||||
|
|
||||||
|
def test_ui_ux_not_nova_relevant(self):
|
||||||
|
result = self.server.call_tool("atelier_list_domains", {})
|
||||||
|
ui = next(d for d in result if d["domain"] == "ui-ux")
|
||||||
|
self.assertFalse(ui["nova_relevant"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestMatrixLookup(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.server = NovaAtelierServer()
|
||||||
|
self.server.load_plugins()
|
||||||
|
|
||||||
|
def test_security_matrix(self):
|
||||||
|
result = self.server.call_tool("atelier_matrix_lookup", {"domain": "security"})
|
||||||
|
self.assertEqual(result["domain"], "security")
|
||||||
|
self.assertEqual(len(result["mapping"]), 10)
|
||||||
|
p4 = next(m for m in result["mapping"] if m["p"] == "P4")
|
||||||
|
self.assertIn("C1", p4["core"])
|
||||||
|
self.assertIn("C7", p4["core"])
|
||||||
|
|
||||||
|
def test_unknown_domain_matrix(self):
|
||||||
|
result = self.server.call_tool("atelier_matrix_lookup", {"domain": "nonexistent"})
|
||||||
|
self.assertEqual(result["mapping"], [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateAgainstPrinciples(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.server = NovaAtelierServer()
|
||||||
|
self.server.load_plugins()
|
||||||
|
|
||||||
|
def test_good_snippet_passes(self):
|
||||||
|
good = """
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_customer(customer_id, request_id):
|
||||||
|
if not customer_id:
|
||||||
|
raise ValueError("customer_id required")
|
||||||
|
logger.info("fetching customer %s (request %s)", customer_id, request_id)
|
||||||
|
return db.query(customer_id)
|
||||||
|
"""
|
||||||
|
result = self.server.call_tool("atelier_validate_against_principles", {"snippet": good})
|
||||||
|
# Should not have FAIL on secrets (no hardcoded secrets)
|
||||||
|
sec_checks = [r for r in result["results"] if r["check_id"].startswith("SEC.1")]
|
||||||
|
for c in sec_checks:
|
||||||
|
self.assertEqual(c["status"], "PASS", f"SEC.1 should PASS: {c}")
|
||||||
|
|
||||||
|
def test_bad_snippet_catches_secret(self):
|
||||||
|
bad = """
|
||||||
|
api_key = "sk-1234567890abcdef"
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
result = self.server.call_tool("atelier_validate_against_principles", {"snippet": bad})
|
||||||
|
# SEC.1 (secrets in code) should FAIL
|
||||||
|
sec1 = next(r for r in result["results"] if r["check_id"] == "SEC.1")
|
||||||
|
self.assertEqual(sec1["status"], "FAIL")
|
||||||
|
|
||||||
|
def test_bad_snippet_catches_swallowed_error(self):
|
||||||
|
bad = """
|
||||||
|
try:
|
||||||
|
do_something()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
result = self.server.call_tool("atelier_validate_against_principles", {"snippet": bad})
|
||||||
|
# C1.2 (handles failure cases) should FAIL because of `except: pass`
|
||||||
|
c12 = next(r for r in result["results"] if r["check_id"] == "C1.2")
|
||||||
|
self.assertEqual(c12["status"], "FAIL")
|
||||||
|
|
||||||
|
def test_bad_snippet_catches_obfuscated_names(self):
|
||||||
|
bad = """
|
||||||
|
def doStuff(data, temp, x):
|
||||||
|
return data + temp + x
|
||||||
|
"""
|
||||||
|
result = self.server.call_tool("atelier_validate_against_principles", {"snippet": bad})
|
||||||
|
# C2.1 (names intent-revealing) should FAIL
|
||||||
|
c21 = next(r for r in result["results"] if r["check_id"] == "C2.1")
|
||||||
|
self.assertEqual(c21["status"], "FAIL")
|
||||||
|
|
||||||
|
def test_result_structure(self):
|
||||||
|
result = self.server.call_tool("atelier_validate_against_principles", {"snippet": "x = 1"})
|
||||||
|
self.assertIn("overall", result)
|
||||||
|
self.assertIn("results", result)
|
||||||
|
self.assertIsInstance(result["results"], list)
|
||||||
|
self.assertGreater(len(result["results"]), 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""tests/test_submission_readiness.py — REQ-220.
|
||||||
|
|
||||||
|
Covers: good contract passes; missing tags fail with MISSING_TAGS;
|
||||||
|
env-missing-mandatory fails with ENV_MISSING_MANDATORY:<env>:<field>;
|
||||||
|
agentic profile missing intent fails with AGENTIC_MISSING_INTENT;
|
||||||
|
missing appSource fails with MISSING_APP_SOURCE.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from core.submission_readiness import check_readiness, ReadinessResult
|
||||||
|
|
||||||
|
GOOD_TAGS = {
|
||||||
|
"nova:owner": "consumer-repo",
|
||||||
|
"nova:contract": "uuid-1234",
|
||||||
|
"nova:environment": "dev",
|
||||||
|
"nova:cost-center": "nova-default",
|
||||||
|
"nova:ref": "CHG0678912",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _base(**overrides):
|
||||||
|
submission = {
|
||||||
|
"contractId": "uuid-1234",
|
||||||
|
"id": "webapi",
|
||||||
|
"name": "Customer Web API",
|
||||||
|
"environment": "dev",
|
||||||
|
"tags": dict(GOOD_TAGS),
|
||||||
|
"policyPreconditions": {"public-ingress": False, "encryption_enabled": True},
|
||||||
|
"profile": "developer",
|
||||||
|
"appSource": {"repo": "consumer/repo", "ref": "main"},
|
||||||
|
"infrastructure": {
|
||||||
|
"static-assets": {"inputs": {"bucket_name": "webapi-assets"}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
submission.update(overrides)
|
||||||
|
return submission
|
||||||
|
|
||||||
|
|
||||||
|
class TestGoodContract(unittest.TestCase):
|
||||||
|
def test_good_contract_passes(self):
|
||||||
|
result = check_readiness(_base())
|
||||||
|
self.assertTrue(result.ready, f"Expected ready, got: {result.reason_codes}")
|
||||||
|
self.assertEqual(result.contract_id, "uuid-1234")
|
||||||
|
|
||||||
|
def test_good_agentic_contract_passes(self):
|
||||||
|
submission = _base(
|
||||||
|
profile="agentic",
|
||||||
|
naturalLanguageIntent="A web API for customer data",
|
||||||
|
confidenceAtSubmission=0.85,
|
||||||
|
agentTrace="LLM generated contract from issue #42",
|
||||||
|
)
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertTrue(result.ready, f"Expected ready, got: {result.reason_codes}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMissingTags(unittest.TestCase):
|
||||||
|
def test_missing_tags_fail(self):
|
||||||
|
submission = _base()
|
||||||
|
submission["tags"] = {"nova:owner": "consumer-repo"}
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
codes = " ".join(result.reason_codes)
|
||||||
|
self.assertIn("MISSING_TAGS", codes)
|
||||||
|
self.assertIn("nova:contract", codes)
|
||||||
|
self.assertIn("nova:environment", codes)
|
||||||
|
self.assertIn("nova:cost-center", codes)
|
||||||
|
self.assertIn("nova:ref", codes)
|
||||||
|
|
||||||
|
def test_empty_tag_value_fails(self):
|
||||||
|
submission = _base()
|
||||||
|
submission["tags"]["nova:owner"] = ""
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
self.assertTrue(any("MISSING_TAGS" in c for c in result.reason_codes))
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvMissingMandatory(unittest.TestCase):
|
||||||
|
def test_qa_missing_e2e_suite_fails(self):
|
||||||
|
submission = _base(environment="qa")
|
||||||
|
submission["tags"]["nova:environment"] = "qa"
|
||||||
|
# No validation.e2eSuite
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
codes = " ".join(result.reason_codes)
|
||||||
|
self.assertIn("ENV_MISSING_MANDATORY:qa:validation.e2eSuite", codes)
|
||||||
|
|
||||||
|
def test_prod_missing_runbook_fails(self):
|
||||||
|
submission = _base(environment="prod")
|
||||||
|
submission["tags"]["nova:environment"] = "prod"
|
||||||
|
# No runbook/dashboard/oncall
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
codes = " ".join(result.reason_codes)
|
||||||
|
self.assertIn("ENV_MISSING_MANDATORY:prod:runbook", codes)
|
||||||
|
self.assertIn("ENV_MISSING_MANDATORY:prod:dashboard", codes)
|
||||||
|
self.assertIn("ENV_MISSING_MANDATORY:prod:oncall", codes)
|
||||||
|
|
||||||
|
def test_dr_missing_drdrillref_fails(self):
|
||||||
|
submission = _base(environment="dr")
|
||||||
|
submission["tags"]["nova:environment"] = "dr"
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
codes = " ".join(result.reason_codes)
|
||||||
|
self.assertIn("ENV_MISSING_MANDATORY:dr:drDrillRef", codes)
|
||||||
|
|
||||||
|
def test_prod_with_all_mandatory_passes(self):
|
||||||
|
submission = _base(
|
||||||
|
environment="prod",
|
||||||
|
runbook="docs/runbooks/webapi.md",
|
||||||
|
dashboard="https://grafana/nova/webapi",
|
||||||
|
oncall="oncall@company.com",
|
||||||
|
)
|
||||||
|
submission["tags"]["nova:environment"] = "prod"
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertTrue(result.ready, f"Expected ready, got: {result.reason_codes}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestAgenticMissingIntent(unittest.TestCase):
|
||||||
|
def test_agentic_missing_all_markers_fails(self):
|
||||||
|
submission = _base(profile="agentic")
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
codes = " ".join(result.reason_codes)
|
||||||
|
self.assertIn("AGENTIC_MISSING_INTENT:naturalLanguageIntent", codes)
|
||||||
|
self.assertIn("AGENTIC_MISSING_INTENT:confidenceAtSubmission", codes)
|
||||||
|
self.assertIn("AGENTIC_MISSING_INTENT:agentTrace", codes)
|
||||||
|
|
||||||
|
def test_agentic_missing_one_marker_fails(self):
|
||||||
|
submission = _base(
|
||||||
|
profile="agentic",
|
||||||
|
naturalLanguageIntent="A web API",
|
||||||
|
confidenceAtSubmission=0.85,
|
||||||
|
# agentTrace missing
|
||||||
|
)
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
self.assertTrue(any("agentTrace" in c for c in result.reason_codes))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMissingAppSource(unittest.TestCase):
|
||||||
|
def test_missing_appsource_fails(self):
|
||||||
|
submission = _base()
|
||||||
|
del submission["appSource"]
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
self.assertTrue(any("MISSING_APP_SOURCE" in c for c in result.reason_codes))
|
||||||
|
|
||||||
|
def test_appsource_missing_ref_fails(self):
|
||||||
|
submission = _base()
|
||||||
|
submission["appSource"] = {"repo": "consumer/repo"}
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
self.assertTrue(any("MISSING_APP_SOURCE" in c for c in result.reason_codes))
|
||||||
|
|
||||||
|
|
||||||
|
class TestPolicyPreconditionMissing(unittest.TestCase):
|
||||||
|
def test_empty_policy_fails(self):
|
||||||
|
submission = _base()
|
||||||
|
submission["policyPreconditions"] = {}
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertFalse(result.ready)
|
||||||
|
self.assertTrue(any("POLICY_PRECONDITION_MISSING" in c for c in result.reason_codes))
|
||||||
|
|
||||||
|
|
||||||
|
class TestReadinessResultStructure(unittest.TestCase):
|
||||||
|
def test_result_to_dict(self):
|
||||||
|
result = check_readiness(_base())
|
||||||
|
d = result.to_dict()
|
||||||
|
self.assertIn("ready", d)
|
||||||
|
self.assertIn("reason_codes", d)
|
||||||
|
self.assertIn("contractId", d)
|
||||||
|
|
||||||
|
def test_result_str_ready(self):
|
||||||
|
result = check_readiness(_base())
|
||||||
|
self.assertIn("READY", str(result))
|
||||||
|
|
||||||
|
def test_result_str_not_ready(self):
|
||||||
|
submission = _base()
|
||||||
|
del submission["appSource"]
|
||||||
|
result = check_readiness(submission)
|
||||||
|
self.assertIn("NOT READY", str(result))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user