Standard production layout: the OKR app (was nested under AINative_OKR_CASAN5/) is now
the repository root. No more wrapper directory.
- Promote AINative_OKR_CASAN5/* -> repo root (backend/ frontend/ packages/ apps/
.specify/ docs/ infra/ nginx/ scripts/ + configs). Merge tool dirs: .gitea (kept the
active deploy ci.yml, added harness-ci.yml + runbooks), .claude (agents/commands +
launch.json), .github moved up.
- Remove redundant: 00_SUBMISSION_PACKAGE, scattered root notes (FPT_CASAN_Full.md,
tu-tuong-casan.md, casan-tu-sinh..., casan_harness_assessment.md, source-review...,
README_CASAN5_REFINED.md), casan-next-plans/ and optimize-docs/ (competition/planning
artifacts — roadmap + design history preserved in git log / commit messages).
- Update all references to the old layout:
- .gitea/workflows/{ci,harness-ci}.yml, .github/workflows/{ci,deploy}.yml:
working-directory .; drop AINative_OKR_CASAN5/ prefix; .specify/{tests,scripts}
-> packages/casan-harness/... (.specify/logs state kept)
- .claude/launch.json, .gitea/*-runbook.md: path prefixes
- CLAUDE.md, README.md: docs/input -> apps/okr/domain/input
- policy-bundle.yaml: 8 policy paths -> packages/casan-harness/...; manifest re-signed
- secrets-scan.sh: fixture excludes -> new package/domain paths.
Full gate from the new root: PASS=64 FAIL=0 SKIP=3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.0 KiB
Python
69 lines
2.0 KiB
Python
#!/usr/bin/env python3
|
|
import json
|
|
import os
|
|
import sys
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
MODE = sys.argv[1] if len(sys.argv) > 1 else "alert"
|
|
PORT = int(os.environ.get("CASAN_MOCK_PORT", "8080"))
|
|
EVENTS = []
|
|
|
|
|
|
USAGE = [
|
|
{
|
|
"provider": "local-prod-mock",
|
|
"model": "billing-api-emulator",
|
|
"run_id": "infra-lab-run",
|
|
"step": "speckit.implement",
|
|
"input_tokens": 1842,
|
|
"output_tokens": 936,
|
|
"total_tokens": 2778,
|
|
"cost_usd": 0.08334,
|
|
"latency_ms": 4210,
|
|
"status": "success",
|
|
}
|
|
]
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def send_json(self, code, payload):
|
|
body = json.dumps(payload).encode()
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def do_GET(self): # noqa: N802
|
|
path = urlparse(self.path).path
|
|
if path == "/healthz":
|
|
self.send_json(200, {"status": "ok", "mode": MODE, "events": len(EVENTS)})
|
|
elif MODE == "alert" and path == "/events":
|
|
self.send_json(200, EVENTS)
|
|
elif MODE == "billing" and path in ("/usage", "/v1/usage"):
|
|
self.send_json(200, USAGE)
|
|
else:
|
|
self.send_json(404, {"error": "not_found"})
|
|
|
|
def do_POST(self): # noqa: N802
|
|
if MODE != "alert":
|
|
self.send_json(404, {"error": "not_found"})
|
|
return
|
|
size = int(self.headers.get("Content-Length", "0"))
|
|
raw = self.rfile.read(size)
|
|
try:
|
|
payload = json.loads(raw or b"{}")
|
|
except json.JSONDecodeError:
|
|
payload = {"raw": raw.decode(errors="replace")}
|
|
EVENTS.append(payload)
|
|
self.send_json(200, {"ok": True, "events": len(EVENTS)})
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|