init: company-haness 설계
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
"""Reusable state-kernel services behind the compatibility ``state_engine.py`` CLI."""
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Append-only JSONL primitives for the Org OS state kernel."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from contextlib import nullcontext
|
||||
from typing import Any, Callable, ContextManager, Iterable
|
||||
|
||||
|
||||
def read_jsonl(path: str | None, on_error: Callable[[str], None] | None = None) -> list[dict[str, Any]]:
|
||||
if not path or not os.path.exists(path):
|
||||
return []
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
rows.append(value)
|
||||
except Exception as exc:
|
||||
if on_error:
|
||||
on_error(f"event 원장 읽기 실패({path}): {exc}")
|
||||
return rows
|
||||
|
||||
|
||||
def append_jsonl(
|
||||
path: str | None,
|
||||
event: dict[str, Any],
|
||||
*,
|
||||
file_lock: bool = False,
|
||||
on_error: Callable[[str], None] | None = None,
|
||||
) -> bool:
|
||||
if not path:
|
||||
return False
|
||||
try:
|
||||
with open(path, "a", encoding="utf-8") as handle:
|
||||
if file_lock:
|
||||
try:
|
||||
import fcntl
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
||||
except Exception:
|
||||
pass
|
||||
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
return True
|
||||
except Exception as exc:
|
||||
if on_error:
|
||||
on_error(f"event append 실패({path}): {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def atomic_append(
|
||||
entries: Iterable[tuple[str | None, dict[str, Any]]],
|
||||
*,
|
||||
transaction_lock: ContextManager[Any] | None = None,
|
||||
) -> None:
|
||||
"""Append a group of events and truncate every participating tail on failure."""
|
||||
normalized = list(entries)
|
||||
handles: list[tuple[Any, int]] = []
|
||||
with (transaction_lock or nullcontext()):
|
||||
try:
|
||||
for path, _event in normalized:
|
||||
if not path:
|
||||
raise OSError("event path 해석 실패")
|
||||
handle = open(path, "a+", encoding="utf-8")
|
||||
try:
|
||||
import fcntl
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
||||
except Exception:
|
||||
pass
|
||||
handle.seek(0, os.SEEK_END)
|
||||
handles.append((handle, handle.tell()))
|
||||
try:
|
||||
for (handle, _offset), (_path, event) in zip(handles, normalized):
|
||||
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
except Exception:
|
||||
for handle, offset in handles:
|
||||
handle.seek(offset)
|
||||
handle.truncate()
|
||||
handle.flush()
|
||||
raise
|
||||
finally:
|
||||
for handle, _offset in handles:
|
||||
try:
|
||||
handle.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Deterministic workflow materialized-view projection from canonical events."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
PROTECTED_FIELDS = (
|
||||
"quality_gate_status", "quality-gate-status",
|
||||
"release_acceptance_status", "release-acceptance-status",
|
||||
"unresolved_critical_risks", "unresolved-critical-risks",
|
||||
"blocker-open", "blocker_open", "resume-condition",
|
||||
"resume_condition_present", "resume-condition-satisfied",
|
||||
"resume_condition_satisfied", "evidence-grade", "evidence_grade",
|
||||
"human_gate_approved", "human-gate-approved",
|
||||
"current-quality-event-id", "current-quality-event-ids",
|
||||
"current-quality-artifact-id", "current-quality-artifact-sha256",
|
||||
"current-completion-artifact-id", "current-completion-artifact-sha256",
|
||||
"quality-panel-unmet",
|
||||
)
|
||||
|
||||
|
||||
def _latest_artifact_of_kind(artifacts: list[dict[str, Any]], kind: str) -> dict[str, Any] | None:
|
||||
return next((artifact for artifact in reversed(artifacts or [])
|
||||
if isinstance(artifact, dict) and artifact.get("artifact-kind") == kind), None)
|
||||
|
||||
|
||||
def project_workflow(
|
||||
ledger: dict[str, Any],
|
||||
*,
|
||||
trusted_artifacts: list[dict[str, Any]],
|
||||
workflow_events: list[dict[str, Any]],
|
||||
initial_stage: Callable[[str], str],
|
||||
quality_panel_unmet: Callable[[str, list[dict[str, Any]], list[dict[str, Any]]], list[str]],
|
||||
default_plan: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Rebuild protected fields; caller-owned data is never accepted as runtime truth."""
|
||||
projected = dict(ledger)
|
||||
for key in PROTECTED_FIELDS:
|
||||
projected.pop(key, None)
|
||||
projected["artifacts"] = trusted_artifacts
|
||||
quality_events: list[dict[str, Any]] = []
|
||||
release_events: list[dict[str, Any]] = []
|
||||
for event in workflow_events:
|
||||
event_type = event.get("event-type")
|
||||
if event_type == "workflow-initialized":
|
||||
projected["stage"] = event.get("stage") or initial_stage(event.get("plan", default_plan))
|
||||
projected["stage-status"] = "running"
|
||||
projected["last-completed-stage"] = None
|
||||
projected["completed-for-next-stage"] = None
|
||||
projected.pop("blocked-from", None)
|
||||
projected.pop("design-direction-approval", None)
|
||||
projected.pop("experience-foundation-approval", None)
|
||||
for key in (
|
||||
"plan", "tier", "mode", "evidence-contract-version",
|
||||
"parent-workflow-id", "product-decision-id",
|
||||
"direction-input-brief-ref", "direction-input-brief-sha256",
|
||||
):
|
||||
if event.get(key) is not None:
|
||||
projected[key] = event.get(key)
|
||||
elif event_type == "state-transition":
|
||||
projected["stage"] = event.get("to") or projected.get("stage")
|
||||
projected["stage-status"] = "running"
|
||||
projected["last-completed-stage"] = event.get("from")
|
||||
projected["completed-for-next-stage"] = None
|
||||
if event.get("to") == "blocked":
|
||||
projected["blocked-from"] = event.get("from")
|
||||
elif event.get("from") == "blocked":
|
||||
projected.pop("blocked-from", None)
|
||||
elif event_type == "stage-completed":
|
||||
if event.get("stage") == projected.get("stage"):
|
||||
projected["stage-status"] = "completed"
|
||||
projected["last-completed-stage"] = event.get("stage")
|
||||
projected["completed-for-next-stage"] = event.get("intended-next-stage")
|
||||
elif event_type == "quality-gate-recorded":
|
||||
quality_events.append(event)
|
||||
elif event_type == "release-decision-recorded":
|
||||
release_events.append(event)
|
||||
elif event_type == "tier-escalated":
|
||||
projected["tier"] = event.get("to-tier") or projected.get("tier")
|
||||
elif event_type == "workflow-blocked":
|
||||
projected["blocked-from"] = event.get("blocked-from") or projected.get("stage")
|
||||
projected["stage"] = "blocked"
|
||||
projected["stage-status"] = "running"
|
||||
projected["completed-for-next-stage"] = None
|
||||
projected["blocker-open"] = True
|
||||
projected["resume-condition"] = event.get("resume-condition")
|
||||
projected.pop("resume-condition-satisfied", None)
|
||||
elif event_type == "workflow-resumed":
|
||||
projected["stage"] = event.get("to") or projected.get("blocked-from") or projected.get("stage")
|
||||
projected["stage-status"] = "running"
|
||||
projected["completed-for-next-stage"] = None
|
||||
projected.pop("blocked-from", None)
|
||||
projected["blocker-open"] = False
|
||||
projected["resume-condition-satisfied"] = True
|
||||
elif event_type == "direction-approval-registered":
|
||||
projected["design-direction-approval"] = {
|
||||
"report-ref": event.get("report-ref"),
|
||||
"report-sha256": event.get("report-sha256"),
|
||||
"child-workflow-id": event.get("child-workflow-id"),
|
||||
}
|
||||
elif event_type == "experience-foundation-registered":
|
||||
projected["experience-foundation-approval"] = {
|
||||
"child-workflow-id": event.get("child-workflow-id"),
|
||||
"product-decision-id": event.get("product-decision-id"),
|
||||
"benchmark-id": event.get("benchmark-id"),
|
||||
"benchmark-ref": event.get("benchmark-ref"),
|
||||
"benchmark-sha256": event.get("benchmark-sha256"),
|
||||
"strategy-id": event.get("strategy-id"),
|
||||
"strategy-ref": event.get("strategy-ref"),
|
||||
"strategy-sha256": event.get("strategy-sha256"),
|
||||
"technical-id": event.get("technical-id"),
|
||||
"technical-ref": event.get("technical-ref"),
|
||||
"technical-sha256": event.get("technical-sha256"),
|
||||
"operational-id": event.get("operational-id"),
|
||||
"operational-ref": event.get("operational-ref"),
|
||||
"operational-sha256": event.get("operational-sha256"),
|
||||
"blueprint-id": event.get("blueprint-id"),
|
||||
"blueprint-ref": event.get("blueprint-ref"),
|
||||
"blueprint-sha256": event.get("blueprint-sha256"),
|
||||
"wireframe-id": event.get("wireframe-id"),
|
||||
"wireframe-ref": event.get("wireframe-ref"),
|
||||
"wireframe-sha256": event.get("wireframe-sha256"),
|
||||
}
|
||||
|
||||
completion = _latest_artifact_of_kind(trusted_artifacts, "completion-record")
|
||||
if not completion:
|
||||
return projected
|
||||
completion_id = completion.get("artifact-id")
|
||||
completion_sha = completion.get("artifact-sha256")
|
||||
projected["current-completion-artifact-id"] = completion_id
|
||||
projected["current-completion-artifact-sha256"] = completion_sha
|
||||
current_quality = [
|
||||
event for event in quality_events
|
||||
if event.get("reviewed-artifact-id") == completion_id
|
||||
and event.get("reviewed-artifact-sha256") == completion_sha
|
||||
and any(
|
||||
artifact.get("artifact-id") == event.get("review-artifact-id")
|
||||
and artifact.get("artifact-sha256") == event.get("review-artifact-sha256")
|
||||
for artifact in trusted_artifacts
|
||||
)
|
||||
]
|
||||
latest_by_actor = {event.get("actor"): event for event in current_quality}
|
||||
active_quality = list(latest_by_actor.values())
|
||||
if active_quality:
|
||||
failed = any(event.get("status") != "Passed" or event.get("blocker-open")
|
||||
for event in active_quality)
|
||||
projected["quality_gate_status"] = "Failed" if failed else "Passed"
|
||||
projected["blocker-open"] = bool(projected.get("blocker-open")) or any(
|
||||
bool(event.get("blocker-open")) for event in active_quality)
|
||||
last_quality = active_quality[-1]
|
||||
projected["current-quality-event-id"] = last_quality.get("workflow-event-id")
|
||||
projected["current-quality-event-ids"] = sorted(
|
||||
event.get("workflow-event-id") for event in active_quality if event.get("workflow-event-id")
|
||||
)
|
||||
projected["current-quality-artifact-id"] = last_quality.get("review-artifact-id")
|
||||
projected["current-quality-artifact-sha256"] = last_quality.get("review-artifact-sha256")
|
||||
panel_unmet = quality_panel_unmet(projected.get("tier"), trusted_artifacts, active_quality)
|
||||
if panel_unmet:
|
||||
projected["quality-panel-unmet"] = panel_unmet
|
||||
projected.pop("quality_gate_status", None)
|
||||
active_event_ids = projected.get("current-quality-event-ids") or []
|
||||
for event in release_events:
|
||||
if (event.get("reviewed-completion-artifact-id") != completion_id
|
||||
or event.get("reviewed-completion-artifact-sha256") != completion_sha
|
||||
or sorted(event.get("quality-event-set") or []) != active_event_ids):
|
||||
continue
|
||||
if event.get("status") == "Approved" and (
|
||||
projected.get("quality_gate_status") != "Passed" or projected.get("blocker-open")):
|
||||
continue
|
||||
projected["release_acceptance_status"] = event.get("status")
|
||||
projected["unresolved_critical_risks"] = bool(event.get("unresolved-critical-risks"))
|
||||
return projected
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Pure workflow transition evaluation; state I/O remains outside this module."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
def structural_next(
|
||||
current: str,
|
||||
plan_stages: list[str],
|
||||
transitions: list[dict[str, Any]],
|
||||
blocked_from: str | None = None,
|
||||
) -> list[str]:
|
||||
destinations: set[str] = set()
|
||||
if current in plan_stages:
|
||||
index = plan_stages.index(current)
|
||||
if index + 1 < len(plan_stages):
|
||||
destinations.add(plan_stages[index + 1])
|
||||
for transition in transitions:
|
||||
source = transition.get("from")
|
||||
destination = transition.get("to")
|
||||
if source == current:
|
||||
if destination == "<resume>":
|
||||
if blocked_from:
|
||||
destinations.add(blocked_from)
|
||||
elif destination:
|
||||
destinations.add(destination)
|
||||
elif source == "*" and current not in ("blocked", "closed") and destination:
|
||||
destinations.add(destination)
|
||||
destinations.discard("<resume>")
|
||||
destinations.discard("*")
|
||||
return sorted(destinations)
|
||||
|
||||
|
||||
def actor_allowed(
|
||||
transition: dict[str, Any],
|
||||
actor: str | None,
|
||||
role_registry: dict[str, Any],
|
||||
) -> tuple[bool, str | None]:
|
||||
allowed = transition.get("allowed-by") or []
|
||||
if not str(actor or "").strip():
|
||||
return False, "전이 주체(--actor) 미지정 — 누가 전이하는지 명시해야 한다(권한/감사)."
|
||||
if not allowed:
|
||||
return True, None
|
||||
normalized = str(actor).strip()
|
||||
if normalized not in role_registry:
|
||||
return False, f"actor '{normalized}'는 role registry에 없는 역할이다"
|
||||
if isinstance(allowed, dict):
|
||||
concrete = allowed.get("executor") or allowed.get("concrete-roles") or []
|
||||
else:
|
||||
concrete = [value for value in allowed
|
||||
if isinstance(value, str) and not value.endswith("-role-agent")]
|
||||
if normalized in concrete:
|
||||
return True, None
|
||||
if not concrete:
|
||||
return False, f"allowed-by가 placeholder만 포함해 runtime 권한을 결정할 수 없다: {allowed}"
|
||||
return False, f"actor '{actor}' 는 transition executor {concrete} 에 없다(권한 없음)."
|
||||
|
||||
|
||||
def evaluate_transition(
|
||||
transition: dict[str, Any],
|
||||
*,
|
||||
current: str,
|
||||
destination: str,
|
||||
plan: str,
|
||||
plan_stages: set[str],
|
||||
blocked_from: str | None,
|
||||
facts: dict[str, Any],
|
||||
condition_evaluator: Callable[[Any, dict[str, Any]], tuple[bool, str | None]],
|
||||
actor: str | None = None,
|
||||
role_registry: dict[str, Any] | None = None,
|
||||
) -> list[str]:
|
||||
universal = (destination == "blocked" or current == "blocked"
|
||||
or destination == current or transition.get("from") == "*")
|
||||
if plan_stages and not universal and destination not in plan_stages:
|
||||
return [f"plan '{plan}' 시퀀스에 없는 stage 전이 금지: {current} -> {destination} "
|
||||
f"(plan stages: {sorted(plan_stages)})"]
|
||||
if actor:
|
||||
permitted, reason = actor_allowed(transition, actor, role_registry or {})
|
||||
if not permitted:
|
||||
return [reason or "transition actor 권한 없음"]
|
||||
if current == "blocked" and blocked_from and destination != blocked_from:
|
||||
return [f"blocked 재개 대상은 {blocked_from} 여야 합니다(요청: {destination})"]
|
||||
reasons: list[str] = []
|
||||
for condition in transition.get("required-conditions") or []:
|
||||
passed, reason = condition_evaluator(condition, facts)
|
||||
if not passed:
|
||||
reasons.append(reason or f"조건 미충족: {condition}")
|
||||
for condition in transition.get("forbidden-if") or []:
|
||||
passed, _reason = condition_evaluator(condition, facts)
|
||||
if passed:
|
||||
reasons.append(f"금지조건 충족: {condition}")
|
||||
return reasons
|
||||
|
||||
Reference in New Issue
Block a user