This commit is contained in:
valenti
2026-09-08 07:21:34 -04:00
commit a4b224da8e
169 changed files with 27483 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""Seed source_file + code_smell artifacts per Code Analysis page."""
import json
import urllib.request
import urllib.error
HEADERS = {
"Content-Type": "application/json",
"X-ASF-Tenant-Id": "t-demo-003",
"X-ASF-User-Id": "u-mario",
}
BASE = "http://localhost:8000/api/v1"
def req(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(
BASE + path,
data=data,
headers=HEADERS,
method=method,
)
try:
with urllib.request.urlopen(r, timeout=15) as resp:
return resp.status, json.loads(resp.read().decode() or "null")
except urllib.error.HTTPError as e:
txt = e.read().decode()
try:
payload = json.loads(txt)
except Exception:
payload = {"raw": txt}
return e.code, payload
if __name__ == "__main__":
# 1. list projects
code, projects = req("GET", "/projects?limit=10")
assert code == 200, f"projects {code} {projects}"
pid = projects[0]["id"]
print(f"project_id={pid} name={projects[0]['name']}")
# 2. list existing to dedupe by external_id
code, arts = req("GET", "/artifacts?limit=200")
existing_external = {
a["external_id"] for a in arts if a.get("external_id")
}
existing_types = {
(a["type"], a["title"]) for a in arts
}
print(f"existing artifacts total: {len(arts)} ext_ids: {len(existing_external)} type/titles: {len(existing_types)}")
SOURCE_FILES = [
{
"external_id": "SRC-CORE-MW",
"type": "source_file",
"title": "asf-core/app/middleware/correlation.py",
"status": "active",
"source": "git",
"content": (
"\"\"\"Tenant & correlation middleware.\n\n"
"Adds X-ASF-Correlation-Id and coerces X-ASF-Tenant-Id to UUID5.\n\"\"\"\n"
"import uuid\nfrom fastapi import Request, Response\nfrom starlette.middleware.base import BaseHTTPMiddleware\n\n"
"TENANT_NS = uuid.UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8')\n\n"
"def _coerce_uuid(val):\n"
" try:\n"
" return uuid.UUID(val)\n"
" except Exception:\n"
" return uuid.uuid5(TENANT_NS, val or 'default')\n\n"
"class CorrelationMiddleware(BaseHTTPMiddleware):\n"
" async def dispatch(self, request: Request, call_next):\n"
" cid = request.headers.get('X-ASF-Correlation-Id') or str(uuid.uuid4())\n"
" request.state.correlation_id = cid\n"
" tid = _coerce_uuid(request.headers.get('X-ASF-Tenant-Id'))\n"
" request.state.tenant_id = tid\n"
" response: Response = await call_next(request)\n"
" response.headers['X-ASF-Correlation-Id'] = cid\n"
" return response\n"
),
"metadata": {
"language": "python",
"path": "services/asf-core/app/middleware/correlation.py",
"repo": "git@github.com:azurian/asf.git",
"branch": "main",
"loc": 36,
"lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36],
"commit_sha": "a1f22c33",
},
},
{
"external_id": "SRC-WEB-REQ",
"type": "source_file",
"title": "asf-web/app/requirements/page.tsx",
"status": "active",
"source": "git",
"content": (
"\"use client\";\n"
"import React, { useMemo, useState } from \"react\";\n"
"import { useQuery } from \"@tanstack/react-query\";\n"
"import { ClipboardList, Search } from \"lucide-react\";\n"
"import { artifacts, projects } from \"@/lib/api\";\n"
"import { Card, CardBody, CardHeader, CardTitle } from \"@/components/ui/Card\";\n"
"\n"
"export default function RequirementsPage() {\n"
" const [projectFilter, setProjectFilter] = useState('all');\n"
" const [statusFilter, setStatusFilter] = useState('all');\n"
" const projectsQ = useQuery({ queryKey: ['req-projects'], queryFn: () => projects.list({ limit: 50 }) });\n"
" const listQ = useQuery({\n"
" queryKey: ['req-list', projectFilter, statusFilter],\n"
" queryFn: () => artifacts.list({\n"
" status: statusFilter !== 'all' ? statusFilter : undefined,\n"
" project_id: projectFilter !== 'all' ? projectFilter : undefined,\n"
" type: 'requirement',\n"
" limit: 200,\n"
" }),\n"
" });\n"
" return (<Card>...</Card>);\n"
"}\n"
),
"metadata": {
"language": "typescript",
"path": "frontend/asf-web/app/requirements/page.tsx",
"repo": "git@github.com:azurian/asf.git",
"branch": "main",
"loc": 394,
"framework": "nextjs14",
"commit_sha": "a1f22c33",
},
},
{
"external_id": "SRC-WORKER-CB",
"type": "source_file",
"title": "asf-worker/app/worker/consumer.py",
"status": "active",
"source": "git",
"content": (
"\"\"\"RabbitMQ consumer factory + sync callbacks for DIRECT job queues.\"\"\"\n"
"import asyncio\nimport structlog\nfrom aio_pika import IncomingMessage\n\n"
"log = structlog.get_logger(__name__)\n\n"
"def _make_consumer_callback(handler):\n"
" def _cb(message: IncomingMessage):\n"
" loop = asyncio.new_event_loop()\n"
" try:\n"
" loop.run_until_complete(handler(message))\n"
" except Exception as exc:\n"
" log.exception('consumer_handler_error', err=exc)\n"
" finally:\n"
" loop.close()\n"
" return _cb\n"
),
"metadata": {
"language": "python",
"path": "services/asf-worker/app/worker/consumer.py",
"repo": "git@github.com:azurian/asf.git",
"branch": "main",
"loc": 18,
"commit_sha": "b4812a01",
},
},
]
# code smells stored as type=ai_analysis with metadata.kind = "code_smell"
# (to keep ArtifactType enum closed — no tests break)
SMELLS = [
{
"external_id": "CS-001",
"type": "ai_analysis",
"title": "Bare except clause in CorrelationMiddleware.dispatch",
"status": "open",
"source": "ruff:EM102",
"content": (
"Rule Ruff E722 — bare `except Exception` without narrow clause may swallow "
"cancellation errors unrelated to UUID parse. Prefer explicit ValueError/AttributeError "
"or log.exception with correlation_id bound.\n"
"Lines affected: 14-16."
),
"metadata": {
"kind": "code_smell",
"language": "python",
"severity": "medium",
"rule_id": "E722",
"engine": "ruff 0.5.0",
"path": "services/asf-core/app/middleware/correlation.py",
"lines": [14, 15, 16],
"fingerprint": "corr_mw_except_001",
"confidence_pct": 98,
},
},
{
"external_id": "CS-002",
"type": "ai_analysis",
"title": "Missing N+1 query guard in requirements listQ (no select limit on projects)",
"status": "open",
"source": "eslint:perf-nplus1",
"content": (
"Requirements page fetches all 50 projects and maps via projectsQ even when a single project filter is selected. "
"Consider deriving projectNameById lazily and paginating projects lookup.\n"
"Affected page.tsx lines 78-89."
),
"metadata": {
"kind": "code_smell",
"language": "typescript",
"severity": "low",
"rule_id": "asf-perf-003",
"engine": "eslint custom",
"path": "frontend/asf-web/app/requirements/page.tsx",
"lines": [78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89],
"fingerprint": "req_list_nplus1_002",
"confidence_pct": 75,
},
},
{
"external_id": "CS-003",
"type": "ai_analysis",
"title": "Coroutine leak risk: event loop not shared re-created per message",
"status": "open",
"source": "pytype:event-loop",
"content": (
"Worker consumer factory _cb spawns asyncio.new_event_loop() per IncomingMessage. "
"Long-lived handlers under high volume may accumulate GC pressure from leftover loop objects. "
"Recommendation: pass the shared running event loop to handler() via q.consume() and make handler truly async.\n"
"Severity HIGH — affects every DIRECT job 24/7."
),
"metadata": {
"kind": "code_smell",
"language": "python",
"severity": "high",
"rule_id": "ASF-SEC-203",
"engine": "pytype + manual review",
"path": "services/asf-worker/app/worker/consumer.py",
"lines": [8, 9, 10, 11, 12, 13, 14, 15, 16],
"fingerprint": "worker_loop_per_msg_003",
"confidence_pct": 88,
"cwe": "CWE-772",
},
},
]
created = 0
for body in SOURCE_FILES + SMELLS:
if (body["type"], body["title"]) in existing_types or body.get("external_id") in existing_external:
print(f" SKIP exists ext_id={body.get('external_id')} type={body['type']} title={body['title'][:60]}")
continue
body_full = {"project_id": pid, **body}
code, resp = req("POST", "/artifacts", body_full)
if code in (201, 200):
created += 1
print(f" OK [{code}] type={body['type']} id={str(resp['id'])[:8]} title={body['title'][:60]}")
else:
print(f" FAIL [{code}] type={body['type']} body={resp}")
print(f"\nDONE: created={created} new code artifacts")
# print summary counts per type
code, arts2 = req("GET", "/artifacts?limit=200")
from collections import Counter
cnt = Counter(a["type"] for a in arts2)
for t, c in cnt.items():
print(f" count {t}: {c}")
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env bash
#
# seed-demo.sh — Popola ASF con i dati DEMO del tenant t-demo-003.
#
# Sequenza (idempotente dal lato API - POST ritorna 201 per nuovi artifact,
# ma se seed precedenti esistono già verrà creato duplicato perché non abbiamo
# un controllo unique ext_id in questa fase MVP. Per sicurezza cancella TUTTI
# artifacts + jobs prima di ricreare quando si specifica --clean.
#
# Usage:
# ./scripts/seed-demo.sh # seeds normali (3 blocchi trace + code + extra)
# ./scripts/seed-demo.sh --clean # PURGA artifacts + jobs t-demo-003 POI seed
# ./scripts/seed-demo.sh --jobs # + 5 job echo demo in coda (devops/testing/...)
#
# Richiede:
# • ASF Core attivo su http://localhost:8000 (scripts/start-stack.sh)
# • Python .venv attivo o eseguibile in /root/ASF/.venv/bin/python
# • Worker attivo se usi --jobs (altrimenti jobs stanno in QUEUED)
#
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PY="$ROOT/.venv/bin/python"
CORE_URL="${ASF_CORE_URL:-http://localhost:8000}"
TENANT="${X_ASF_TENANT_ID:-t-demo-003}"
USER="${X_ASF_USER_ID:-u-mario}"
CLEAN=0
JOBS=0
for arg in "$@"; do
case "$arg" in
--clean) CLEAN=1 ;;
--jobs) JOBS=1 ;;
-h|--help)
sed -n '2,26p' "$0"
exit 0 ;;
esac
done
log() { printf " \033[1;34m→\033[0m %s\n" "$*"; }
ok() { printf " \033[1;32m✔\033[0m %s\n" "$*"; }
fail() { printf " \033[1;31m✘\033[0m %s\n" "$*" >&2; exit 1; }
curl_h() {
# $1 = method, $2 = path, stdin optional body. Prints status + JSON body.
local m="$1" p="$2"
shift 2
curl -s -X "$m" -H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
-H "Content-Type: application/json" \
"${CORE_URL}/api/v1${p}" "$@"
}
# --- Sanity check: Core UP ---
log "ASF Core health check su $CORE_URL"
H=$(curl -s -o /dev/null -w "%{http_code}" "${CORE_URL}/api/v1/health" || true)
if [[ "$H" != "200" ]]; then
fail "ASF Core non risponde (HTTP $H). Avvia con: bash scripts/start-stack.sh"
fi
ok "ASF Core UP (health=$H)"
# --- Cleanup (opzionale) ---
if [[ $CLEAN == 1 ]]; then
log "Cleanup artifacts + jobs per tenant $TENANT (DELETE /artifacts e /jobs listati)"
# MVP: asf-core non espone DELETE /artifacts bulk. Useremo GET lista + DELETE /artifacts/{id}.
A_IDS=$(curl_h GET "/artifacts?limit=500" | "$PY" -c '
import json,sys
d=json.load(sys.stdin)
for a in d: print(a["id"])
')
N_A=$(echo -n "$A_IDS" | grep -c . || true)
log " DELETE $N_A artifacts..."
if [[ -n "$A_IDS" ]]; then
while read -r id; do
curl -s -o /dev/null -X DELETE \
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
"${CORE_URL}/api/v1/artifacts/${id}" || true
done <<< "$A_IDS"
fi
J_IDS=$(curl_h GET "/jobs?limit=500" | "$PY" -c '
import json,sys
d=json.load(sys.stdin)
for j in d: print(j["id"])
' 2>/dev/null || true)
N_J=$(echo -n "$J_IDS" | grep -c . || true)
log " PATCH CANCEL $N_J jobs..."
if [[ -n "$J_IDS" ]]; then
while read -r id; do
curl -s -o /dev/null -X PATCH \
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
-H "Content-Type: application/json" \
-d '{"status":"cancelled"}' \
"${CORE_URL}/api/v1/jobs/${id}" || true
done <<< "$J_IDS"
fi
ok "Cleanup completato"
fi
# --- Seeds ---
log "Seed 1/4: Traceability artifacts (REQ → TASK → PR → TEST/BUILD → DEPLOY)"
"$PY" "$ROOT/scripts/seed-traceability.py" >/tmp/seed1.log 2>&1 \
|| fail "seed-traceability fallito. Log: /tmp/seed1.log"
ok " OK ($(wc -l < /tmp/seed1.log) righe log)"
log "Seed 2/4: Relazioni traceability (5 edges: implements/verifies/deployed_as/generated_from)"
"$PY" "$ROOT/scripts/seed-rels-only.py" >/tmp/seed2.log 2>&1 \
|| fail "seed-rels fallito. Log: /tmp/seed2.log"
ok " OK ($(wc -l < /tmp/seed2.log) righe log)"
log "Seed 3/4: Source files + Code smells (3 files, 3 smells)"
"$PY" "$ROOT/scripts/seed-code-artifacts.py" >/tmp/seed3.log 2>&1 \
|| fail "seed-code-artifacts fallito. Log: /tmp/seed3.log"
ok " OK ($(wc -l < /tmp/seed3.log) righe log)"
log "Seed 4/4: Extra DOC / COMMIT / SEC / INC + 4 relazioni (densify graph)"
"$PY" "$ROOT/scripts/seed-extra-rels.py" >/tmp/seed4.log 2>&1 \
|| fail "seed-extra-rels fallito. Log: /tmp/seed4.log"
ok " OK ($(wc -l < /tmp/seed4.log) righe log)"
# --- Jobs demo (opzionale) ---
if [[ $JOBS == 1 ]]; then
log "Enqueue 5 demo jobs (echo handler DEVOPS/TESTING/...)"
OUT=$(curl -s -X POST \
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
-H "Content-Type: application/json" \
--data-raw '{
"jobs": [
{"type":"devops","priority":"normal","payload":{"name":"deploy-dev","env":"dev"}},
{"type":"testing","priority":"high","payload":{"name":"smoke-tests","framework":"pytest"}},
{"type":"devops","priority":"normal","payload":{"name":"build-image","image":"asf/app:latest"}},
{"type":"notifications","priority":"low","payload":{"channel":"email","to":"admins"}},
{"type":"indexing","priority":"normal","payload":{"source":"github/asf","branch":"main"}}
]
}' "${CORE_URL}/api/v1/jobs/batch" 2>&1 || echo "fallback")
# Fallback per /batch non esistente (MVP usa singolo POST per job)
if [[ "$OUT" == *"fallback"* || "$OUT" == *"Not Found"* || "$OUT" == *"404"* || "$OUT" == *"405"* ]]; then
log " /batch non disponibile → 5 POST singoli:"
TYPES=("devops" "testing" "devops" "notifications" "indexing")
PAYLOADS=(
'{"name":"deploy-dev","env":"dev"}'
'{"name":"smoke-tests","framework":"pytest"}'
'{"name":"build-image","image":"asf/app:latest"}'
'{"channel":"email","to":"admins"}'
'{"source":"github/asf","branch":"main"}'
)
PRIO=("normal" "high" "normal" "low" "normal")
for i in 0 1 2 3 4; do
JS=$(curl -s -X POST \
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
-H "Content-Type: application/json" \
-d "{\"type\":\"${TYPES[$i]}\",\"priority\":\"${PRIO[$i]}\",\"payload\":${PAYLOADS[$i]}}" \
"${CORE_URL}/api/v1/jobs/submit")
echo -n "$JS" | "$PY" -c 'import json,sys; j=json.load(sys.stdin); print(" - job id=", j.get("id","?")[:8]+"…", "accepted=", j.get("accepted"))' 2>/dev/null
done
else
ok " /batch accettato"
fi
ok "5 demo jobs accodati (avvia asf-worker per completarli)"
fi
# --- Report finale ---
log "Report finale tenant $TENANT:"
echo ""
curl_h GET "/artifacts?limit=500" | "$PY" - <<'PY'
import json,sys
from collections import Counter
d=json.load(sys.stdin)
c=Counter(a.get("type","?") for a in d)
print(" Artifacts totali:", len(d))
for k,v in sorted(c.items(), key=lambda x:-x[1]):
print(f" - {k:18s} {v}")
PY
echo ""
curl_h GET "/traceability/graph" 2>/dev/null | "$PY" -c '
import json,sys
g=json.load(sys.stdin)
print(f" Traceability graph: nodes={len(g.get(\"nodes\",[]))} edges={len(g.get(\"edges\",[]))}")
' 2>/dev/null || true
echo ""
curl_h GET "/jobs" 2>/dev/null | "$PY" - <<'PY'
import json,sys
from collections import Counter
d=json.load(sys.stdin)
c=Counter(j.get("status","?") for j in d)
print(" Jobs totali:", len(d))
for k,v in sorted(c.items(), key=lambda x:-x[1]):
print(f" - {k:10s} {v}")
PY
echo ""
ok "SEED DEMO completato."
echo ""
echo " Suggerimenti:"
echo " • Apri http://localhost:3000 per vedere UI (hard refresh Ctrl+Shift+R se serve)"
echo " • bash scripts/status-stack.sh per stato core/web/worker"
echo " • bash scripts/seed-demo.sh --clean --jobs per ricreare TUTTO da zero"
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""POST extra artifacts + 4 relazioni per densificare Traceability + Context Explorer.
Creati in tenant t-demo-003 / unico project esistente:
1. DOC "Product Requirements.md" (DOCUMENT) --references--> REQ-103
2. COMMIT "0abc123 feat: REQ-103" (CODE_COMMIT) --generated_from--> PR#88
3. SEC "SEC-025 SQL injection input sanitization" (SECURITY_FINDING) --mitigated_by--> PR#88
4. INC "INC-77 Login timeout" (INCIDENT) --caused_by--> DEP-44 deployment
Rileva artifact IDs esistenti via LIST + ext_id exact match.
"""
from __future__ import annotations
import json
import sys
import time
import urllib.request
import urllib.error
import urllib.parse
BASE = "http://localhost:8000/api/v1"
H = {
"X-ASF-Tenant-Id": "t-demo-003",
"X-ASF-User-Id": "u-mario",
"Content-Type": "application/json",
}
LOOKUP = {
"REQ": "REQ-103",
"PR": "PR#88",
"DEP": "DEP-44",
}
def req(method: str, path: str, body=None) -> tuple[int, object]:
data = None
if body is not None:
data = json.dumps(body).encode()
r = urllib.request.Request(BASE + path, data=data, method=method, headers=H)
try:
with urllib.request.urlopen(r, timeout=10) as resp:
text = resp.read().decode()
return resp.status, json.loads(text) if text else None
except urllib.error.HTTPError as e:
body = e.read().decode()
print(f" !! {method} {path} -> {e.code}: {body[:300]}", file=sys.stderr)
return e.code, body
def list_artifacts() -> list[dict]:
s, d = req("GET", "/artifacts?limit=500")
assert s == 200 and isinstance(d, list), f"list artifacts failed: {s}"
return d
def find_project_id(arts: list[dict]) -> str:
"""Unico project del tenant: take artifact project_id non-None first."""
for a in arts:
if a.get("project_id"):
return a["project_id"]
s, pl = req("GET", "/projects?limit=50")
assert s == 200 and isinstance(pl, list) and pl
return pl[0]["id"]
def create_artifact(payload: dict) -> dict:
s, d = req("POST", "/artifacts", payload)
if s != 201 or not isinstance(d, dict):
raise RuntimeError(f"CREATE {payload.get('type')} {payload.get('external_id')} HTTP {s}")
return d
def create_rel(src: str, tgt: str, rel_type: str) -> int:
body = {
"source_artifact_id": src,
"target_artifact_id": tgt,
"relationship_type": rel_type,
}
s, _ = req("POST", "/relationships", body)
return s
def main() -> int:
arts = list_artifacts()
existing: dict[str, str] = {}
for k, ext in LOOKUP.items():
match = next((a for a in arts if a.get("external_id") == ext), None)
if match is None:
print(f" !! missing {ext}, aborting.")
return 2
existing[k] = match["id"]
print(f" {k:3s} {ext!r:12s} -> {match['id'][:10]}…")
pid = find_project_id(arts)
print(f" project id = {pid[:10]}…")
print("\n== Creating 4 extra artifacts ==")
extra = [
{
"project_id": pid,
"type": "document",
"external_id": "DOC-PRD-2026",
"title": "Product Requirements v2.3",
"status": "active",
"content": "# Product Requirements 2.3\n\nLiving PRD covering authentication, billing and reporting.\n\n## Goals\nProvide clear tenancy and billing.\n",
"metadata": {"format": "markdown", "page_count": 18, "owner": "pm"},
},
{
"project_id": pid,
"type": "commit",
"external_id": "COMMIT-0abc123",
"title": "feat(auth): implement REQ-103 SSO flow",
"content": "commit 0abc123df\nAuthor: mario\nDate: today\n\nAdds Okta SAML callback + session refresh route.\n",
"metadata": {"sha": "0abc123df", "author": "mario@asf.test", "repo": "asf/mono", "branch": "main"},
},
{
"project_id": pid,
"type": "security_finding",
"external_id": "SEC-025",
"title": "SQL injection risk in profile search (low confidence)",
"status": "open",
"content": "Input param q passed directly to $regex without sanitization. Scanner: semgrep. Confidence 82%.",
"metadata": {"cwe": "CWE-89", "severity": "high", "scanner": "semgrep", "confidence_pct": 82},
},
{
"project_id": pid,
"type": "incident",
"external_id": "INC-77",
"title": "Login timeout spike 05:00-05:14 UTC",
"status": "mitigated",
"content": "Session refresh endpoint returned 5xx for 4% of users. MTTR 14 minutes. 27 customers impacted.",
"metadata": {"severity": "sev2", "mttr_min": 14, "customers_impacted": 27},
},
]
ids: list[str] = []
for pl in extra:
d = create_artifact(pl)
print(f" [201] {pl['type']:15s} {pl['external_id']:18s} id={d['id'][:8]}…")
ids.append(d["id"])
doc_id, commit_id, sec_id, inc_id = ids
print("\n== Creating 4 extra relationships ==")
edges = [
(doc_id, existing["REQ"], "documented_by", "DOC --documented_by--> REQ"),
(commit_id, existing["PR"], "generated_from", "COMMIT --generated_from--> PR"),
(sec_id, existing["PR"], "related_to", "SEC --related_to--> PR"),
(inc_id, existing["DEP"], "caused_by", "INC --caused_by--> DEP"),
]
all_ok = True
for src, tgt, rt, label in edges:
s = create_rel(src, tgt, rt)
chk = "OK" if s == 201 else "FAIL"
print(f" [{s:3d}] {chk:4s} {label}")
if s != 201:
all_ok = False
time.sleep(0.3)
s, g = req("GET", f"/traceability/graph?project_id={pid}")
if s == 200 and isinstance(g, dict):
print(f"\n== Traceability graph now: nodes={len(g.get('nodes', []))} edges={len(g.get('edges', []))} ==")
return 0 if all_ok else 5
if __name__ == "__main__":
raise SystemExit(main())
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Retry 5 POST relationships SENZA campo `meta` (was extra_forbidden 422). Chained graph DEP→BUILD→PR→TASK→REQ + TEST→PR."""
from __future__ import annotations
import json, subprocess, sys
BASE = "http://127.0.0.1:8000/api/v1"
H = ["-H","X-ASF-Tenant-Id: t-demo-003","-H","X-ASF-User-Id: u-mario","-H","Content-Type: application/json"]
def curl(method, path, body=None):
cmd = ["curl","-s","-m","15","-o","-","-w","\n%{http_code}","-X",method] + H
if body is not None: cmd += ["--data", json.dumps(body, ensure_ascii=False)]
cmd.append(BASE+path)
r = subprocess.run(cmd, capture_output=True, text=True)
out = (r.stdout or "{}").strip()
if "\n" in out:
jp, cp = out.rsplit("\n", 1)
else:
jp, cp = "", out
try: code = int(cp)
except: code = 0
try: data = json.loads(jp or "null")
except Exception as exc: data = f"ERR {exc}: {jp[:200]!r}"
print(f" [{method:6s}{path:22s}] HTTP={code:3d}",
f"id={str(data.get('id',''))[:10] if isinstance(data,dict) else '?'}",
"rel=", (data.get("source_artifact_id","")[:8] if isinstance(data,dict) else "?"),
"→",(data.get("target_artifact_id","")[:8] if isinstance(data,dict) else "?"),
"type=",(data.get("relationship_type","?") if isinstance(data,dict) else "?"),
file=sys.stderr)
return code, data
def main():
_, arts = curl("GET", "/artifacts?limit=50")
assert isinstance(arts, list)
by_type = {}
for a in arts:
if isinstance(a,dict):
by_type.setdefault(a["type"], []).append(a)
req = by_type["requirement"][0]
task = by_type["task"][0]
pr = by_type["pull_request"][0]
test = by_type["test"][0]
build = by_type["build"][0]
deploy= by_type["deployment"][0]
print(f" req={str(req['id'])[:10]} task={str(task['id'])[:10]} pr={str(pr['id'])[:10]} test={str(test['id'])[:10]} build={str(build['id'])[:10]} deploy={str(deploy['id'])[:10]}", file=sys.stderr)
rels = [
(task["id"], req["id"], "implements"),
(pr["id"], task["id"], "generated_from"),
(build["id"], pr["id"], "generated_from"),
(test["id"], pr["id"], "verifies"),
(deploy["id"], build["id"], "deployed_as"),
]
oks=0
for s,t,k in rels:
code, d = curl("POST","/relationships", {"source_artifact_id": s, "target_artifact_id": t, "relationship_type": k})
if 200 <= code < 300: oks += 1
print(f"\n rels OK = {oks}/5", file=sys.stderr)
code, graph = curl("GET", "/traceability/graph?limit=200")
assert code == 200 and isinstance(graph, dict)
nodes = graph.get("nodes", []); edges = graph.get("edges", [])
print(json.dumps({"nodes":len(nodes),"edges":len(edges),
"node_types":[n.get("type") for n in nodes],
"edge_labels":[e.get("label") for e in edges]}))
return 0 if oks == 5 else 2
raise SystemExit(main())
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Seed tenant t-demo-003 with 5 artifacts + 5 relationships for a rich Traceability Graph demo.
Endpoint testati via curl:
- POST /api/v1/artifacts
- POST /api/v1/relationships
- GET /api/v1/traceability/graph
"""
from __future__ import annotations
import json
import subprocess
import sys
from typing import Any
BASE = "http://127.0.0.1:8000/api/v1"
H = [
"-H", "X-ASF-Tenant-Id: t-demo-003",
"-H", "X-ASF-User-Id: u-mario",
"-H", "Content-Type: application/json",
]
def curl(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any] | list | str]:
cmd = ["curl", "-s", "-m", "15", "-o", "-", "-w", "\n%{http_code}", "-X", method] + H
if body is not None:
cmd += ["--data", json.dumps(body, ensure_ascii=False)]
cmd.append(f"{BASE}{path}")
r = subprocess.run(cmd, capture_output=True, text=True)
out_raw = r.stdout.strip() or "{}"
# Last line is the http code we injected with -w
if "\n" in out_raw:
json_part, code_part = out_raw.rsplit("\n", 1)
else:
json_part, code_part = "", out_raw
try:
code = int(code_part)
except ValueError:
code = 0
try:
data = json.loads(json_part or "null")
except Exception as exc: # pragma: no cover
data = f"JSON_ERR {exc} RAW={json_part[:200]!r}"
print(f" [{method:6s} {path:24s}] HTTP={code:3d}",
f"type={data.get('type','?') if isinstance(data,dict) else '?'}\tid={str(data.get('id',''))[:10] if isinstance(data,dict) else '?'}",
file=sys.stderr)
return code, data
def main() -> int:
print("# Step 1: list existing artifacts + project_id", file=sys.stderr)
_, arts = curl("GET", "/artifacts?limit=50")
assert isinstance(arts, list), f"artifacts list not a list: {arts!r}"
req_103 = None
project_id = None
for a in arts:
if isinstance(a, dict) and a.get("external_id") == "REQ-103":
req_103 = a
project_id = a.get("project_id")
break
if not req_103:
# fallback: first artifact
req_103 = arts[0] if arts else None
project_id = req_103.get("project_id") if isinstance(req_103, dict) else None
print(f" req_103 id={str(req_103['id'])[:10] if isinstance(req_103,dict) else None} project_id={str(project_id)[:10] if project_id else None}",
file=sys.stderr)
def make(type_: str, ext: str, title: str, content: str) -> dict[str, Any]:
code, data = curl(
"POST", "/artifacts",
{
"type": type_,
"external_id": ext,
"title": title,
"content": content,
"status": "done",
"project_id": project_id,
},
)
assert code == 201 and isinstance(data, dict) and "id" in data, f"make {type_} FAILED HTTP={code} {data}"
return data
print(file=sys.stderr); print("# Step 2: create 5 extra artifacts", file=sys.stderr)
task = make("task", "TASK-101", "Implementa tenant trust header coerce", "FastAPI correlation middleware: UUID UUID5 per stringhe t-demo-xxx, no 400 invalid")
pr = make("pull_request", "PR#88", "feat(rbac): tenant trust + policies v1", "feat(rbac): add trust header middleware + policy service helpers")
test = make("test", "TEST-22", "cross-tenant reads/writes blocked", "Pytest suite proving cross-tenant artifact operations return 404/403")
build = make("build", "BUILD-910","asf-core build #910 (main)", "GitHub Actions build main branch, all checks green.")
deploy = make("deployment", "DEP-44", "asf-core v0.5.0 → prod-west-1", "ArgoCD sync asf-core to cluster prod-west-1 eu-west")
print(file=sys.stderr); print("# Step 3: create 5 relationships (chain: DEP → BUILD → PR → TASK → REQ, TEST → PR)", file=sys.stderr)
rels_pairs = [
(task, req_103, "implements"),
(pr, task, "generated_from"),
(build, pr, "generated_from"),
(test, pr, "verifies"),
(deploy, build, "deployed_as"),
]
for src, tgt, kind in rels_pairs:
code, rel = curl(
"POST", "/relationships",
{
"source_artifact_id": src["id"],
"target_artifact_id": tgt["id"],
"relationship_type": kind,
"meta": {"seed": "traceability_demo"},
},
)
assert code < 300 and isinstance(rel, dict) and "id" in rel, f"REL FAIL {kind} -> {rel}"
print(file=sys.stderr); print("# Step 4: GET traceability graph → counts", file=sys.stderr)
code, graph = curl("GET", "/traceability/graph?limit=200")
assert code == 200 and isinstance(graph, dict), f"GRAPH FAIL HTTP={code} {graph}"
nodes = graph.get("nodes", [])
edges = graph.get("edges", [])
print(f"\n=== SEED SUCCESS ===", file=sys.stderr)
print(f" graph.nodes = {len(nodes)} graph.edges = {len(edges)}", file=sys.stderr)
# print to stdout for bash pipeline capture (JSON):
print(json.dumps({"nodes": len(nodes), "edges": len(edges),
"node_ids": [{"id":str(n["id"])[:10],"type":n.get("type"),"label":n.get("label")} for n in nodes],
"edge_labels": [{"s":e["source"][:8],"t":e["target"][:8],"lbl":e.get("label")} for e in edges]}))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env bash
# start-stack.sh — avvia ASF Core, ASF Web (Next.js), e opzionalmente ASF Worker.
# Usage:
# ./scripts/start-stack.sh # core + web, reload + dev mode
# ./scripts/start-stack.sh --no-reload # core senza --reload (productionish)
# ./scripts/start-stack.sh --with-worker # avvia anche asf-worker (7 code)
# ./scripts/start-stack.sh --web-only # solo frontend Next.js
# ./scripts/start-stack.sh --core-only # solo backend Core (FastAPI)
# Bindings default: 0.0.0.0 (core 8000, web 3000).
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
usage() {
sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
WITH_WORKER=0
RELOAD=1
CORE=1
WEB=1
for arg in "$@"; do
case "$arg" in
--with-worker|-w) WITH_WORKER=1 ;;
--no-reload) RELOAD=0 ;;
--web-only) CORE=0; WITH_WORKER=0 ;;
--core-only) WEB=0 ;;
-h|--help) usage ;;
*) echo "Unknown option: $arg"; usage ;;
esac
done
# ------------------------------------------------------------
# 1) Preflight checks
# ------------------------------------------------------------
VENV_DIR="$ROOT_DIR/.venv"
if [ ! -d "$VENV_DIR" ]; then
echo "[start-stack] ❌ .venv non trovata in $VENV_DIR. Crea prima la venv: python -m venv .venv && source .venv/bin/activate" >&2
exit 1
fi
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
ENV_FILE="$ROOT_DIR/.env"
if [ ! -f "$ENV_FILE" ]; then
echo "[start-stack] ⚠️ .env non trovato, uso .env.example (se esiste)."
if [ -f "$ROOT_DIR/.env.example" ]; then
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
fi
fi
# shellcheck disable=SC1090
set -a; [ -f "$ENV_FILE" ] && source "$ENV_FILE"; set +a
export ASF_ROOT="$ROOT_DIR"
export PYTHONPATH="\
$ROOT_DIR/packages/asf-domain:\
$ROOT_DIR/services/asf-events:\
$ROOT_DIR/services/asf-connectors:\
$ROOT_DIR/services/asf-core:\
$ROOT_DIR/services/asf-worker:\
$ROOT_DIR/services/asf-ai:\
$ROOT_DIR/services/asf-context\
${PYTHONPATH:+:$PYTHONPATH}"
export NEXT_PUBLIC_ASF_CORE_URL="${NEXT_PUBLIC_ASF_CORE_URL:-http://localhost:8000}"
export NEXT_PUBLIC_DEV_TENANT_ID="${NEXT_PUBLIC_DEV_TENANT_ID:-t-demo-001}"
export NEXT_PUBLIC_DEV_USER_ID="${NEXT_PUBLIC_DEV_USER_ID:-u-admin}"
LOG_DIR="$ROOT_DIR/var/log/asf"
PID_DIR="$ROOT_DIR/var/run"
mkdir -p "$LOG_DIR" "$PID_DIR"
# ------------------------------------------------------------
# 2) Helpers
# ------------------------------------------------------------
pids=()
names=()
start_in_dir() {
local name="$1"; shift
local dir="$1"; shift
local log="$LOG_DIR/$name.log"
echo " starting $name ($(basename "$dir")) ..."
(
cd "$dir"
# shellcheck disable=SC2093
exec "$@" >"$log" 2>&1
) &
local pid=$!
pids+=("$pid")
names+=("$name")
echo " pid=$pid log=$log"
}
print_header() {
local COLS
COLS=$(tput cols 2>/dev/null || echo 80)
printf '═%.0s' $(seq 1 "$COLS"); echo
printf " ASF local stack · %s · tenant=%s user=%s\n" \
"$(date '+%F %T %Z')" \
"$NEXT_PUBLIC_DEV_TENANT_ID" \
"$NEXT_PUBLIC_DEV_USER_ID"
printf '═%.0s' $(seq 1 "$COLS"); echo
}
graceful_stop() {
echo
echo "[start-stack] SIGINT/SIGTERM received — stopping ${#pids[@]} processes (${names[*]})."
for i in "${!pids[@]}"; do
local pid="${pids[$i]}"
local name="${names[$i]}"
if kill -0 "$pid" 2>/dev/null; then
echo " stopping $name (pid=$pid)"
kill -TERM "$pid" 2>/dev/null || true
fi
done
# wait them out
local waited=0
while [ $waited -lt 10 ]; do
local alive=0
for pid in "${pids[@]}"; do kill -0 "$pid" 2>/dev/null && alive=1; done
[ $alive -eq 0 ] && break
sleep 1; waited=$((waited+1))
done
# force kill any stragglers
for pid in "${pids[@]}"; do
kill -0 "$pid" 2>/dev/null && { echo " force kill pid=$pid"; kill -9 "$pid" 2>/dev/null || true; }
done
echo "[start-stack] stopped."
exit 0
}
trap graceful_stop INT TERM
# ------------------------------------------------------------
# 3) Dependency quick liveness (Mongo + Rabbit — best effort)
# ------------------------------------------------------------
print_header
echo "[start-stack] python=$(python -V 2>&1 | cut -d' ' -f2) node=$(node -v) npm=v$(npm -v 2>/dev/null)"
echo "[start-stack] PYTHONPATH[0:80]=${PYTHONPATH:0:80}…"
echo
echo "[deps] Mongo / Rabbit best-effort reachability (5s timeout)…"
python - <<PY 2>/dev/null || true
import os,socket,time
for host,port,name in [("172.19.40.102",27017,"Mongo rs0 PRIMARY"),("172.19.40.103",27017,"Mongo rs0 SEED"),("172.19.40.201",5672,"RabbitMQ AMQP"),("172.19.40.201",15672,"RabbitMQ HTTP mgmt")]:
s=socket.socket(); s.settimeout(2)
try:
s.connect((host,port)); print(f" ✅ {name:22s} {host}:{port} reachable")
except Exception as e:
print(f" ⚠️ {name:22s} {host}:{port} unreachable ({e})")
finally: s.close()
PY
echo
# ------------------------------------------------------------
# 4) Start processes
# ------------------------------------------------------------
echo "[start-stack] launching services (bind 0.0.0.0)…"
# Core (FastAPI)
if [ $CORE -eq 1 ]; then
CORE_ARGS=(uvicorn app.main:app --host 0.0.0.0 --port 8000)
[ $RELOAD -eq 1 ] && CORE_ARGS+=(--reload)
start_in_dir "asf-core" "$ROOT_DIR/services/asf-core" "${CORE_ARGS[@]}"
echo " → http://0.0.0.0:8000/docs (OpenAPI)"
fi
# Web (Next.js dev)
if [ $WEB -eq 1 ]; then
start_in_dir "asf-web" "$ROOT_DIR/frontend/asf-web" npm run dev -- --hostname 0.0.0.0 --port 3000
echo " → http://0.0.0.0:3000 (UI)"
fi
# Worker
if [ $WITH_WORKER -eq 1 ]; then
start_in_dir "asf-worker" "$ROOT_DIR/services/asf-worker" python -m app.worker
echo " → queues=$ASF_WORKER_JOB_QUEUES"
fi
echo
# Save PIDs on disk for `stop-stack.sh`
: > "$PID_DIR/asf-stack.pids"
for i in "${!pids[@]}"; do
echo "${names[$i]} ${pids[$i]}" >> "$PID_DIR/asf-stack.pids"
done
echo "[start-stack] PIDs salvati in $PID_DIR/asf-stack.pids"
echo
echo "Tipi 'Ctrl+C' per arrestare TUTTI i processi. Oppure usa: ./scripts/stop-stack.sh"
echo
# ------------------------------------------------------------
# 5) Smoke test (first 20s)
# ------------------------------------------------------------
deadline=$((SECONDS + 25))
ok_core=0; ok_web=0
while [ $SECONDS -lt $deadline ]; do
if [ $CORE -eq 1 ] && [ $ok_core -eq 0 ]; then
code=$(curl -s -m 4 -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/api/v1/health || true)
[ "$code" = "200" ] && { ok_core=1; echo " ✅ asf-core up /health HTTP=$code"; }
fi
if [ $WEB -eq 1 ] && [ $ok_web -eq 0 ]; then
code=$(curl -s -m 4 -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/ || true)
[ "$code" = "200" ] && { ok_web=1; echo " ✅ asf-web up / HTTP=$code"; }
fi
need=0
[ $CORE -eq 1 ] && [ $ok_core -eq 0 ] && need=1
[ $WEB -eq 1 ] && [ $ok_web -eq 0 ] && need=1
[ $need -eq 0 ] && break
sleep 2
done
[ $CORE -eq 1 ] && [ $ok_core -eq 0 ] && echo " ⚠️ asf-core non ha risposto /health in tempo (guarda $LOG_DIR/asf-core.log)"
[ $WEB -eq 1 ] && [ $ok_web -eq 0 ] && echo " ⚠️ asf-web non ha risposto / in tempo (Next.js 14 first compile slow: attendi / guarda $LOG_DIR/asf-web.log)"
echo
echo "ASF stack ready. In attesa (foreground)… Ctrl+C = stop."
# wait
for pid in "${pids[@]}"; do
wait "$pid" || true
done
graceful_stop
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# status-stack.sh — PIDs, logs, HTTP live per ASF Core/Web/Worker.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PID_FILE="$ROOT_DIR/var/run/asf-stack.pids"
LOG_DIR="$ROOT_DIR/var/log/asf"
echo "ASF stack STATUS $(date '+%F %T %Z')"
echo
printf "%-18s %-8s %-6s %-6s %s\n" "NAME" "PID" "PORT" "HTTP" "LOG"
printf '─%.0s' $(seq 1 90); echo
check_http() {
local url="$1"
curl -s -m 3 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo "000"
}
# Core
pid=$(pgrep -f "uvicorn app.main:app" 2>/dev/null | head -1 || true)
http=$(check_http "http://127.0.0.1:8000/api/v1/health")
printf "%-18s %-8s %-6s %-6s %s\n" "asf-core" "${pid:--}" "8000" "$http" "$LOG_DIR/asf-core.log"
# Web
pid=$(pgrep -af "next-server|next dev" 2>/dev/null | awk '{print $1}' | head -1 || true)
http=$(check_http "http://127.0.0.1:3000/")
printf "%-18s %-8s %-6s %-6s %s\n" "asf-web" "${pid:--}" "3000" "$http" "$LOG_DIR/asf-web.log"
# Worker
pid=$(pgrep -f "python -m app.worker" 2>/dev/null | head -1 || true)
printf "%-18s %-8s %-6s %-6s %s\n" "asf-worker" "${pid:--}" "AMQP" "-" "$LOG_DIR/asf-worker.log"
echo
echo "Pid file:"; [ -f "$PID_FILE" ] && cat "$PID_FILE" || echo " (nessuno)"
echo
echo "Logs tail (10 lines most recent each):"
for f in "$LOG_DIR"/*.log; do
[ -e "$f" ] || continue
echo
echo "───── $f (last 10 lines) ─────"
tail -10 "$f" | sed 's/^/ │ /'
done
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# stop-stack.sh — ferma i processi avviati da start-stack.sh.
# Legge i PIDs da var/run/asf-stack.pids, oppure cerca per pattern.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PID_FILE="$ROOT_DIR/var/run/asf-stack.pids"
echo "ASF stack STOP $(date '+%F %T %Z')"
echo
stop_group() {
local pid="$1" name="$2"
if ! kill -0 "$pid" 2>/dev/null; then
echo " ✗ $name (pid=$pid) già spento"
return 0
fi
echo -n " ⏹ $name pid=$pid → "
kill -TERM "$pid" 2>/dev/null || true
local w=0
while [ $w -lt 8 ]; do
kill -0 "$pid" 2>/dev/null || break
sleep 1; w=$((w+1))
done
if kill -0 "$pid" 2>/dev/null; then
echo -n "SIGKILL "
kill -9 "$pid" 2>/dev/null || true
sleep 1
fi
kill -0 "$pid" 2>/dev/null && echo "FAIL (ancora vivo)" || echo "ok"
}
stop_patterns() {
echo "[fallback] kill via pattern match…"
for pattern in \
"uvicorn app.main:app" \
"next dev" "next-server" \
"python -m app.worker" "asf-worker.*run_worker"; do
pids=$(pgrep -af "$pattern" 2>/dev/null | awk '{print $1}' || true)
[ -z "$pids" ] && continue
echo " pattern '$pattern' → pids=$pids"
for p in $pids; do stop_group "$p" "$pattern"; done
done
}
if [ -f "$PID_FILE" ]; then
echo "[from pidfile] $PID_FILE"
while IFS=' ' read -r name pid rest; do
[ -z "${name:-}" ] && continue
[ -z "${pid:-}" ] && continue
stop_group "$pid" "$name"
done < "$PID_FILE"
rm -f "$PID_FILE"
else
echo "[no pidfile $PID_FILE]"
stop_patterns
fi
# final sweep
echo
echo "ports still listening:"
ss -ltnp 2>/dev/null | grep -E ":(8000|3000) " || echo " → nessuna porta 8000 / 3000 usata (ok)."
echo "done."