feat: make Core reports commercially production-ready

This commit is contained in:
thanhnv
2026-07-29 13:12:11 +07:00
parent 67d482ae7f
commit 7f01cf980a
27 changed files with 1197 additions and 23 deletions
+19
View File
@@ -18,6 +18,12 @@ Mô hình production mặc định:
suite, test scripts, internal CI runners, `level5`, dashboard lab, source docs suite, test scripts, internal CI runners, `level5`, dashboard lab, source docs
hay release tooling. hay release tooling.
Ngay sau init, CASAN ghi hai projection do CASAN sở hữu:
`.casan/discovery.json` và `.casan/readiness.json`. Readiness tách ba chiều:
**Core**, **Domain Pipeline** và **Provider Telemetry**. Core có thể sẵn sàng
cho prompt/report dù hai chiều tùy chọn còn `not_configured` hoặc
`optional_unavailable`.
## Trạng thái sản phẩm ## Trạng thái sản phẩm
| Thành phần | Trạng thái | Phạm vi | | Thành phần | Trạng thái | Phạm vi |
@@ -57,6 +63,7 @@ không cần Node/npm hoặc Platform.
```bash ```bash
# Sau một prompt # Sau một prompt
casan report latest casan report latest
casan readiness --refresh # Core / Domain / Provider, không chạy pipeline
casan view # mở run mới nhất trong Core viewer casan view # mở run mới nhất trong Core viewer
casan report export --format html # run dossier, chỉ tạo khi được yêu cầu casan report export --format html # run dossier, chỉ tạo khi được yêu cầu
casan report export --h6 --format html # H6 dossier on-demand casan report export --h6 --format html # H6 dossier on-demand
@@ -154,6 +161,7 @@ Project đã init không bị hỏi lại runtime: CASAN giữ nguyên mode hi
```bash ```bash
casan doctor casan doctor
casan readiness --refresh
casan verify-harness casan verify-harness
casan edition show casan edition show
``` ```
@@ -161,6 +169,11 @@ casan edition show
Với Codex, mở `/hooks`, review và trust đúng project hook sau lần init hoặc khi Với Codex, mở `/hooks`, review và trust đúng project hook sau lần init hoặc khi
bootstrap hash thay đổi. bootstrap hash thay đổi.
`casan readiness` là product status dành cho người vận hành và dashboard;
`casan doctor` là diagnostic sâu cho integrity, hook, smoke test và trust.
Không dùng trạng thái thiếu Domain Pack hoặc thiếu token/cost provider để hạ
Core thành failed.
Codex hooks gọi bootstrap tương đối từ project root và không phụ thuộc vào Codex hooks gọi bootstrap tương đối từ project root và không phụ thuộc vào
`git rev-parse`, nên ownership hoặc cấu hình Git không thể làm hỏng lifecycle `git rev-parse`, nên ownership hoặc cấu hình Git không thể làm hỏng lifecycle
hook. Git vẫn được khuyến nghị mạnh cho source provenance, review diff và hook. Git vẫn được khuyến nghị mạnh cho source provenance, review diff và
@@ -383,6 +396,9 @@ installation hoặc project runtime.
│ ├── version.lock │ ├── version.lock
│ ├── casan-hook.py │ ├── casan-hook.py
│ ├── agentic.env │ ├── agentic.env
│ ├── discovery.json
│ ├── readiness.json
│ ├── domain.json # chỉ khi chọn manifest bằng casan domain configure
│ ├── init-manifest.json │ ├── init-manifest.json
│ └── runtime/casan-core/ # chỉ khi --runtime vendored │ └── runtime/casan-core/ # chỉ khi --runtime vendored
├── .specify/ ├── .specify/
@@ -403,6 +419,9 @@ installation hoặc project runtime.
| `.casan/version.lock` | Pin version và SHA-256 của Core runtime đã resolve | | `.casan/version.lock` | Pin version và SHA-256 của Core runtime đã resolve |
| `.casan/casan-hook.py` | Stdlib bootstrap, verify pin rồi dispatch adapter | | `.casan/casan-hook.py` | Stdlib bootstrap, verify pin rồi dispatch adapter |
| `.casan/agentic.env` | Compatibility/reference flags; runtime đọc `config.json` | | `.casan/agentic.env` | Compatibility/reference flags; runtime đọc `config.json` |
| `.casan/discovery.json` | Inventory bounded các marker/source/requirements/Domain Pack candidate; không sửa source |
| `.casan/readiness.json` | Contract Core / Domain Pipeline / Provider Telemetry dùng chung cho CLI và viewer |
| `.casan/domain.json` | Reference CASAN-owned tới manifest do project sở hữu; chỉ tạo khi `casan domain configure` |
| `.casan/init-manifest.json` | Danh sách file CASAN quản lý, checksum và backup | | `.casan/init-manifest.json` | Danh sách file CASAN quản lý, checksum và backup |
| `.casan/runtime/casan-core/` | Core production-only; chỉ có ở Vendored | | `.casan/runtime/casan-core/` | Core production-only; chỉ có ở Vendored |
| `.specify/logs`, `.specify/state` | Trace, audit và state runtime; không commit | | `.specify/logs`, `.specify/state` | Trace, audit và state runtime; không commit |
+22 -2
View File
@@ -76,6 +76,8 @@ Commands:
init [--runtime managed|vendored] Adopt/reconfigure CASAN (interactive wizard by default) init [--runtime managed|vendored] Adopt/reconfigure CASAN (interactive wizard by default)
uninstall [--purge] Remove CASAN from this project (preserves user config) uninstall [--purge] Remove CASAN from this project (preserves user config)
doctor [--client ...] Verify configured hooks, pin, adapters, and VS Code route doctor [--client ...] Verify configured hooks, pin, adapters, and VS Code route
readiness [--refresh] [--json] Assess Core, Domain Pipeline, and Provider Telemetry
domain <status|discover|configure> Manage optional project-owned Domain Pack selection
edition <show|set> Show / change the product edition edition <show|set> Show / change the product edition
level <show|set 1..4> Deprecated alias for edition level <show|set 1..4> Deprecated alias for edition
verify-harness Verify the resolved harness matches the project pin verify-harness Verify the resolved harness matches the project pin
@@ -90,7 +92,7 @@ Commands:
report latest [--json] Show the latest prompt assurance receipt report latest [--json] Show the latest prompt assurance receipt
report export [trace] [--format] Export a run/H6 dossier on demand report export [trace] [--format] Export a run/H6 dossier on demand
view [trace-id] Open the Core Local Assurance Viewer view [trace-id] Open the Core Local Assurance Viewer
pipeline [--manifest path] Run the manifest-driven SRS→test pipeline pipeline [--manifest path] Run an optional project-owned SRS→test pipeline
dashboard <start|status|stop|open> Run local assurance (Core) or Control Plane dashboard <start|status|stop|open> Run local assurance (Core) or Control Plane
version Print version version Print version
help This help help This help
@@ -116,6 +118,10 @@ case "$cmd" in
doctor) doctor)
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: doctor requires the casan-devkit package" >&2; exit 1; } [[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: doctor requires the casan-devkit package" >&2; exit 1; }
exec python3 "$DEVKIT_ROOT/casan-init.py" doctor "$@" ;; exec python3 "$DEVKIT_ROOT/casan-init.py" doctor "$@" ;;
readiness)
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" readiness "$@" ;;
domain)
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" domain "$@" ;;
level) level)
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: level requires the casan-devkit package" >&2; exit 1; } [[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: level requires the casan-devkit package" >&2; exit 1; }
sub="${1:-show}"; shift || true sub="${1:-show}"; shift || true
@@ -180,7 +186,21 @@ case "$cmd" in
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" view "$@" ;; exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" view "$@" ;;
pipeline) pipeline)
RUNNER="$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs" RUNNER="$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs"
[[ -f "$RUNNER" ]] || { echo "casan: pipeline runner is not installed" >&2; exit 1; } if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
cat <<'EOF'
Usage: casan pipeline [project runner arguments]
Runs a project-owned manifest-driven SRS→test pipeline when
scripts/run-casan-pipeline.mjs exists. The pipeline is an optional Domain Pack
capability; CASAN Core prompt assurance and visual reports do not depend on it.
EOF
exit 0
fi
if [[ ! -f "$RUNNER" ]]; then
echo "CASAN_PIPELINE_NOT_CONFIGURED — no project-owned Domain Pipeline runner was found." >&2
echo "Core remains available: use 'casan view' and 'casan readiness'." >&2
exit 2
fi
exec node "$RUNNER" "$@" ;; exec node "$RUNNER" "$@" ;;
dashboard) dashboard)
CONTROL_PLANE_SCRIPT="$HARNESS/../casan-control-panel/scripts/control-plane-local.sh" CONTROL_PLANE_SCRIPT="$HARNESS/../casan-control-panel/scripts/control-plane-local.sh"
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -0,0 +1,52 @@
# Core-only commercial product audit
Audit date: 2026-07-29
Reference project: `Basic Design (Screen&Report)_v2.7`
## Problem reproduced
The previous empty-state overview displayed READY and operational-looking
zeroes before any governed run existed. Core, Domain Pipeline, and Provider
Telemetry were not separated, so a customer could not tell which capability
was installed, optional, or waiting for evidence.
Evidence:
- `00-init-empty-state.png`
- `01-assurance-overview.png`
- `02-run-dossier-h1-h7.png`
- `03-h6-trace-filtered.png`
## Corrected product contract
- `casan init --edition core` writes `.casan/discovery.json` and
`.casan/readiness.json`.
- Core can be `ready` while Domain Pipeline is `not_configured`/`discovered`
and Provider Telemetry is `optional_unavailable`.
- The overview hides operational KPIs until a governed run exists.
- The empty H6 view hides failure, latency, token, cost, coverage, chart and
export controls rather than displaying fabricated zeroes.
- HTML/JSON export remains on-demand.
- `casan pipeline --help` never executes a pipeline; a missing project runner
returns `CASAN_PIPELINE_NOT_CONFIGURED` without marking Core failed.
Evidence:
- `04-commercial-readiness-empty-state.png`
- `05-commercial-h6-empty-state.png`
- `06-basic-design-commercial-overview.png`
- `07-basic-design-commercial-h6.png`
## Acceptance evidence
- clean Core-only vendored init: Core `ready`, Domain Pipeline
`not_configured`, Provider Telemetry `optional_unavailable`;
- no application source or `apps/` directory created by Core init;
- Basic Design discovery: Core `ready_with_attention` because Claude/Codex are
healthy while the selected VS Code route lacks its extension; Domain
Pipeline `discovered`; Provider Telemetry `available`;
- Basic Design H1–H7 domain smoke: seven controls passed;
- Inspect latest run opens the interactive H1–H7 dossier;
- Open H6 opens the trace-filtered AgentOps dossier;
- Core and DevKit release artifacts build and pass extracted-artifact smoke;
- hybrid install suite: 104 pass, 0 fail.
+11
View File
@@ -13,6 +13,7 @@ sh install.sh --level devkit
cd /path/to/my-project cd /path/to/my-project
casan init --project ticketing --client claude,codex casan init --project ticketing --client claude,codex
casan doctor casan doctor
casan readiness --refresh
casan verify-harness casan verify-harness
``` ```
@@ -31,6 +32,16 @@ The viewer is single-project, offline-capable and read-only. It starts lazily on
`127.0.0.1`, so projects do not need Platform, Node/npm or an always-running `127.0.0.1`, so projects do not need Platform, Node/npm or an always-running
service merely to inspect H1–H7 and H6. service merely to inspect H1–H7 and H6.
The dashboard and CLI expose three independent states:
- Core: installation acceptance and client activation;
- Domain Pipeline: optional project-specific SRS→test configuration;
- Provider Telemetry: optional model/provider token and cost coverage.
`not_configured` Domain Pipeline and `optional_unavailable` Provider Telemetry
do not block Core. `casan report export` is on-demand and must not run after
every prompt.
Use this for developer workstations and managed CI runners. CI must install the Use this for developer workstations and managed CI runners. CI must install the
same release recorded by `.casan/version.lock` before running gates. same release recorded by `.casan/version.lock` before running gates.
@@ -0,0 +1,64 @@
# CASAN Commercial Readiness Contract
## Promise
After `casan init`, a repository can use CASAN Core prompt assurance and the
Local Assurance Viewer without modifying application source, installing a
Domain Pack, running a domain pipeline, or exporting HTML after every prompt.
CASAN never turns missing evidence into zero and never calls an installation
READY when a required client activation step remains.
## State model
| Dimension | Ready state | Non-blocking state | Owner |
|---|---|---|---|
| Core | `ready`, `ready_with_attention` | — | CASAN runtime + selected client |
| Domain Pipeline | `configured` | `discovered`, `not_configured` | Project team |
| Provider Telemetry | `available` | `optional_unavailable` | Client/provider adapter |
Core installation acceptance uses H1–H7 as an installation contract:
1. H1 project configuration exists and is valid;
2. H2 selected client integration is installed and trust is explicit;
3. H3 local report capability exists in the resolved runtime;
4. H4 enforcement mode is disclosed (`observe` is a warning);
5. H5 evidence directories are writable;
6. H6 provider token/cost telemetry is measured when available and explicitly
optional when unavailable;
7. H7 bootstrap and version/hash pin exist.
When at least one selected client route is operational but another selected
route fails the deep doctor check, Core is `ready_with_attention`: reports and
governed prompts remain usable through the healthy route, while the broken
route is shown as an explicit activation action.
These checks do not claim that a real prompt run is H1–H7 certified. Run
certification is created only from canonical lifecycle evidence after a prompt.
## CASAN-owned files
- `.casan/discovery.json`: bounded read-only discovery projection;
- `.casan/readiness.json`: shared CLI/viewer state;
- `.casan/domain.json`: optional reference to a project-owned manifest.
No command above creates requirements, traceability mappings, tests, source
files or a fake runnable pipeline.
## Operator path
```bash
casan init
casan doctor
casan readiness --refresh
casan view
# Optional, only for project-specific SRS→test acceptance
casan domain discover
casan domain configure path/to/project.manifest.json
casan pipeline --help
```
`casan pipeline` runs only when the project provides
`scripts/run-casan-pipeline.mjs`. Otherwise it returns
`CASAN_PIPELINE_NOT_CONFIGURED` while leaving Core fully usable.
@@ -7,6 +7,7 @@ reports. Platform is not a prerequisite for basic visibility.
Core owns the single-project review loop: Core owns the single-project review loop:
- commercial readiness split into Core, Domain Pipeline, and Provider Telemetry;
- latest assurance receipt and run history; - latest assurance receipt and run history;
- interactive H1→H7 evidence spine; - interactive H1→H7 evidence spine;
- H6 runtime, reliability, token/cost coverage, freshness and findings; - H6 runtime, reliability, token/cost coverage, freshness and findings;
@@ -38,6 +39,7 @@ review artifact, not the evidence source of truth.
```bash ```bash
casan report latest casan report latest
casan readiness --refresh
casan view [trace-id] [--no-open] casan view [trace-id] [--no-open]
casan report export [trace-id] --format html|json [-o path] casan report export [trace-id] --format html|json [-o path]
@@ -52,6 +54,11 @@ casan dashboard stop
When Platform code is installed, `casan dashboard` manages the centralized When Platform code is installed, `casan dashboard` manages the centralized
Control Plane. `casan view` remains the predictable Core single-project viewer. Control Plane. `casan view` remains the predictable Core single-project viewer.
An empty project must not show operational zeroes or a fabricated READY
verdict. Before the first governed prompt, the overview shows installation
readiness and actionable onboarding only. Operational KPIs appear after
canonical run evidence exists.
## Runtime and packaging contract ## Runtime and packaging contract
- Python standard library only; no Node/npm or network dependency. - Python standard library only; no Node/npm or network dependency.
@@ -109,6 +116,8 @@ The Core viewer is releasable only when all of these pass:
6. browser validation covers the report hierarchy, interaction and responsive 6. browser validation covers the report hierarchy, interaction and responsive
navigation; navigation;
7. existing harness, DevKit installation and packaging suites remain green. 7. existing harness, DevKit installation and packaging suites remain green.
8. a clean `casan init --edition core` project reports Core ready without
requiring a Domain Pack, provider token/cost telemetry, or app-source edits.
## Maturity statement ## Maturity statement
+12 -2
View File
@@ -1,7 +1,8 @@
# CASAN Domain Pack Guide # CASAN Domain Pack Guide
A **Domain Pack** is the per-project data the harness needs to govern YOUR domain. It lives A **Domain Pack** is optional per-project data the harness needs for a
at `apps/<project>/domain/` and is selected via `CASAN_DOMAIN_ROOT`. The harness code project-specific SRS→test pipeline. Core prompt assurance and visual reporting
do not require it. A pack commonly lives at `apps/<project>/domain/`. The harness code
(`packages/casan-harness/`) never contains domain data — this is what makes it reusable. (`packages/casan-harness/`) never contains domain data — this is what makes it reusable.
Scaffold template: `packages/casan-devkit/templates/domain-pack/`. Scaffold template: `packages/casan-devkit/templates/domain-pack/`.
@@ -39,9 +40,18 @@ apps/<project>/domain/
## Wire it up ## Wire it up
```bash ```bash
casan domain discover
casan domain configure apps/<project>/domain/project.manifest.json
casan domain status
# Existing automation remains supported
export CASAN_PROJECT_MANIFEST=apps/<project>/domain/project.manifest.json export CASAN_PROJECT_MANIFEST=apps/<project>/domain/project.manifest.json
bin/casan gate # runs domain-dependent suites against your pack bin/casan gate # runs domain-dependent suites against your pack
``` ```
`casan domain configure` writes only `.casan/domain.json`; it references an
existing in-project manifest and never copies, rewrites or invents application
requirements, source mappings or tests.
`domain_root` is also recorded per-project in `packages/casan-harness/config/project-registry.json` `domain_root` is also recorded per-project in `packages/casan-harness/config/project-registry.json`
so `bin/casan reuse` can prove multi-project reuse. so `bin/casan reuse` can prove multi-project reuse.
+87 -3
View File
@@ -30,6 +30,7 @@ from __future__ import annotations
import argparse import argparse
from contextlib import contextmanager from contextlib import contextmanager
import hashlib import hashlib
import importlib.util
import json import json
import os import os
import queue import queue
@@ -160,6 +161,9 @@ def _render_init(result):
("Clients", _client_names(result["clients"])), ("Clients", _client_names(result["clients"])),
("Files", "%d created or updated" % len(result["created"])), ("Files", "%d created or updated" % len(result["created"])),
("Harness", result["harness_version"]), ("Harness", result["harness_version"]),
("Core readiness", result["readiness"]["dimensions"]["core"]["status"].replace("_", " ")),
("Domain Pipeline", result["readiness"]["dimensions"]["domain_pipeline"]["status"].replace("_", " ")),
("Provider Telemetry", result["readiness"]["dimensions"]["provider_telemetry"]["status"].replace("_", " ")),
]) ])
print() print()
print(_color("1", "Integrations")) print(_color("1", "Integrations"))
@@ -178,9 +182,13 @@ def _render_init(result):
_mark(ok), "VS Code extension", extension.get("status"))) _mark(ok), "VS Code extension", extension.get("status")))
print() print()
print(_color("1", "Next steps")) print(_color("1", "Next steps"))
print(" 1. Run `casan doctor`") actions = result["readiness"].get("next_actions") or []
if "codex" in result["clients"]: steps = [action["message"] for action in actions]
print(" 2. In a local Codex client, open `/hooks` and trust this project's hook.") steps.append("Run `casan doctor` for the deep integrity and client smoke check.")
steps.append(
"After a governed prompt, run `casan view` to inspect its H1–H7 evidence.")
for index, step in enumerate(steps, 1):
print(" %d. %s" % (index, step))
def _render_verify(result): def _render_verify(result):
@@ -1221,6 +1229,7 @@ def cmd_init(args):
cfg = { cfg = {
"schema_version": "21.3", "schema_version": "21.3",
"project_id": project, "project_id": project,
"project_name": project,
"created_at": now_iso(), "created_at": now_iso(),
"enforcement_mode": args.mode, "enforcement_mode": args.mode,
"integration_mode": args.integration_mode, "integration_mode": args.integration_mode,
@@ -1413,6 +1422,40 @@ def cmd_init(args):
newly_owned_files.update(domain_files) newly_owned_files.update(domain_files)
level_extras.append("apps/%s/domain (%d files)" % (project, n)) level_extras.append("apps/%s/domain (%d files)" % (project, n))
# ── Commercial readiness contract.
# Core acceptance never invents or modifies application-domain inputs.
# Domain Pipeline and Provider Telemetry have independent readiness states.
readiness_module_path = os.path.join(
active_harness, "scripts", "python", "readiness.py")
if not os.path.isfile(readiness_module_path):
sys.stderr.write(
"casan init: resolved Core is missing the readiness contract\n")
return 1
readiness_state_paths = (
os.path.join(target, ".casan", "discovery.json"),
os.path.join(target, ".casan", "readiness.json"),
)
for state_path in readiness_state_paths:
mark_owned_if_absent(state_path)
try:
spec = importlib.util.spec_from_file_location(
"casan_runtime_readiness", readiness_module_path)
readiness_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(readiness_module)
client_state = {}
if "codex" in clients:
client_state["codex"] = _codex_project_hook_trust(
target, timeout=3)
readiness = readiness_module.assess(
target, persist=True, client_state=client_state)
except (AttributeError, OSError, TypeError, ValueError) as error:
sys.stderr.write(
"casan init: cannot generate commercial readiness state: %s\n" %
error)
return 1
for state_path in readiness_state_paths:
created_add(state_path)
# ── manifest (so uninstall/verify know what init created) ── # ── manifest (so uninstall/verify know what init created) ──
owned_file_hashes = { owned_file_hashes = {
path: digest path: digest
@@ -1460,6 +1503,7 @@ def cmd_init(args):
"level_extras": level_extras, "level_extras": level_extras,
"level_removed": level_removed, "level_removed": level_removed,
"level_retained": level_retained, "level_retained": level_retained,
"readiness": readiness,
"note": ( "note": (
"managed runtime is referenced by version/hash lock" "managed runtime is referenced by version/hash lock"
if runtime_mode == "managed" else if runtime_mode == "managed" else
@@ -1910,6 +1954,18 @@ def cmd_doctor(args):
ready = ready and client_ready ready = ready and client_ready
checks["client_checks"][client] = item checks["client_checks"][client] = item
for client, item in checks["client_checks"].items():
if item.get("ready") or client == "codex":
continue
checks["required_actions"].append({
"code": "%s_activation" % client.replace("-", "_"),
"message": (
item.get("install_action")
or "Repair the selected %s client route, then run `casan doctor`."
% CLIENT_LABELS.get(client, client)
),
})
if "codex" in clients: if "codex" in clients:
codex_check = checks["client_checks"].get("codex", {}) codex_check = checks["client_checks"].get("codex", {})
trust = codex_check.get("trust", {}) trust = codex_check.get("trust", {})
@@ -1964,6 +2020,31 @@ def cmd_doctor(args):
else "user_action_required" if checks["required_actions"] else "user_action_required" if checks["required_actions"]
else "ready" else "ready"
) )
if harness:
readiness_module_path = os.path.join(
harness, "scripts", "python", "readiness.py")
try:
if not os.path.isfile(readiness_module_path):
raise FileNotFoundError
spec = importlib.util.spec_from_file_location(
"casan_doctor_readiness", readiness_module_path)
readiness_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(readiness_module)
readiness_clients = {
client: {
**(item.get("trust", {}) if client == "codex" else {}),
"operational": bool(item.get("ready")),
}
for client, item in checks["client_checks"].items()
}
checks["readiness"] = readiness_module.assess(
target, persist=True, client_state=readiness_clients)
except FileNotFoundError:
pass
except (AttributeError, OSError, TypeError, ValueError) as error:
checks["warnings"].append(
"Commercial readiness state could not be refreshed: %s." %
error)
_emit_json_or_human(args, checks, _render_doctor) _emit_json_or_human(args, checks, _render_doctor)
return 0 if ready else 2 return 0 if ready else 2
@@ -2150,6 +2231,9 @@ def cmd_uninstall(args):
".casan/version.lock", ".casan/version.lock",
".casan/agentic.env", ".casan/agentic.env",
".casan/casan-hook.py", ".casan/casan-hook.py",
".casan/domain.json",
".casan/discovery.json",
".casan/readiness.json",
".casan/init-manifest.json"): ".casan/init-manifest.json"):
_remove_owned_file( _remove_owned_file(
os.path.join(target, *relative.split("/")), removed, target) os.path.join(target, *relative.split("/")), removed, target)
@@ -80,6 +80,7 @@ for required in (
"packages/casan-harness/scripts/python/local_report.py", "packages/casan-harness/scripts/python/local_report.py",
"packages/casan-harness/scripts/python/local_viewer.py", "packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/report_cli.py", "packages/casan-harness/scripts/python/report_cli.py",
"packages/casan-harness/scripts/python/readiness.py",
): ):
assert (root / required).is_file(), required assert (root / required).is_file(), required
registry = json.loads( registry = json.loads(
@@ -148,6 +149,7 @@ for required in (
"packages/casan-harness/config/tool-registry.yaml", "packages/casan-harness/config/tool-registry.yaml",
"packages/casan-harness/scripts/bash/casan-harness.sh", "packages/casan-harness/scripts/bash/casan-harness.sh",
"packages/casan-harness/scripts/python/local_viewer.py", "packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/readiness.py",
"packaging/runtime-layout.json", "packaging/runtime-layout.json",
"scripts/copy-runtime.py", "scripts/copy-runtime.py",
"install.sh", "install.sh",
@@ -159,9 +161,31 @@ echo "===== ② casan init (config only, no harness copy) ====="
PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package.json" PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package.json"
( cd "$PROJ" && "$CASAN" init --project my-app --mode enforce >/dev/null 2>&1 ) \ ( cd "$PROJ" && "$CASAN" init --project my-app --mode enforce >/dev/null 2>&1 ) \
&& pass "casan init completes" || fail "casan init failed" && pass "casan init completes" || fail "casan init failed"
for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .casan/discovery.json .casan/readiness.json .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do
[[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f" [[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f"
done done
python3 - "$PROJ/.casan/readiness.json" <<'PY' \
&& pass "Core readiness is independent from optional Domain/Provider states" \
|| fail "commercial readiness contract is invalid"
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["dimensions"]["core"]["status"] in {"ready", "ready_with_attention"}
assert d["dimensions"]["domain_pipeline"]["status"] == "not_configured"
assert d["dimensions"]["provider_telemetry"]["status"] == "optional_unavailable"
PY
CASAN_APP_ROOT="$PROJ" "$CASAN" pipeline --help 2>&1 \
| grep -q "optional Domain Pack" \
&& pass "pipeline help describes the optional capability without executing it" \
|| fail "pipeline help executed or hid the Core/Domain boundary"
set +e
PIPELINE_MISSING="$(CASAN_APP_ROOT="$PROJ" "$CASAN" pipeline 2>&1)"
PIPELINE_RC=$?
set -e
[[ "$PIPELINE_RC" -eq 2 && "$PIPELINE_MISSING" == *"CASAN_PIPELINE_NOT_CONFIGURED"* ]] \
&& pass "missing Domain Pipeline is explicit and does not impersonate a Core failure" \
|| fail "missing Domain Pipeline capability was not reported truthfully"
python3 - "$PROJ/.casan/config.json" <<'PY' \ python3 - "$PROJ/.casan/config.json" <<'PY' \
&& pass "config declares supported and excluded client surfaces" \ && pass "config declares supported and excluded client surfaces" \
|| fail "config client surface contract is incomplete" || fail "config client surface contract is incomplete"
@@ -125,11 +125,14 @@ main { max-width: 1500px; margin: 0 auto; padding: 28px clamp(20px,4vw,48px) 64p
font-size: 9px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; font-size: 9px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap;
} }
.pill.pass, .pill.certified, .pill.success, .pill.fresh, .pill.operational { background: var(--green-soft); color: var(--green); } .pill.pass, .pill.certified, .pill.success, .pill.fresh, .pill.operational { background: var(--green-soft); color: var(--green); }
.pill.warning, .pill.attention, .pill.partial, .pill.stale, .pill.in-progress, .pill.in_progress { background: var(--amber-soft); color: var(--amber); } .pill.warning, .pill.attention, .pill.ready-with-attention, .pill.ready_with_attention, .pill.partial, .pill.stale, .pill.in-progress, .pill.in_progress { background: var(--amber-soft); color: var(--amber); }
.pill.error, .pill.failed, .pill.fail, .pill.blocked, .pill.non-certified, .pill.non_certified { background: var(--rose-soft); color: var(--rose); } .pill.error, .pill.failed, .pill.fail, .pill.blocked, .pill.non-certified, .pill.non_certified { background: var(--rose-soft); color: var(--rose); }
.pill.neutral, .pill.queued, .pill.unknown, .pill.no-data, .pill.no_data { background: #f2f4f7; color: #475467; } .pill.neutral, .pill.queued, .pill.unknown, .pill.no-data, .pill.no_data { background: #f2f4f7; color: #475467; }
.hero .pill { border: 1px solid #344054; background: #1d2939; color: #d0d5dd; } .hero .pill { border: 1px solid #344054; background: #1d2939; color: #d0d5dd; }
.metric-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; } .metric-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; }
.metric-grid.readiness-grid { grid-template-columns: repeat(3,minmax(0,1fr)); }
.readiness-grid .metric-value { font-size: 24px; text-transform: capitalize; }
.onboarding-actions { margin-bottom: 18px; }
.metric-card, .panel { .metric-card, .panel {
border: 1px solid var(--line); border-radius: 18px; background: white; box-shadow: var(--shadow); border: 1px solid var(--line); border-radius: 18px; background: white; box-shadow: var(--shadow);
} }
@@ -275,6 +278,7 @@ tbody tr:hover { background: #f0fdfa; }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.metric-grid { grid-template-columns: repeat(2,1fr); } .metric-grid { grid-template-columns: repeat(2,1fr); }
.metric-grid.readiness-grid { grid-template-columns: 1fr; }
.two-col, .breakdown-grid, .dossier { grid-template-columns: 1fr; } .two-col, .breakdown-grid, .dossier { grid-template-columns: 1fr; }
.hero-grid { grid-template-columns: 1fr; } .hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; } .hero-actions { justify-content: flex-start; }
@@ -27,6 +27,7 @@
? "Unavailable" : new Intl.NumberFormat("en-US").format(Number(value)); ? "Unavailable" : new Intl.NumberFormat("en-US").format(Number(value));
const time = (value) => value ? String(value).replace("T", " ").replace("Z", " UTC") : "No timestamp"; const time = (value) => value ? String(value).replace("T", " ").replace("Z", " UTC") : "No timestamp";
const money = (value) => value === null || value === undefined ? "Unavailable" : `$${Number(value).toFixed(6)}`; const money = (value) => value === null || value === undefined ? "Unavailable" : `$${Number(value).toFixed(6)}`;
const statusLabel = (value) => String(value || "unknown").replaceAll("_", " ");
const pill = (value) => `<span class="pill ${slug(value)}">${esc(String(value || "unknown").replaceAll("_", " "))}</span>`; const pill = (value) => `<span class="pill ${slug(value)}">${esc(String(value || "unknown").replaceAll("_", " "))}</span>`;
async function api(path) { async function api(path) {
@@ -122,6 +123,22 @@
const certified = latest.certified === true; const certified = latest.certified === true;
const latestTrace = latest.trace_id || recent[0]?.trace_id; const latestTrace = latest.trace_id || recent[0]?.trace_id;
const maturity = profile.maturity || {}; const maturity = profile.maturity || {};
const readiness = profile.readiness || {};
const dimensions = readiness.dimensions || {};
const core = dimensions.core || { status: "not_assessed", summary: "Core readiness has not been assessed." };
const domain = dimensions.domain_pipeline || { status: "not_assessed", summary: "Domain Pipeline readiness has not been assessed." };
const telemetry = dimensions.provider_telemetry || { status: "not_assessed", summary: "Provider Telemetry readiness has not been assessed." };
const nextActions = readiness.next_actions || [];
const coreReady = ["ready", "ready_with_attention"].includes(core.status);
const readinessActionList = `<div class="findings">${nextActions.map((action, index) => `<div class="finding warning">
<span class="finding-index">${String(index + 1).padStart(2, "0")}</span>
<div><code>${esc(action.code)}</code><p>${esc(action.message)}</p></div>
</div>`).join("")}</div>`;
const readinessCards = `<section class="metric-grid readiness-grid" aria-label="Commercial readiness">
${metric("Core", statusLabel(core.status), core.summary)}
${metric("Domain Pipeline", statusLabel(domain.status), domain.summary)}
${metric("Provider Telemetry", statusLabel(telemetry.status), telemetry.summary)}
</section>`;
const actions = latestTrace const actions = latestTrace
? `<button class="button primary" type="button" data-open-trace="${esc(latestTrace)}">Inspect latest run</button> ? `<button class="button primary" type="button" data-open-trace="${esc(latestTrace)}">Inspect latest run</button>
<button class="button secondary" type="button" data-open-h6="${esc(latestTrace)}">Open H6</button>` <button class="button secondary" type="button" data-open-h6="${esc(latestTrace)}">Open H6</button>`
@@ -129,22 +146,43 @@
$("#overview-view").innerHTML = $("#overview-view").innerHTML =
hero({ hero({
eyebrow: "CASAN Core · Local assurance", eyebrow: "CASAN Core · Local assurance",
title: latestTrace ? "One prompt. One evidence trail." : "Ready for the first governed run.", title: latestTrace ? "One prompt. One evidence trail." :
description: "A read-only, offline-capable assurance workspace reconstructed from canonical project evidence. No Platform service or HTML generation on the prompt hot path.", coreReady ? "Core reporting is ready." : "Finish Core activation.",
verdict: latestTrace ? (certified ? "certified" : "review required") : "ready", description: latestTrace
? "A read-only, offline-capable assurance workspace reconstructed from canonical project evidence. No Platform service or HTML generation on the prompt hot path."
: coreReady
? "Core visual reporting works without a Domain Pack or application-source changes. Send a governed prompt to create the first run dossier."
: "Core visual reporting works without a Domain Pack or application-source changes. Complete the activation action below, then send a governed prompt.",
verdict: latestTrace ? (certified ? "certified" : "review required") : core.status,
meta: [ meta: [
`${profile.project_name} · ${profile.edition}`, `${profile.project_name} · ${profile.edition}`,
latestTrace ? `Trace ${latestTrace}` : "No finalized trace yet", latestTrace ? `Trace ${latestTrace}` : "No finalized trace yet",
`Maturity ${maturity.level == null ? "not assessed" : `L${maturity.level}`} · ${maturity.status}`, `Maturity ${maturity.level == null ? "not assessed" : `L${maturity.level}`} · ${statusLabel(maturity.status)}`,
], ],
actions, actions,
}) + }) + readinessCards +
`<section class="metric-grid"> (latestTrace && nextActions.length ? panel(
"Activation attention",
"A selected client route needs action.",
"Healthy routes remain operational; complete this step before relying on the affected integration.",
readinessActionList,
pill(core.status),
) : "") +
(latestTrace ? `<section class="metric-grid">
${metric("Governed records", number(registry.count), `${recent.length} recent records loaded`)} ${metric("Governed records", number(registry.count), `${recent.length} recent records loaded`)}
${metric("Latest H6 quality", latest.telemetry_quality || "Unavailable", "Missing values are disclosed, never coerced to zero")} ${metric("Latest H6 quality", latest.telemetry_quality || "Unavailable", "Missing values are disclosed, never coerced to zero")}
${metric("Observed failures", number(failures), `${recent.length ? Math.round((failures / recent.length) * 100) : 0}% of loaded records`)} ${metric("Observed failures", number(failures), `${recent.length ? Math.round((failures / recent.length) * 100) : 0}% of loaded records`)}
${metric("Evidence gates", latestTrace ? "H1 → H7" : "Waiting", latestTrace ? "Control-by-control reconstruction available" : "Evidence appears after a prompt finalizes")} ${metric("Evidence gates", latestTrace ? "H1 → H7" : "Waiting", latestTrace ? "Control-by-control reconstruction available" : "Evidence appears after a prompt finalizes")}
</section>` + </section>` : panel(
"First-run onboarding",
"No operational KPIs are shown until evidence exists.",
"CASAN does not turn missing telemetry into zeroes or claim a run is certified before a governed prompt finishes.",
`${nextActions.length ? `<div class="onboarding-actions">${readinessActionList}</div>` : ""}
<div class="two-col">
<div class="notice success"><strong>Available now</strong><br>Local viewer, H1–H7 run dossier, history, H6 AgentOps and on-demand JSON/HTML export.</div>
<div class="notice warning"><strong>Optional expansion</strong><br>Install a Domain Pack only when the team needs project-specific SRS→test acceptance.</div>
</div>`
)) +
disclosure( disclosure(
"About this local report", "About this local report",
"Edition boundaries and implementation notes are hidden until needed.", "Edition boundaries and implementation notes are hidden until needed.",
@@ -172,13 +210,22 @@
function renderRuns() { function renderRuns() {
const rows = state.registry.runs || []; const rows = state.registry.runs || [];
const selected = state.run; const selected = state.run;
const hasRuns = rows.length > 0;
const body = hero({ const body = hero({
eyebrow: "Run assurance registry", eyebrow: "Run assurance registry",
title: "Every governed run, one evidence trail.", title: hasRuns ? "Every governed run, one evidence trail." : "No governed runs yet.",
description: "Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent dossier.", description: hasRuns
verdict: rows.some((row) => row.status === "failed") ? "attention" : "operational", ? "Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent dossier."
meta: [`${state.registry.count} records`, `${rows.filter((row) => row.status === "failed").length} failures`, "Local canonical evidence"], : "The registry will populate after a configured client finalizes its first governed prompt.",
actions: `<button class="button primary" type="button" data-view-target="h6">Open H6 dossier</button>`, verdict: hasRuns
? (rows.some((row) => row.status === "failed") ? "attention" : "operational")
: "waiting",
meta: hasRuns
? [`${state.registry.count} records`, `${rows.filter((row) => row.status === "failed").length} failures`, "Local canonical evidence"]
: ["No operational counters yet", "Local canonical evidence"],
actions: hasRuns
? `<button class="button primary" type="button" data-view-target="h6">Open H6 dossier</button>`
: "",
}) + (selected ? renderTrace(selected) : "") + recentRunsPanel(rows); }) + (selected ? renderTrace(selected) : "") + recentRunsPanel(rows);
$("#runs-view").innerHTML = body; $("#runs-view").innerHTML = body;
bindActions(); bindActions();
@@ -225,10 +272,33 @@
function renderH6() { function renderH6() {
const data = state.h6; const data = state.h6;
if (!data) return; if (!data) return;
const hasRuns = Number(data.summary?.runs) > 0;
const query = data.scope || {}; const query = data.scope || {};
const queryObject = Object.fromEntries(Object.entries(query).filter(([, value]) => value)); const queryObject = Object.fromEntries(Object.entries(query).filter(([, value]) => value));
const actions = `<a class="button secondary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "json" })}">JSON evidence</a> const actions = `<a class="button secondary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "html" })}">Export dossier</a>`; <a class="button primary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "html" })}">Export dossier</a>`;
if (!hasRuns) {
const provider = state.project?.readiness?.dimensions?.provider_telemetry || {};
$("#h6-view").innerHTML =
hero({
eyebrow: "H6 · AgentOps assurance",
title: "H6 begins with the first governed run.",
description: "No runtime record exists yet, so CASAN hides failure, latency, token and cost counters instead of presenting fabricated zeroes.",
verdict: "waiting",
meta: [data.report_id, `Generated ${time(data.generated_at)}`, "No operational evidence yet"],
actions: "",
}) +
panel(
"H6 onboarding",
"Provider telemetry is optional.",
provider.summary || "Provider token and cost telemetry has not been observed; this does not block Core.",
`<div class="two-col">
<div class="notice success"><strong>Core remains ready</strong><br>Send a prompt through a configured client. CASAN will persist runtime evidence automatically.</div>
<div class="notice warning"><strong>No automatic export</strong><br>Open H6 or export a dossier only when a review artifact is needed.</div>
</div>`,
);
return;
}
const findings = data.findings.length ? `<div class="findings">${data.findings.map((item, index) => `<article class="finding ${slug(item.severity)}"> const findings = data.findings.length ? `<div class="findings">${data.findings.map((item, index) => `<article class="finding ${slug(item.severity)}">
<span class="finding-index">${String(index + 1).padStart(2, "0")}</span><div><code>${esc(item.code)}</code><p>${esc(item.message)}</p></div></article>`).join("")}</div>` <span class="finding-index">${String(index + 1).padStart(2, "0")}</span><div><code>${esc(item.code)}</code><p>${esc(item.message)}</p></div></article>`).join("")}</div>`
: `<div class="notice success">No threshold breach was detected in the selected evidence boundary.</div>`; : `<div class="notice success">No threshold breach was detected in the selected evidence boundary.</div>`;
@@ -174,6 +174,7 @@ def paths(root: str | Path) -> dict[str, Path]:
state = state_root(root) state = state_root(root)
return { return {
"config": Path(root).resolve() / ".casan" / "config.json", "config": Path(root).resolve() / ".casan" / "config.json",
"readiness": Path(root).resolve() / ".casan" / "readiness.json",
"receipt": state / "state" / "latest-run.json", "receipt": state / "state" / "latest-run.json",
"metrics": state / "logs" / "cost" / "metrics.jsonl", "metrics": state / "logs" / "cost" / "metrics.jsonl",
"provider": state / "logs" / "level5" / "provider-usage.jsonl", "provider": state / "logs" / "level5" / "provider-usage.jsonl",
@@ -185,6 +186,7 @@ def paths(root: str | Path) -> dict[str, Path]:
def project_profile(root: str | Path) -> dict[str, Any]: def project_profile(root: str | Path) -> dict[str, Any]:
config = read_json(paths(root)["config"]) config = read_json(paths(root)["config"])
readiness = read_json(paths(root)["readiness"])
maturity = config.get("maturity") if isinstance(config.get("maturity"), dict) else {} maturity = config.get("maturity") if isinstance(config.get("maturity"), dict) else {}
return { return {
"project_id": _text(config.get("project_id"), Path(root).resolve().name), "project_id": _text(config.get("project_id"), Path(root).resolve().name),
@@ -199,6 +201,26 @@ def project_profile(root: str | Path) -> dict[str, Any]:
"status": _text(maturity.get("status"), "not_assessed"), "status": _text(maturity.get("status"), "not_assessed"),
"evidence": _text(maturity.get("evidence")) or None, "evidence": _text(maturity.get("evidence")) or None,
}, },
"readiness": readiness if readiness else {
"schema_version": 1,
"overall_status": "not_assessed",
"dimensions": {
"core": {
"status": "not_assessed",
"summary": "Run `casan readiness --refresh` to assess this installation.",
"acceptance": [],
},
"domain_pipeline": {
"status": "not_assessed",
"summary": "Domain Pipeline readiness has not been assessed.",
},
"provider_telemetry": {
"status": "not_assessed",
"summary": "Provider Telemetry readiness has not been assessed.",
},
},
"next_actions": [],
},
"viewer": { "viewer": {
"mode": "local_core", "mode": "local_core",
"scope": "single_project", "scope": "single_project",
@@ -0,0 +1,524 @@
#!/usr/bin/env python3
"""CASAN commercial-readiness discovery and assessment.
This module is intentionally stdlib-only. It assesses the CASAN-owned
installation contract without changing application source or inventing domain
traceability. Core, Domain Pipeline, and Provider Telemetry are independent
dimensions because each has a different owner and activation lifecycle.
"""
from __future__ import annotations
from datetime import datetime, timezone
import json
import os
from pathlib import Path
from typing import Any
SCHEMA_VERSION = 1
SKIP_DIRECTORIES = {
".casan", ".git", ".hg", ".idea", ".specify", ".svn", ".venv",
"coverage", "dist", "node_modules", "target", "vendor",
}
PROJECT_MARKERS = (
"package.json", "pyproject.toml", "pom.xml", "build.gradle",
"build.gradle.kts", "go.mod", "Cargo.toml", "composer.json",
)
SOURCE_ROOT_NAMES = ("src", "app", "apps", "packages", "lib", "services")
REQUIREMENT_NAMES = (
"requirements.md", "requirement.md", "srs.md", "spec.md",
"product-requirements.md",
)
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
return value if isinstance(value, dict) else {}
except (OSError, TypeError, ValueError):
return {}
def write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
os.replace(temporary, path)
def configure_domain(root: str | Path, manifest: str | Path) -> dict[str, Any]:
"""Select an existing project manifest without copying or editing app code."""
project_root = Path(root).expanduser().resolve()
selected = Path(manifest).expanduser()
if not selected.is_absolute():
selected = project_root / selected
selected = selected.resolve()
try:
relative = selected.relative_to(project_root).as_posix()
except ValueError as error:
raise ValueError("domain manifest must stay inside the project root") from error
if not selected.is_file():
raise ValueError("domain manifest does not exist")
try:
document = json.loads(selected.read_text(encoding="utf-8"))
except (OSError, TypeError, ValueError) as error:
raise ValueError("domain manifest must be valid JSON") from error
if not isinstance(document, dict) or not (
document.get("project_id") or document.get("schema_version")
):
raise ValueError(
"domain manifest must declare project_id or schema_version")
value = {
"schema_version": SCHEMA_VERSION,
"configured_at": utc_now(),
"manifest": relative,
"ownership": "project_source_referenced_by_casan",
}
write_json(project_root / ".casan" / "domain.json", value)
return value
def _relative(root: Path, path: Path) -> str:
return path.relative_to(root).as_posix()
def _bounded_files(root: Path, maximum_depth: int = 5) -> list[Path]:
files: list[Path] = []
for current, directories, names in os.walk(root):
current_path = Path(current)
try:
depth = len(current_path.relative_to(root).parts)
except ValueError:
continue
directories[:] = sorted(
name for name in directories
if name not in SKIP_DIRECTORIES and not name.startswith(".cache")
)
if depth >= maximum_depth:
directories[:] = []
for name in sorted(names):
files.append(current_path / name)
if len(files) >= 10_000:
return files
return files
def discover(root: str | Path) -> dict[str, Any]:
project_root = Path(root).expanduser().resolve()
priority_files: list[Path] = []
for relative in ("apps", "packages", "docs"):
candidate = project_root / relative
if candidate.is_dir():
priority_files.extend(_bounded_files(candidate, maximum_depth=6))
files = list(dict.fromkeys(
priority_files + _bounded_files(project_root)
))
markers = [
name for name in PROJECT_MARKERS
if (project_root / name).is_file()
]
source_roots = [
name for name in SOURCE_ROOT_NAMES
if (project_root / name).is_dir()
]
domain_packs: list[str] = []
manifests: list[str] = []
requirements: list[str] = []
for path in files:
relative = _relative(project_root, path)
lowered = path.name.lower()
if lowered == "domain-pack.yaml":
domain_packs.append(relative)
elif lowered == "project.manifest.json":
manifests.append(relative)
elif (
lowered in REQUIREMENT_NAMES
or ("requirement" in lowered and lowered.endswith(".md"))
):
requirements.append(relative)
return {
"schema_version": SCHEMA_VERSION,
"generated_at": utc_now(),
"project_root": str(project_root),
"project_markers": markers[:32],
"source_roots": source_roots[:32],
"domain_pack_candidates": domain_packs[:64],
"project_manifest_candidates": manifests[:64],
"requirements_candidates": requirements[:64],
"scan": {
"maximum_depth": 5,
"maximum_files_per_root": 10_000,
"priority_roots": [
value for value in ("apps", "packages", "docs")
if (project_root / value).is_dir()
],
"files_observed": len(files),
"truncated": len(files) >= 10_000,
},
}
def _contains(path: Path, *needles: str) -> bool:
try:
if not path.is_file() or path.stat().st_size > 2 * 1024 * 1024:
return False
content = path.read_text(encoding="utf-8", errors="replace")
return all(needle in content for needle in needles)
except OSError:
return False
def _runtime_harness(root: Path, config: dict[str, Any]) -> Path | None:
runtime_path = config.get("runtime_path")
if not isinstance(runtime_path, str) or not runtime_path.strip():
return None
base = Path(runtime_path).expanduser()
if not base.is_absolute():
base = root / base
candidate = base.resolve()
nested = candidate / "packages" / "casan-harness"
if nested.is_dir():
return nested
return candidate if candidate.name == "casan-harness" and candidate.is_dir() else None
def _check(
gate: str,
title: str,
status: str,
summary: str,
evidence: list[str] | None = None,
) -> dict[str, Any]:
return {
"gate": gate,
"title": title,
"status": status,
"summary": summary,
"evidence": evidence or [],
}
def _provider_available(root: Path) -> bool:
candidates = (
root / ".specify" / "logs" / "level5" / "provider-usage.jsonl",
root / ".specify" / "logs" / "cost" / "metrics.jsonl",
)
for path in candidates:
try:
if path.is_file() and path.stat().st_size > 2:
text = path.read_text(encoding="utf-8", errors="replace")[-262_144:]
for line in reversed(text.splitlines()):
try:
row = json.loads(line)
except (TypeError, ValueError):
continue
if not isinstance(row, dict):
continue
if (
isinstance(row.get("provider"), str) and row["provider"].strip()
or isinstance(row.get("model"), str) and row["model"].strip()
or isinstance(row.get("total_tokens"), (int, float))
or isinstance(row.get("cost_estimate"), (int, float))
):
return True
except OSError:
continue
return False
def assess(
root: str | Path,
*,
persist: bool = False,
client_state: dict[str, Any] | None = None,
) -> dict[str, Any]:
project_root = Path(root).expanduser().resolve()
config_path = project_root / ".casan" / "config.json"
lock_path = project_root / ".casan" / "version.lock"
bootstrap_path = project_root / ".casan" / "casan-hook.py"
config = read_json(config_path)
lock = read_json(lock_path)
domain_config = read_json(project_root / ".casan" / "domain.json")
previous_readiness = read_json(project_root / ".casan" / "readiness.json")
if client_state is None:
previous_core = (
(previous_readiness.get("dimensions") or {}).get("core") or {}
)
previous_client_state = previous_core.get("client_activation")
client_state = (
previous_client_state
if isinstance(previous_client_state, dict) else {}
)
discovery = discover(project_root)
clients = [
str(value) for value in config.get("clients", [])
if isinstance(value, str)
]
runtime = _runtime_harness(project_root, config)
h1_ok = bool(config.get("project_id") and config.get("schema_version"))
h2_evidence: list[str] = []
client_integrations: list[dict[str, Any]] = []
for client in clients:
declared_state = (client_state or {}).get(client) or {}
if client == "claude":
relative = ".claude/settings.json"
present = _contains(
project_root / relative, "casan-hook.py", "--client claude")
operational = bool(
present and declared_state.get("operational", True))
elif client == "codex":
relative = ".codex/hooks.json"
present = _contains(
project_root / relative, "casan-hook.py", "--client codex")
trust_state = declared_state
operational = bool(
present and trust_state.get("verified")
and trust_state.get("operational")
)
elif client == "vscode-copilot":
relative = ".casan/casan-hook.py"
present = (project_root / relative).is_file()
operational = bool(
present and declared_state.get("operational", True))
else:
client_integrations.append({
"client": client,
"status": "unsupported",
"evidence": None,
})
continue
if present:
h2_evidence.append(relative)
client_integrations.append({
"client": client,
"status": (
"operational" if operational else
"action_required" if present else
"missing"
),
"evidence": relative if present else None,
})
operational_clients = sum(
item["status"] == "operational" for item in client_integrations)
configured_clients = sum(
item["status"] in {"operational", "action_required"}
for item in client_integrations)
h2_status = (
"warning" if operational_clients and operational_clients < len(client_integrations) else
"pass" if operational_clients else
"action_required" if configured_clients else
"fail"
)
report_engine = (
runtime / "scripts" / "python" / "local_report.py"
if runtime else Path()
)
h3_ok = bool(runtime and report_engine.is_file())
enforcement = str(config.get("enforcement_mode") or "unknown")
h4_status = "pass" if enforcement == "enforce" else "warning"
state_dirs = (
project_root / ".specify" / "logs",
project_root / ".specify" / "state",
)
h5_ok = all(
path.is_dir() and os.access(path, os.W_OK | os.X_OK)
for path in state_dirs
)
provider_available = _provider_available(project_root)
h7_ok = bool(
lock.get("harness_hash")
and lock.get("harness_version")
and bootstrap_path.is_file()
)
trust = (client_state or {}).get("codex") or {}
codex_action_required = (
"codex" in clients
and not (trust.get("verified") and trust.get("operational"))
)
checks = [
_check("H1", "Project contract", "pass" if h1_ok else "fail",
"CASAN project identity and schema are valid." if h1_ok
else "CASAN project configuration is missing or invalid.",
[".casan/config.json"] if h1_ok else []),
_check("H2", "Client activation", h2_status,
f"{operational_clients} configured client route(s) are operational."
if h2_status in {"pass", "warning"} else
"Configured client hooks require an activation step."
if h2_status == "action_required" else
"No complete supported client integration is installed.",
h2_evidence),
_check("H3", "Local report capability", "pass" if h3_ok else "fail",
"Core Local Assurance Viewer and on-demand export are available."
if h3_ok else "The resolved Core report runtime is unavailable.",
[str(report_engine)] if h3_ok else []),
_check("H4", "Enforcement boundary", h4_status,
"Policy enforcement is active." if enforcement == "enforce"
else "Observe mode records evidence but does not enforce decisions.",
[".casan/config.json"]),
_check("H5", "Evidence persistence", "pass" if h5_ok else "fail",
"Local logs and state directories are writable." if h5_ok
else "CASAN evidence directories are missing or not writable.",
[".specify/logs", ".specify/state"] if h5_ok else []),
_check("H6", "Provider telemetry", "pass" if provider_available else "optional_unavailable",
"Provider token/cost telemetry is available." if provider_available
else "Core runtime evidence is available; provider token/cost telemetry is optional and not observed yet."),
_check("H7", "Pinned runtime", "pass" if h7_ok else "fail",
"The project bootstrap and pinned runtime contract are present."
if h7_ok else "The bootstrap or version pin is incomplete.",
[".casan/casan-hook.py", ".casan/version.lock"] if h7_ok else []),
]
blocking = any(item["status"] == "fail" for item in checks)
action_required = any(
item["status"] == "action_required" for item in checks
)
attention = any(item["status"] == "warning" for item in checks)
core_status = (
"blocked" if blocking else
"action_required" if action_required else
"ready_with_attention" if attention else
"ready"
)
manifests = discovery["project_manifest_candidates"]
packs = discovery["domain_pack_candidates"]
selected_manifest = domain_config.get("manifest")
selected_path = (
project_root / selected_manifest
if isinstance(selected_manifest, str) else None
)
if selected_path and selected_path.is_file():
domain_status = "configured"
domain_summary = (
"A project manifest is selected. Run project validation or the "
"project-owned pipeline to produce domain acceptance evidence."
)
elif manifests:
domain_status = "discovered"
domain_summary = (
"Project manifest candidates were discovered but none is selected. "
"Use `casan domain configure <manifest>` when domain acceptance is needed."
)
elif packs:
domain_status = "discovered"
domain_summary = (
"A Domain Pack was discovered, but no project manifest proves a "
"runnable domain pipeline."
)
else:
domain_status = "not_configured"
domain_summary = (
"No Domain Pack is configured. Core prompt assurance and reports "
"remain fully available without modifying application source."
)
telemetry_status = "available" if provider_available else "optional_unavailable"
next_actions: list[dict[str, str]] = []
if blocking:
next_actions.append({
"code": "repair_core",
"message": "Run `casan init` again, then `casan doctor`.",
})
if codex_action_required:
reason = str(trust.get("reason") or trust.get("status") or "not_verified")
next_actions.append({
"code": "verify_codex_trust",
"message": (
"Open `/hooks` in Codex and review the project hooks, then run "
f"`casan doctor` (current trust state: {reason})."
),
})
for integration in client_integrations:
if (
integration["status"] != "operational"
and integration["client"] != "codex"
):
next_actions.append({
"code": "activate_%s" % integration["client"].replace("-", "_"),
"message": (
f"Complete activation for the selected "
f"{integration['client']} client route, then run `casan doctor`."
),
})
optional_actions: list[dict[str, str]] = []
if domain_status != "configured":
optional_actions.append({
"code": "configure_domain_pipeline",
"message": (
"Optional: install/configure a CASAN Domain Pack when the team "
"needs project-specific SRS→test acceptance."
),
})
readiness = {
"schema_version": SCHEMA_VERSION,
"generated_at": utc_now(),
"project_id": str(config.get("project_id") or project_root.name),
"overall_status": core_status,
"dimensions": {
"core": {
"status": core_status,
"summary": (
"Core is ready for governed prompts and visual local reports."
if core_status == "ready" else
"Core is operational through at least one client route; another selected route needs attention."
if core_status == "ready_with_attention" else
"Core requires an explicit activation step."
if core_status == "action_required" else
"Core installation is incomplete."
),
"acceptance": checks,
"client_activation": client_state or {},
"client_integrations": client_integrations,
},
"domain_pipeline": {
"status": domain_status,
"summary": domain_summary,
"domain_packs": packs,
"project_manifests": manifests,
"selected_manifest": selected_manifest
if selected_path and selected_path.is_file() else None,
},
"provider_telemetry": {
"status": telemetry_status,
"summary": (
"Provider token and cost telemetry has been observed."
if provider_available else
"Provider token and cost telemetry is optional and has not "
"been observed; this does not block Core."
),
},
},
"next_actions": next_actions,
"optional_actions": optional_actions,
"discovery_file": ".casan/discovery.json",
}
if persist:
write_json(project_root / ".casan" / "discovery.json", discovery)
write_json(project_root / ".casan" / "readiness.json", readiness)
return readiness
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", default=os.getcwd())
parser.add_argument("--write", action="store_true")
arguments = parser.parse_args()
print(json.dumps(
assess(arguments.root, persist=arguments.write),
ensure_ascii=False,
indent=2,
))
@@ -24,6 +24,11 @@ from local_report import (
report_html, report_html,
run_report, run_report,
) )
from readiness import (
assess as assess_readiness,
configure_domain,
discover as discover_project,
)
def find_root(start: str) -> Path: def find_root(start: str) -> Path:
@@ -302,6 +307,21 @@ def parser() -> argparse.ArgumentParser:
latest = commands.add_parser("latest", help="show the latest finalized prompt receipt") latest = commands.add_parser("latest", help="show the latest finalized prompt receipt")
latest.add_argument("--json", action="store_true") latest.add_argument("--json", action="store_true")
readiness = commands.add_parser(
"readiness",
help="assess Core, Domain Pipeline, and Provider Telemetry separately",
)
readiness.add_argument("--refresh", action="store_true")
readiness.add_argument("--json", action="store_true")
domain = commands.add_parser(
"domain",
help="discover or select an optional project-owned Domain Pack manifest",
)
domain.add_argument("action", choices=["status", "discover", "configure"])
domain.add_argument("manifest", nargs="?")
domain.add_argument("--json", action="store_true")
view = commands.add_parser("view", help="open a trace in the local Core viewer") view = commands.add_parser("view", help="open a trace in the local Core viewer")
view.add_argument("trace_id", nargs="?") view.add_argument("trace_id", nargs="?")
view.add_argument("--no-open", action="store_true") view.add_argument("--no-open", action="store_true")
@@ -333,6 +353,51 @@ def main(argv=None) -> int:
print(json.dumps(receipt, ensure_ascii=False, indent=2)) print(json.dumps(receipt, ensure_ascii=False, indent=2))
return 0 if receipt else 1 return 0 if receipt else 1
return print_receipt(receipt, root) return print_receipt(receipt, root)
if args.command == "readiness":
result = assess_readiness(root, persist=args.refresh)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
dimensions = result["dimensions"]
print(f"CASAN readiness — {result['project_id']}")
print(f" Core {dimensions['core']['status']}")
print(f" Domain Pipeline {dimensions['domain_pipeline']['status']}")
print(f" Provider Telemetry {dimensions['provider_telemetry']['status']}")
for action in result.get("next_actions", []):
print(f" → {action['message']}")
return 2 if result["overall_status"] == "blocked" else 0
if args.command == "domain":
try:
if args.action == "discover":
payload = discover_project(root)
elif args.action == "configure":
if not args.manifest:
raise ValueError(
"`casan domain configure` requires a manifest path")
configure_domain(root, args.manifest)
payload = assess_readiness(root, persist=True)
else:
payload = assess_readiness(root, persist=False)[
"dimensions"]["domain_pipeline"]
except ValueError as error:
print(f"CASAN_DOMAIN_INVALID — {error}", file=sys.stderr)
return 2
if args.json:
print(json.dumps(payload, ensure_ascii=False, indent=2))
elif args.action == "discover":
print("CASAN Domain discovery")
print(
f" Manifests {len(payload['project_manifest_candidates'])}")
print(f" Domain Packs {len(payload['domain_pack_candidates'])}")
print(f" Requirements {len(payload['requirements_candidates'])}")
else:
dimension = (
payload["dimensions"]["domain_pipeline"]
if "dimensions" in payload else payload
)
print(f"CASAN Domain Pipeline — {dimension['status']}")
print(f" {dimension['summary']}")
return 0
if args.command == "viewer": if args.command == "viewer":
return viewer_command(root, args.action, args.port, args.no_open) return viewer_command(root, args.action, args.port, args.no_open)
if args.command == "export": if args.command == "export":
@@ -27,6 +27,7 @@ CASAN_BIN="$BUNDLE/bin/casan"
test -f "$BUNDLE/packages/casan-harness/assets/local-viewer/index.html" test -f "$BUNDLE/packages/casan-harness/assets/local-viewer/index.html"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_report.py" test -f "$BUNDLE/packages/casan-harness/scripts/python/local_report.py"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_viewer.py" test -f "$BUNDLE/packages/casan-harness/scripts/python/local_viewer.py"
test -f "$BUNDLE/packages/casan-harness/scripts/python/readiness.py"
test ! -e "$BUNDLE/packages/casan-control-panel" test ! -e "$BUNDLE/packages/casan-control-panel"
test ! -e "$BUNDLE/package.json" test ! -e "$BUNDLE/package.json"
@@ -37,18 +38,26 @@ mkdir -p \
"$PROJECT/.specify/logs/trace-events" \ "$PROJECT/.specify/logs/trace-events" \
"$PROJECT/.specify/logs/cost" "$PROJECT/.specify/logs/cost"
python3 - "$PROJECT" <<'PY' python3 - "$PROJECT" "$BUNDLE" <<'PY'
import json import json
from pathlib import Path from pathlib import Path
import sys import sys
root = Path(sys.argv[1]) root = Path(sys.argv[1])
bundle = Path(sys.argv[2])
(root / ".casan/config.json").write_text(json.dumps({ (root / ".casan/config.json").write_text(json.dumps({
"schema_version": "21.3", "schema_version": "21.3",
"project_id": "artifact-project", "project_id": "artifact-project",
"project_name": "Artifact Project", "project_name": "Artifact Project",
"edition": "core", "edition": "core",
"enforcement_mode": "enforce", "enforcement_mode": "enforce",
"runtime_path": str(bundle),
"clients": ["vscode-copilot"],
}), encoding="utf-8")
(root / ".casan/casan-hook.py").write_text("# casan-hook.py\n", encoding="utf-8")
(root / ".casan/version.lock").write_text(json.dumps({
"harness_version": "artifact",
"harness_hash": "artifact-hash",
}), encoding="utf-8") }), encoding="utf-8")
trace_id = "artifact-run-1" trace_id = "artifact-run-1"
trace = { trace = {
@@ -100,6 +109,7 @@ PY
( (
cd "$PROJECT" cd "$PROJECT"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" readiness --refresh --json > "$WORK/readiness.json"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report latest | grep -q "view_command=casan view artifact-run-1" CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report latest | grep -q "view_command=casan view artifact-run-1"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export artifact-run-1 \ CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export artifact-run-1 \
--format html --output "$WORK/run.html" >/dev/null --format html --output "$WORK/run.html" >/dev/null
@@ -118,6 +128,13 @@ assert d["summary"]["runs"] == 1
assert d["summary"]["tokens"]["total"] is None assert d["summary"]["tokens"]["total"] is None
assert d["summary"]["cost_usd"]["estimated"] is None assert d["summary"]["cost_usd"]["estimated"] is None
PY PY
python3 - "$WORK/readiness.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["dimensions"]["core"]["status"] == "ready"
assert d["dimensions"]["domain_pipeline"]["status"] == "not_configured"
assert d["dimensions"]["provider_telemetry"]["status"] == "optional_unavailable"
PY
URL="$(CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" view artifact-run-1 --no-open)" URL="$(CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" view artifact-run-1 --no-open)"
VIEWER_STARTED=1 VIEWER_STARTED=1
@@ -30,6 +30,16 @@ class LocalReportTests(unittest.TestCase):
"enforcement_mode": "enforce", "enforcement_mode": "enforce",
"maturity": {"level": 4, "status": "assessed"}, "maturity": {"level": 4, "status": "assessed"},
}), encoding="utf-8") }), encoding="utf-8")
(self.root / ".casan" / "readiness.json").write_text(json.dumps({
"schema_version": 1,
"overall_status": "ready",
"dimensions": {
"core": {"status": "ready", "acceptance": []},
"domain_pipeline": {"status": "not_configured"},
"provider_telemetry": {"status": "optional_unavailable"},
},
"next_actions": [],
}), encoding="utf-8")
for path in ( for path in (
".specify/state", ".specify/state",
".specify/logs/trace", ".specify/logs/trace",
@@ -92,6 +102,8 @@ class LocalReportTests(unittest.TestCase):
self.assertEqual(h6["summary"]["runs"], 0) self.assertEqual(h6["summary"]["runs"], 0)
self.assertIsNone(h6["summary"]["tokens"]["total"]) self.assertIsNone(h6["summary"]["tokens"]["total"])
self.assertTrue(any(item["code"] == "METRICS_MISSING" for item in h6["findings"])) self.assertTrue(any(item["code"] == "METRICS_MISSING" for item in h6["findings"]))
profile = REPORT.project_profile(self.root)
self.assertEqual(profile["readiness"]["dimensions"]["core"]["status"], "ready")
def test_run_reconstructs_all_gates_and_redacts_sensitive_fields(self): def test_run_reconstructs_all_gates_and_redacts_sensitive_fields(self):
self.write_run() self.write_run()
@@ -160,6 +172,10 @@ class LocalReportTests(unittest.TestCase):
self.assertIn('tabindex="-1"', script) self.assertIn('tabindex="-1"', script)
self.assertIn('dossier.focus({ preventScroll: true })', script) self.assertIn('dossier.focus({ preventScroll: true })', script)
self.assertIn("dossier.scrollIntoView({", script) self.assertIn("dossier.scrollIntoView({", script)
self.assertIn("No operational KPIs are shown until evidence exists.", script)
self.assertIn("H6 begins with the first governed run.", script)
self.assertIn("No operational counters yet", script)
self.assertNotIn('"Ready for the first governed run."', script)
if __name__ == "__main__": if __name__ == "__main__":
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""Acceptance tests for CASAN's three-dimensional readiness contract."""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
MODULE = Path(__file__).resolve().parents[1] / "scripts" / "python" / "readiness.py"
SPEC = importlib.util.spec_from_file_location("casan_readiness", MODULE)
READINESS = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(READINESS)
class ReadinessTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
self.runtime = self.root / "runtime"
report = (
self.runtime / "packages" / "casan-harness" / "scripts" /
"python" / "local_report.py"
)
report.parent.mkdir(parents=True)
report.write_text("# report engine\n", encoding="utf-8")
(self.root / ".casan").mkdir()
(self.root / ".casan" / "casan-hook.py").write_text(
"# bootstrap\n", encoding="utf-8")
(self.root / ".casan" / "config.json").write_text(json.dumps({
"schema_version": "21.3",
"project_id": "commercial-core",
"edition": "core",
"enforcement_mode": "enforce",
"runtime_path": str(self.runtime),
"clients": ["codex"],
}), encoding="utf-8")
(self.root / ".casan" / "version.lock").write_text(json.dumps({
"harness_version": "1.0.5",
"harness_hash": "abc123",
}), encoding="utf-8")
(self.root / ".codex").mkdir()
(self.root / ".codex" / "hooks.json").write_text(
'{"hooks":{"SessionStart":[{"hooks":[{"command":"python3 .casan/casan-hook.py --client codex"}]}]}}',
encoding="utf-8",
)
(self.root / ".specify" / "logs").mkdir(parents=True)
(self.root / ".specify" / "state").mkdir(parents=True)
(self.root / "package.json").write_text(
'{"name":"commercial-core"}', encoding="utf-8")
def tearDown(self):
self.temporary.cleanup()
def test_core_is_ready_without_a_domain_pack_or_provider_telemetry(self):
result = READINESS.assess(self.root, client_state={
"codex": {
"verified": True,
"operational": True,
"status": "trusted",
},
})
dimensions = result["dimensions"]
self.assertEqual(result["overall_status"], "ready")
self.assertEqual(dimensions["core"]["status"], "ready")
self.assertEqual(
dimensions["domain_pipeline"]["status"], "not_configured")
self.assertEqual(
dimensions["provider_telemetry"]["status"],
"optional_unavailable",
)
self.assertEqual(
dimensions["core"]["acceptance"][5]["status"],
"optional_unavailable",
)
def test_unverified_codex_trust_is_an_action_not_a_fake_ready_state(self):
result = READINESS.assess(self.root, client_state={
"codex": {
"verified": True,
"operational": False,
"status": "modified",
},
})
self.assertEqual(result["overall_status"], "action_required")
self.assertEqual(
result["dimensions"]["core"]["acceptance"][1]["status"],
"action_required",
)
self.assertTrue(any(
action["code"] == "verify_codex_trust"
for action in result["next_actions"]
))
def test_one_healthy_route_keeps_core_operational_with_attention(self):
config_path = self.root / ".casan" / "config.json"
config = json.loads(config_path.read_text(encoding="utf-8"))
config["clients"] = ["codex", "vscode-copilot"]
config_path.write_text(json.dumps(config), encoding="utf-8")
result = READINESS.assess(self.root, client_state={
"codex": {
"verified": True,
"operational": True,
"status": "trusted",
},
"vscode-copilot": {"operational": False},
})
self.assertEqual(result["overall_status"], "ready_with_attention")
self.assertEqual(
result["dimensions"]["core"]["acceptance"][1]["status"],
"warning",
)
self.assertTrue(any(
action["code"] == "activate_vscode_copilot"
for action in result["next_actions"]
))
def test_discovery_is_casan_owned_and_never_creates_domain_inputs(self):
result = READINESS.assess(self.root, persist=True, client_state={
"codex": {
"verified": True,
"operational": True,
"status": "trusted",
},
})
self.assertEqual(result["dimensions"]["domain_pipeline"]["domain_packs"], [])
self.assertTrue((self.root / ".casan" / "discovery.json").is_file())
self.assertTrue((self.root / ".casan" / "readiness.json").is_file())
self.assertFalse((self.root / "apps").exists())
def test_domain_configuration_references_existing_project_source_only(self):
manifest = self.root / "docs" / "project.manifest.json"
manifest.parent.mkdir()
manifest.write_text(json.dumps({
"schema_version": 1,
"project_id": "commercial-core",
}), encoding="utf-8")
configured = READINESS.configure_domain(
self.root, "docs/project.manifest.json")
self.assertEqual(configured["manifest"], "docs/project.manifest.json")
result = READINESS.assess(self.root, client_state={
"codex": {
"verified": True,
"operational": True,
"status": "trusted",
},
})
self.assertEqual(
result["dimensions"]["domain_pipeline"]["status"], "configured")
self.assertEqual(
result["dimensions"]["domain_pipeline"]["selected_manifest"],
"docs/project.manifest.json",
)
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -7,7 +7,7 @@
"level": 1, "level": 1,
"package_names": ["casan-core", "casan-harness"], "package_names": ["casan-core", "casan-harness"],
"status": "implemented", "status": "implemented",
"summary": "Minimal H1-H7 production runtime: security/action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, lightweight CLI, and a zero-dependency single-project Local Assurance Viewer with H1-H7, H6, history and on-demand export. Source-only tests and Platform helpers are excluded.", "summary": "Minimal H1-H7 production runtime: security/action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, lightweight CLI, three-dimensional commercial readiness, and a zero-dependency single-project Local Assurance Viewer with H1-H7, H6, history and on-demand export. Source-only tests and Platform helpers are excluded.",
"target_users": ["governance-harness-only projects", "BJT initial adoption", "CI gate usage"], "target_users": ["governance-harness-only projects", "BJT initial adoption", "CI gate usage"],
"includes": [ "includes": [
"packages/casan-harness", "packages/casan-harness",
@@ -44,6 +44,7 @@
"docs/packaging/DOCKER_GUIDE.md", "docs/packaging/DOCKER_GUIDE.md",
"docs/packaging/CASAN_PACKAGING_PLAN.md", "docs/packaging/CASAN_PACKAGING_PLAN.md",
"docs/packaging/CORE_LOCAL_ASSURANCE_VIEWER.md", "docs/packaging/CORE_LOCAL_ASSURANCE_VIEWER.md",
"docs/packaging/COMMERCIAL_READINESS_CONTRACT.md",
"scripts/package-release.sh" "scripts/package-release.sh"
], ],
"artifact": "casan-devkit-v{VERSION}.tar.gz" "artifact": "casan-devkit-v{VERSION}.tar.gz"
+1
View File
@@ -40,6 +40,7 @@
"scripts/python/local_report.py", "scripts/python/local_report.py",
"scripts/python/local_viewer.py", "scripts/python/local_viewer.py",
"scripts/python/report_cli.py", "scripts/python/report_cli.py",
"scripts/python/readiness.py",
"scripts/python/harness_hash.py" "scripts/python/harness_hash.py"
] ]
}, },