fix template, remove okr, use casan.*
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"project_id": "AINative_OKR_CASAN4",
|
||||
"domain": "SDD OKR",
|
||||
"domain_root": "apps/okr/domain",
|
||||
"manifest": "apps/okr/domain/project.manifest.json",
|
||||
"context_roots": [
|
||||
"apps/okr/domain",
|
||||
"apps/okr/frontend",
|
||||
@@ -41,6 +42,7 @@
|
||||
"project_id": "CASAN_SERVICE_DESK",
|
||||
"domain": "IT Service Desk",
|
||||
"domain_root": "apps/service-desk/domain",
|
||||
"manifest": "apps/service-desk/domain/project.manifest.json",
|
||||
"context_roots": [
|
||||
"apps/service-desk"
|
||||
],
|
||||
|
||||
@@ -76,7 +76,12 @@ fi
|
||||
# apps/okr/domain (compat symlinks bridge the pre-move .specify paths). A different
|
||||
# app sets CASAN_DOMAIN_ROOT to its own apps/<project>/domain (Plan-06 reuse).
|
||||
if [[ -z "${CASAN_DOMAIN_ROOT:-}" ]]; then
|
||||
if [[ -d "$CASAN_APP_ROOT/apps/okr/domain" ]]; then
|
||||
if [[ -n "${CASAN_PROJECT_MANIFEST:-}${CASAN_PROJECT_ID:-}" ]] && command -v python3 >/dev/null 2>&1; then
|
||||
_casan_manifest_args=(domain-root --root "$CASAN_APP_ROOT")
|
||||
[[ -n "${CASAN_PROJECT_MANIFEST:-}" ]] && _casan_manifest_args+=(--manifest "$CASAN_PROJECT_MANIFEST")
|
||||
[[ -n "${CASAN_PROJECT_ID:-}" ]] && _casan_manifest_args+=(--project "$CASAN_PROJECT_ID")
|
||||
CASAN_DOMAIN_ROOT="$(python3 "$CASAN_HARNESS_ROOT/scripts/bash/project_manifest.py" "${_casan_manifest_args[@]}")" || return 2
|
||||
elif [[ -d "$CASAN_APP_ROOT/apps/okr/domain" ]]; then
|
||||
CASAN_DOMAIN_ROOT="$CASAN_APP_ROOT/apps/okr/domain"
|
||||
else
|
||||
# Fallback for the pre-split monolithic layout: domain data co-located in .specify.
|
||||
|
||||
@@ -6,7 +6,9 @@ set -uo pipefail
|
||||
# first because it rewrites `.specify/logs`.
|
||||
#
|
||||
# Env:
|
||||
# CASAN_CI_RUN_FRONTEND=0|1 default 1
|
||||
# CASAN_CI_RUN_PROJECT=0|1 default 1 (manifest build + test commands)
|
||||
# CASAN_CI_RUN_BACKEND=0|1 default 0 (legacy workspace override)
|
||||
# CASAN_CI_RUN_FRONTEND=0|1 default 0 (legacy workspace override)
|
||||
# CASAN_CI_RUN_CONTROL_PANEL=0|1 default 1
|
||||
# CASAN_CI_RUN_INFRA_LAB=0|1 default 0 (Docker Compose lab is optional in CI)
|
||||
# CASAN_CI_STEP_TIMEOUT_SEC default 600
|
||||
@@ -78,6 +80,7 @@ run "adversarial-harness" bash "$TESTS/adversarial-harness-tests.sh"
|
||||
run "phase1-track-a" bash "$TESTS/phase1-track-a-tests.sh"
|
||||
run "phase2-track-c" bash "$TESTS/phase2-track-c-tests.sh"
|
||||
run "phase2-sourcegen" bash "$TESTS/phase2-sourcegen-tests.sh"
|
||||
run "phase-project-shell" bash "$TESTS/phase-project-shell-tests.sh"
|
||||
run "phase3-evidence-pack" bash "$TESTS/phase3-evidence-pack-tests.sh"
|
||||
run "phase3-model-router" bash "$TESTS/phase3-model-router-tests.sh"
|
||||
run "phase-h5-approval" bash "$TESTS/phase-h5-approval-tests.sh"
|
||||
@@ -172,7 +175,15 @@ run "phase-chat-tenant" bash "$TESTS/phase-chat-tenant-tests.sh"
|
||||
run "test-integrity" python3 "$SCRIPT_DIR/test-integrity.py" verify
|
||||
run "bundle-integrity" python3 "$SCRIPT_DIR/bundle-integrity.py" verify
|
||||
|
||||
if [[ "${CASAN_CI_RUN_BACKEND:-1}" == "1" ]]; then
|
||||
if [[ "${CASAN_CI_RUN_PROJECT:-1}" == "1" ]]; then
|
||||
run "project-manifest" python3 "$SCRIPT_DIR/project_manifest.py" validate --root "$ROOT"
|
||||
run "project-build" python3 "$SCRIPT_DIR/project_manifest.py" run --root "$ROOT" --kind build
|
||||
run "project-test" python3 "$SCRIPT_DIR/project_manifest.py" run --root "$ROOT" --kind test
|
||||
else
|
||||
skip "project-build/test (CASAN_CI_RUN_PROJECT=0)"
|
||||
fi
|
||||
|
||||
if [[ "${CASAN_CI_RUN_BACKEND:-0}" == "1" ]]; then
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
run "backend-tests" npm test -w backend
|
||||
else
|
||||
@@ -182,7 +193,7 @@ else
|
||||
skip "backend-tests (CASAN_CI_RUN_BACKEND=0)"
|
||||
fi
|
||||
|
||||
if [[ "${CASAN_CI_RUN_FRONTEND:-1}" == "1" ]]; then
|
||||
if [[ "${CASAN_CI_RUN_FRONTEND:-0}" == "1" ]]; then
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
run "frontend-vitest" npm test -w frontend
|
||||
else
|
||||
|
||||
@@ -21,6 +21,8 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from project_manifest import load as load_project_manifest
|
||||
|
||||
# Plan-01: this script lives at <harness>/scripts/bash/; the harness root is two levels up.
|
||||
_HARNESS = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
|
||||
@@ -51,6 +53,17 @@ def status_rc(env_key):
|
||||
|
||||
def main():
|
||||
root, run_id, pack_dir = sys.argv[1:4]
|
||||
try:
|
||||
project = load_project_manifest(root)
|
||||
domain_root = os.path.join(root, project["domain_root"])
|
||||
requirements_path = os.path.join(root, project["requirements"])
|
||||
feature_id = project["feature"]["id"]
|
||||
except (OSError, ValueError):
|
||||
domain_root = os.environ.get("CASAN_DOMAIN_ROOT", os.path.join(root, "apps/okr/domain"))
|
||||
requirements_path = os.path.join(domain_root, "input", "requirement.md")
|
||||
if not os.path.isfile(requirements_path):
|
||||
requirements_path = os.path.join(domain_root, "input", "okr-requirement.md")
|
||||
feature_id = "casan-demo"
|
||||
os.makedirs(pack_dir, exist_ok=True)
|
||||
logs = os.path.join(root, ".specify", "logs")
|
||||
|
||||
@@ -59,7 +72,7 @@ def main():
|
||||
# H1 context
|
||||
reports["h1-context-report.json"] = {
|
||||
"harness": "H1-context", "run_id": run_id,
|
||||
"context_yaml": os.path.exists(os.path.join(root, "docs/output/output_logs/casan-demo/pipeline-context.yaml")),
|
||||
"context_yaml": os.path.exists(os.path.join(root, "docs/output/output_logs", feature_id, "pipeline-context.yaml")),
|
||||
"note": "path/artifact validation performed by context-validate.sh at run time",
|
||||
}
|
||||
|
||||
@@ -88,9 +101,9 @@ def main():
|
||||
sys.executable,
|
||||
traceability_script,
|
||||
"--requirements",
|
||||
os.path.join(root, "apps/okr/domain/input/okr-requirement.md"),
|
||||
requirements_path,
|
||||
"--map",
|
||||
os.path.join(root, "apps/okr/domain/traceability-map.json"),
|
||||
os.path.join(domain_root, "traceability-map.json"),
|
||||
"--out",
|
||||
traceability_out,
|
||||
"--gate",
|
||||
@@ -148,7 +161,7 @@ def main():
|
||||
fp = json.load(open(fp_json, encoding="utf-8"))
|
||||
except ValueError:
|
||||
fp = None
|
||||
vectors_path = os.path.join(root, "apps/okr/domain/corpus/redteam-vectors.jsonl")
|
||||
vectors_path = os.path.join(domain_root, "corpus", "redteam-vectors.jsonl")
|
||||
vectors = read_jsonl(vectors_path)
|
||||
reports["redteam-result.json"] = {
|
||||
"run_id": run_id, "vectors_defined": len(vectors),
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply an approved Goal patch with bounded paths and rollback-on-failure."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import importlib.util
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
@@ -20,6 +23,12 @@ def root() -> str:
|
||||
|
||||
ROOT = root()
|
||||
INBOX = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash", "approval-inbox.py")
|
||||
_MANIFEST_SPEC = importlib.util.spec_from_file_location(
|
||||
"casan_project_manifest",
|
||||
os.path.join(os.path.dirname(__file__), "project_manifest.py"),
|
||||
)
|
||||
PROJECT_MANIFEST = importlib.util.module_from_spec(_MANIFEST_SPEC)
|
||||
_MANIFEST_SPEC.loader.exec_module(PROJECT_MANIFEST)
|
||||
|
||||
|
||||
def now() -> str:
|
||||
@@ -62,15 +71,33 @@ def verify_approval(job: dict) -> dict:
|
||||
return proposal
|
||||
|
||||
|
||||
def verification_commands(files: list[str]) -> list[list[str]]:
|
||||
commands = [["git", "diff", "--check", "--", *files]]
|
||||
if any(path.startswith("apps/okr/frontend/") for path in files):
|
||||
commands.append(["npm", "run", "build", "-w", "@ainative-okr/frontend"])
|
||||
commands.append(["npm", "test", "-w", "@ainative-okr/frontend"])
|
||||
if any(path.startswith("apps/okr/backend/") for path in files):
|
||||
commands.append(["npm", "run", "build", "-w", "@ainative-okr/backend"])
|
||||
commands.append(["npm", "test", "-w", "@ainative-okr/backend"])
|
||||
return commands
|
||||
def _manifest_for_files(files: list[str]) -> dict:
|
||||
selected = os.environ.get("CASAN_PROJECT_MANIFEST")
|
||||
project = os.environ.get("CASAN_PROJECT_ID")
|
||||
if selected or project:
|
||||
return PROJECT_MANIFEST.load(ROOT, selected, project)
|
||||
|
||||
registry = load(os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json"))
|
||||
candidates = []
|
||||
for entry in registry.get("projects", []):
|
||||
manifest_path = entry.get("manifest")
|
||||
if not manifest_path or entry.get("status") != "active":
|
||||
continue
|
||||
try:
|
||||
manifest = PROJECT_MANIFEST.load(ROOT, manifest_path)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if any(any(path.startswith(root.rstrip("/") + "/") for root in manifest["source_roots"]) for path in files):
|
||||
candidates.append(manifest)
|
||||
if len(candidates) != 1:
|
||||
reason = "none" if not candidates else "multiple"
|
||||
raise RuntimeError(f"GOAL_APPLY_PROJECT_MANIFEST_{reason.upper()}")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def verification_commands(files: list[str], manifest: dict | None = None) -> list[list[str]]:
|
||||
project = manifest or _manifest_for_files(files)
|
||||
return [["git", "diff", "--check", "--", *files], *PROJECT_MANIFEST.verification_commands(project, files)]
|
||||
|
||||
|
||||
def execute(job_path: str, actor: str) -> dict:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$CASAN_APP_ROOT"
|
||||
MANIFEST_TOOL="$SCRIPT_DIR/project_manifest.py"
|
||||
PASS=0 FAIL=0 SKIP=0
|
||||
|
||||
manifest_args=(--root "$ROOT")
|
||||
[[ -n "${CASAN_PROJECT_MANIFEST:-}" ]] && manifest_args+=(--manifest "$CASAN_PROJECT_MANIFEST")
|
||||
[[ -n "${CASAN_PROJECT_ID:-}" ]] && manifest_args+=(--project "$CASAN_PROJECT_ID")
|
||||
|
||||
run() {
|
||||
local name="$1"; shift
|
||||
echo "==> $name"
|
||||
if "$@"; then
|
||||
echo "PROJECT_GATE_PASS $name"; PASS=$((PASS + 1))
|
||||
else
|
||||
echo "PROJECT_GATE_FAIL $name" >&2; FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
run "manifest" python3 "$MANIFEST_TOOL" validate "${manifest_args[@]}"
|
||||
if [[ "$FAIL" -ne 0 ]]; then
|
||||
echo "PROJECT_GATE_SUMMARY PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
project_id="$(python3 "$MANIFEST_TOOL" get "${manifest_args[@]}" --field project_id)"
|
||||
requirements="$(python3 "$MANIFEST_TOOL" get "${manifest_args[@]}" --field requirements)"
|
||||
architecture="$(python3 "$MANIFEST_TOOL" get "${manifest_args[@]}" --field architecture)"
|
||||
domain_root="$(python3 "$MANIFEST_TOOL" domain-root "${manifest_args[@]}")"
|
||||
evidence_dir="$ROOT/docs/output/casan/$project_id/project-gate"
|
||||
mkdir -p "$evidence_dir"
|
||||
|
||||
run "requirement-security" bash "$SCRIPT_DIR/artifact-scan.sh" "$ROOT/$requirements" "$project_id-requirements"
|
||||
run "architecture-security" bash "$SCRIPT_DIR/artifact-scan.sh" "$ROOT/$architecture" "$project_id-architecture"
|
||||
if [[ "${CASAN_PROJECT_GATE_RUN_CORPUS:-1}" == "1" ]]; then
|
||||
run "corpus-quality" env CASAN_DOMAIN_ROOT="$domain_root" bash "$SCRIPT_DIR/benign-fp-report.sh" "$evidence_dir/benign-fp-report.json"
|
||||
else
|
||||
echo "PROJECT_GATE_SKIP corpus-quality"; SKIP=$((SKIP + 1))
|
||||
fi
|
||||
run "traceability" python3 "$SCRIPT_DIR/traceability-matrix.py" \
|
||||
--requirements "$ROOT/$requirements" \
|
||||
--map "$domain_root/traceability-map.json" \
|
||||
--out "$evidence_dir/traceability-matrix.json" --gate
|
||||
|
||||
if [[ "${CASAN_PROJECT_GATE_RUN_BUILD:-1}" == "1" ]]; then
|
||||
run "build" python3 "$MANIFEST_TOOL" run "${manifest_args[@]}" --kind build
|
||||
else
|
||||
echo "PROJECT_GATE_SKIP build"; SKIP=$((SKIP + 1))
|
||||
fi
|
||||
if [[ "${CASAN_PROJECT_GATE_RUN_TEST:-1}" == "1" ]]; then
|
||||
run "test" python3 "$MANIFEST_TOOL" run "${manifest_args[@]}" --kind test
|
||||
else
|
||||
echo "PROJECT_GATE_SKIP test"; SKIP=$((SKIP + 1))
|
||||
fi
|
||||
|
||||
python3 - "$evidence_dir/summary.json" "$project_id" "$PASS" "$FAIL" "$SKIP" <<'PY'
|
||||
import json, sys
|
||||
path, project, passed, failed, skipped = sys.argv[1:]
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump({"project_id": project, "pass": int(passed), "fail": int(failed), "skip": int(skipped), "accepted": int(failed) == 0}, handle, indent=2)
|
||||
handle.write("\n")
|
||||
PY
|
||||
|
||||
echo "PROJECT_GATE_SUMMARY project=$project_id PASS=$PASS FAIL=$FAIL SKIP=$SKIP evidence=$evidence_dir"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Strict, dependency-free reader for the CASAN project manifest contract."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ManifestError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _fail(message: str) -> None:
|
||||
raise ManifestError(f"CASAN_PROJECT_MANIFEST_INVALID: {message}")
|
||||
|
||||
|
||||
def safe_relative(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value or os.path.isabs(value):
|
||||
_fail(f"{label} must be a non-empty repository-relative POSIX path")
|
||||
normalized = os.path.normpath(value).replace(os.sep, "/")
|
||||
if normalized == ".." or normalized.startswith("../") or "/../" in normalized:
|
||||
_fail(f"{label} escapes the repository root")
|
||||
return normalized.removeprefix("./")
|
||||
|
||||
|
||||
def within(root: str, value: object, label: str, must_exist: bool = True) -> str:
|
||||
relative = safe_relative(value, label)
|
||||
root_real = os.path.realpath(root)
|
||||
candidate = os.path.realpath(os.path.join(root_real, relative)) if must_exist else os.path.abspath(os.path.join(root_real, relative))
|
||||
if os.path.commonpath([root_real, candidate]) != root_real:
|
||||
_fail(f"{label} resolves outside the repository root")
|
||||
if must_exist and not os.path.exists(candidate):
|
||||
_fail(f"{label} does not exist: {relative}")
|
||||
return candidate
|
||||
|
||||
|
||||
def _json(path: str, label: str) -> dict:
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
_fail(f"{label} is not valid JSON: {error}")
|
||||
if not isinstance(value, dict):
|
||||
_fail(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _commands(value: object, label: str, allowed: set[str]) -> list[list[str]]:
|
||||
if not isinstance(value, list):
|
||||
_fail(f"{label} must be an array")
|
||||
result = []
|
||||
for index, command in enumerate(value):
|
||||
if not isinstance(command, list) or not command or any(not isinstance(part, str) or not part for part in command):
|
||||
_fail(f"{label}[{index}] must be a non-empty argv array")
|
||||
if command[0] not in allowed:
|
||||
_fail(f"{label}[{index}] executable is not allowed: {command[0]}")
|
||||
result.append(command.copy())
|
||||
return result
|
||||
|
||||
|
||||
def load(root: str, manifest_path: str | None = None, project_id: str | None = None) -> dict:
|
||||
root = os.path.realpath(root)
|
||||
selected = manifest_path or os.environ.get("CASAN_PROJECT_MANIFEST")
|
||||
requested = project_id or os.environ.get("CASAN_PROJECT_ID")
|
||||
if selected:
|
||||
path = within(root, selected, "manifest")
|
||||
elif requested:
|
||||
registry = _json(os.path.join(root, "packages/casan-harness/level5/project-registry.json"), "project registry")
|
||||
entry = next((item for item in registry.get("projects", []) if item.get("project_id") == requested), None)
|
||||
if not entry:
|
||||
_fail(f"project is not registered: {requested}")
|
||||
selected = entry.get("manifest") or f"{safe_relative(entry.get('domain_root'), 'registry domain_root')}/project.manifest.json"
|
||||
path = within(root, selected, "registered manifest")
|
||||
else:
|
||||
path = within(root, "apps/okr/domain/project.manifest.json", "default manifest")
|
||||
|
||||
raw = _json(path, "project manifest")
|
||||
if raw.get("schema_version") != 1:
|
||||
_fail("schema_version must be 1")
|
||||
if not re.fullmatch(r"[a-z][a-z0-9-]{1,62}", str(raw.get("project_id", ""))):
|
||||
_fail("project_id must be a lowercase slug")
|
||||
feature = raw.get("feature")
|
||||
if not isinstance(feature, dict) or not re.fullmatch(r"[0-9]{3}-[a-z0-9-]+", str(feature.get("id", ""))):
|
||||
_fail("feature.id must match NNN-slug")
|
||||
if not re.fullmatch(r"MOD-[0-9]{2,}", str(feature.get("module_id", ""))):
|
||||
_fail("feature.module_id must match MOD-NN")
|
||||
|
||||
for key in ("domain_root", "requirements", "architecture", "quality_profile"):
|
||||
raw[key] = safe_relative(raw.get(key), key)
|
||||
within(root, raw[key], key)
|
||||
raw["artifacts_root"] = safe_relative(raw.get("artifacts_root", "docs/output"), "artifacts_root")
|
||||
|
||||
source_roots = raw.get("source_roots")
|
||||
if not isinstance(source_roots, list) or not source_roots:
|
||||
_fail("source_roots must contain at least one path")
|
||||
raw["source_roots"] = list(dict.fromkeys(safe_relative(item, "source_roots") for item in source_roots))
|
||||
for item in raw["source_roots"]:
|
||||
within(root, item, "source_root")
|
||||
|
||||
profile = _json(within(root, raw["quality_profile"], "quality_profile"), "quality profile")
|
||||
allowed = set(profile.get("allowed_command_executables", []))
|
||||
if profile.get("schema_version") != 1 or not allowed:
|
||||
_fail("quality profile version or command allowlist is invalid")
|
||||
raw["quality"] = profile
|
||||
raw["commands"] = {
|
||||
"build": _commands(raw.get("commands", {}).get("build"), "commands.build", allowed),
|
||||
"test": _commands(raw.get("commands", {}).get("test"), "commands.test", allowed),
|
||||
}
|
||||
if profile.get("require_build_commands") and not raw["commands"]["build"]:
|
||||
_fail("build commands are required by the quality profile")
|
||||
if profile.get("require_test_commands") and not raw["commands"]["test"]:
|
||||
_fail("test commands are required by the quality profile")
|
||||
|
||||
verification = raw.get("verification")
|
||||
if not isinstance(verification, list) or (profile.get("require_verification_mapping") and not verification):
|
||||
_fail("verification mapping is required")
|
||||
normalized_rules = []
|
||||
for index, rule in enumerate(verification):
|
||||
if not isinstance(rule, dict):
|
||||
_fail(f"verification[{index}] must be an object")
|
||||
prefix = safe_relative(rule.get("path_prefix"), f"verification[{index}].path_prefix").rstrip("/") + "/"
|
||||
normalized_rules.append({"path_prefix": prefix, "commands": _commands(rule.get("commands"), f"verification[{index}].commands", allowed)})
|
||||
raw["verification"] = normalized_rules
|
||||
if profile.get("fail_on_unmapped_source_root"):
|
||||
for source in raw["source_roots"]:
|
||||
prefix = source.rstrip("/") + "/"
|
||||
if not any(prefix.startswith(rule["path_prefix"]) or rule["path_prefix"].startswith(prefix) for rule in normalized_rules):
|
||||
_fail(f"source root has no verification rule: {source}")
|
||||
|
||||
raw["manifest_path"] = os.path.relpath(path, root).replace(os.sep, "/")
|
||||
raw["root"] = root
|
||||
return raw
|
||||
|
||||
|
||||
def verification_commands(manifest: dict, files: list[str]) -> list[list[str]]:
|
||||
commands: list[list[str]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
for rule in manifest["verification"]:
|
||||
if any(safe_relative(path, "changed file").startswith(rule["path_prefix"]) for path in files):
|
||||
for command in rule["commands"]:
|
||||
key = tuple(command)
|
||||
if key not in seen:
|
||||
commands.append(command.copy())
|
||||
seen.add(key)
|
||||
return commands
|
||||
|
||||
|
||||
def run_commands(manifest: dict, kind: str) -> int:
|
||||
if kind not in {"build", "test"}:
|
||||
_fail(f"unsupported command kind: {kind}")
|
||||
for command in manifest["commands"][kind]:
|
||||
print(f"CASAN_PROJECT_COMMAND kind={kind} argv={json.dumps(command, ensure_ascii=False)}", flush=True)
|
||||
timeout = int(os.environ.get("CASAN_PROJECT_COMMAND_TIMEOUT_SEC", "600"))
|
||||
try:
|
||||
result = subprocess.run(command, cwd=manifest["root"], check=False, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"CASAN_PROJECT_COMMAND_TIMEOUT kind={kind} seconds={timeout}", file=sys.stderr)
|
||||
return 124
|
||||
if result.returncode != 0:
|
||||
print(f"CASAN_PROJECT_COMMAND_FAILED kind={kind} rc={result.returncode}", file=sys.stderr)
|
||||
return result.returncode
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=["validate", "get", "domain-root", "run", "verification"])
|
||||
parser.add_argument("--root", default=os.getcwd())
|
||||
parser.add_argument("--manifest")
|
||||
parser.add_argument("--project")
|
||||
parser.add_argument("--kind", choices=["build", "test"])
|
||||
parser.add_argument("--file", action="append", default=[])
|
||||
parser.add_argument("--field", choices=["project_id", "requirements", "architecture", "domain_root", "quality_profile"])
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
manifest = load(args.root, args.manifest, args.project)
|
||||
if args.action == "domain-root":
|
||||
print(os.path.join(manifest["root"], manifest["domain_root"]))
|
||||
return 0
|
||||
if args.action == "get":
|
||||
if not args.field:
|
||||
_fail("--field is required for get")
|
||||
print(manifest[args.field])
|
||||
return 0
|
||||
if args.action == "validate":
|
||||
print(json.dumps({
|
||||
"status": "valid",
|
||||
"project_id": manifest["project_id"],
|
||||
"manifest": manifest["manifest_path"],
|
||||
"quality_profile": manifest["quality"]["profile_id"],
|
||||
"build_commands": len(manifest["commands"]["build"]),
|
||||
"test_commands": len(manifest["commands"]["test"]),
|
||||
"verification_rules": len(manifest["verification"]),
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
if args.action == "run":
|
||||
if not args.kind:
|
||||
_fail("--kind is required for run")
|
||||
return run_commands(manifest, args.kind)
|
||||
print(json.dumps(verification_commands(manifest, args.file), ensure_ascii=False))
|
||||
return 0
|
||||
except (ManifestError, OSError, ValueError) as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -110,9 +110,14 @@ def resolve_files(root: str, values):
|
||||
|
||||
def main() -> int:
|
||||
root = project_root()
|
||||
domain = os.environ.get("CASAN_DOMAIN_ROOT", os.path.join(root, "apps/okr/domain"))
|
||||
if not os.path.isabs(domain):
|
||||
domain = os.path.join(root, domain)
|
||||
generic_requirement = os.path.join(domain, "input", "requirement.md")
|
||||
legacy_requirement = os.path.join(domain, "input", "okr-requirement.md")
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--requirements", default=os.path.join(root, "apps/okr/domain/input/okr-requirement.md"))
|
||||
ap.add_argument("--map", default=os.path.join(root, "apps/okr/domain/traceability-map.json"))
|
||||
ap.add_argument("--requirements", default=generic_requirement if os.path.isfile(generic_requirement) else legacy_requirement)
|
||||
ap.add_argument("--map", default=os.path.join(domain, "traceability-map.json"))
|
||||
ap.add_argument("--out", default=os.path.join(root, "docs/output/casan/traceability-matrix.json"))
|
||||
ap.add_argument("--gate", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
@@ -24,19 +24,19 @@ OUT = ROOT / "docs" / "output" / "output_logs" / "casan-demo" / "pipeline-contex
|
||||
|
||||
steps = [
|
||||
("step-0", "detect-existing-spec", "agent_step"),
|
||||
("step-1-srs", "okr.srs", "agent_step"),
|
||||
("step-2-bd", "okr.bd", "agent_step"),
|
||||
("step-1-srs", "casan.srs", "agent_step"),
|
||||
("step-2-bd", "casan.bd", "agent_step"),
|
||||
("step-3-spec", "speckit.specify", "agent_step"),
|
||||
("step-4-clarify", "speckit.clarify", "agent_step"),
|
||||
("step-5-review-spec", "okr.reviewspec", "agent_step"),
|
||||
("step-5-review-spec", "casan.reviewspec", "agent_step"),
|
||||
("step-6-plan", "speckit.plan", "agent_step"),
|
||||
("step-7-review-plan", "okr.reviewplan", "agent_step"),
|
||||
("step-8-dd", "okr.dd", "agent_step"),
|
||||
("step-8b-testcases", "okr.testkit.gen-testcases", "agent_step"),
|
||||
("step-7-review-plan", "casan.reviewplan", "agent_step"),
|
||||
("step-8-dd", "casan.dd", "agent_step"),
|
||||
("step-8b-testcases", "casan.testkit.gen-testcases", "agent_step"),
|
||||
("step-9-tasks", "speckit.tasks", "agent_step"),
|
||||
("step-10-implement", "speckit.implement", "write_code"),
|
||||
("step-11-review-code", "okr.reviewcode", "agent_step"),
|
||||
("step-12-testkit", "okr.testkit.run-tests", "agent_step"),
|
||||
("step-11-review-code", "casan.reviewcode", "agent_step"),
|
||||
("step-12-testkit", "casan.testkit.run-tests", "agent_step"),
|
||||
("step-13-launch", "boss.launch", "deploy"),
|
||||
]
|
||||
|
||||
|
||||
@@ -202,5 +202,11 @@ class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
self.assertIn(["npm", "run", "build", "-w", "@ainative-okr/frontend"], commands)
|
||||
self.assertIn(["npm", "test", "-w", "@ainative-okr/frontend"], commands)
|
||||
|
||||
def test_service_desk_patch_uses_service_desk_manifest_commands(self):
|
||||
commands = EXECUTOR.verification_commands(["apps/service-desk/src/ticket.js"])
|
||||
self.assertIn(["node", "--check", "apps/service-desk/src/ticket.js"], commands)
|
||||
self.assertIn(["node", "--test", "apps/service-desk/test/ticket.test.mjs"], commands)
|
||||
self.assertNotIn(["npm", "test", "-w", "@ainative-okr/backend"], commands)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
|
||||
python3 "$CASAN_APP_ROOT/packages/casan-devkit/tests/project-scaffold-tests.py"
|
||||
python3 "$CASAN_HARNESS_ROOT/scripts/bash/project_manifest.py" validate \
|
||||
--root "$CASAN_APP_ROOT" --manifest apps/okr/domain/project.manifest.json >/dev/null
|
||||
python3 "$CASAN_HARNESS_ROOT/scripts/bash/project_manifest.py" validate \
|
||||
--root "$CASAN_APP_ROOT" --manifest apps/service-desk/domain/project.manifest.json >/dev/null
|
||||
node --check "$CASAN_APP_ROOT/scripts/casan-project.mjs"
|
||||
node --check "$CASAN_APP_ROOT/scripts/casan-step.mjs"
|
||||
node --check "$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs"
|
||||
echo "PROJECT_SHELL_TESTS_PASS"
|
||||
Reference in New Issue
Block a user