feat: make Core reports commercially production-ready
@@ -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 |
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||