algo-cli-runtime 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- ollama_cli/__init__.py +67 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""B65. Multi-Source Parallel Fan-Out.
|
|
2
|
+
|
|
3
|
+
N questions × M sources = NM parallel tasks.
|
|
4
|
+
Source: deep-research-agent pattern.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FanOutTask:
|
|
16
|
+
question: str
|
|
17
|
+
source: str # "web", "github", "arxiv", "local"
|
|
18
|
+
query: str
|
|
19
|
+
priority: int = 5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FanOutResult:
|
|
24
|
+
question: str
|
|
25
|
+
source: str
|
|
26
|
+
query: str
|
|
27
|
+
results: list[tuple[str, str]] = field(default_factory=list) # (title, content)
|
|
28
|
+
duration_s: float = 0.0
|
|
29
|
+
error: str | None = None
|
|
30
|
+
success: bool = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ParallelFanOut:
|
|
34
|
+
"""Execute N questions × M sources in parallel."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, max_workers: int = 4, timeout: float = 30.0) -> None:
|
|
37
|
+
self._max_workers = max_workers
|
|
38
|
+
self._timeout = timeout
|
|
39
|
+
self._source_handlers: dict[str, Callable[[str], list[tuple[str, str]]]] = {}
|
|
40
|
+
|
|
41
|
+
def register_source(self, name: str,
|
|
42
|
+
handler: Callable[[str], list[tuple[str, str]]]) -> None:
|
|
43
|
+
self._source_handlers[name] = handler
|
|
44
|
+
|
|
45
|
+
def fan_out(self, questions: list[str], sources: list[str]) -> list[FanOutResult]:
|
|
46
|
+
"""Run all question×source combinations in parallel."""
|
|
47
|
+
tasks: list[FanOutTask] = []
|
|
48
|
+
for q in questions:
|
|
49
|
+
for src in sources:
|
|
50
|
+
tasks.append(FanOutTask(question=q, source=src, query=q))
|
|
51
|
+
|
|
52
|
+
return self._execute(tasks)
|
|
53
|
+
|
|
54
|
+
def _execute(self, tasks: list[FanOutTask]) -> list[FanOutResult]:
|
|
55
|
+
results: list[FanOutResult] = []
|
|
56
|
+
|
|
57
|
+
with ThreadPoolExecutor(max_workers=self._max_workers) as pool:
|
|
58
|
+
future_to_task = {
|
|
59
|
+
pool.submit(self._run_task, task): task for task in tasks
|
|
60
|
+
}
|
|
61
|
+
for future in as_completed(future_to_task, timeout=self._timeout):
|
|
62
|
+
task = future_to_task[future]
|
|
63
|
+
try:
|
|
64
|
+
results.append(future.result())
|
|
65
|
+
except Exception as e:
|
|
66
|
+
results.append(FanOutResult(
|
|
67
|
+
question=task.question, source=task.source,
|
|
68
|
+
query=task.query, error=str(e), success=False,
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
return results
|
|
72
|
+
|
|
73
|
+
def _run_task(self, task: FanOutTask) -> FanOutResult:
|
|
74
|
+
handler = self._source_handlers.get(task.source)
|
|
75
|
+
if not handler:
|
|
76
|
+
return FanOutResult(
|
|
77
|
+
question=task.question, source=task.source,
|
|
78
|
+
query=task.query, error=f"No handler for source '{task.source}'",
|
|
79
|
+
success=False,
|
|
80
|
+
)
|
|
81
|
+
start = time.time()
|
|
82
|
+
try:
|
|
83
|
+
results = handler(task.query)
|
|
84
|
+
return FanOutResult(
|
|
85
|
+
question=task.question, source=task.source,
|
|
86
|
+
query=task.query, results=results,
|
|
87
|
+
duration_s=time.time() - start,
|
|
88
|
+
)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return FanOutResult(
|
|
91
|
+
question=task.question, source=task.source,
|
|
92
|
+
query=task.query, error=str(e), success=False,
|
|
93
|
+
duration_s=time.time() - start,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def merge_results(self, results: list[FanOutResult]) -> dict[str, list[tuple[str, str, str]]]:
|
|
97
|
+
"""Merge results by question: {question: [(source, title, content), ...]}"""
|
|
98
|
+
merged: dict[str, list[tuple[str, str, str]]] = {}
|
|
99
|
+
for r in results:
|
|
100
|
+
if not r.success:
|
|
101
|
+
continue
|
|
102
|
+
for title, content in r.results:
|
|
103
|
+
merged.setdefault(r.question, []).append((r.source, title, content))
|
|
104
|
+
return merged
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""B51. Declarative Permission Modes + Structural Spawn Safety.
|
|
2
|
+
|
|
3
|
+
Config-level tool sets and path restrictions. Prevent permission
|
|
4
|
+
escalation structurally. Source: aloop pattern.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum, auto
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionLevel(Enum):
|
|
13
|
+
READ_ONLY = auto()
|
|
14
|
+
STANDARD = auto()
|
|
15
|
+
ELEVATED = auto()
|
|
16
|
+
UNRESTRICTED = auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Tool sets per permission level
|
|
20
|
+
DEFAULT_TOOL_SETS: dict[PermissionLevel, set[str]] = {
|
|
21
|
+
PermissionLevel.READ_ONLY: {
|
|
22
|
+
"read_file", "search_files", "list_directory", "harness_search",
|
|
23
|
+
"harness_read", "web_search", "web_fetch", "read_pdf",
|
|
24
|
+
},
|
|
25
|
+
PermissionLevel.STANDARD: {
|
|
26
|
+
"read_file", "search_files", "list_directory", "write_file",
|
|
27
|
+
"edit_file", "run_shell", "harness_search", "harness_read",
|
|
28
|
+
"web_search", "web_fetch", "read_pdf", "embed_text",
|
|
29
|
+
},
|
|
30
|
+
PermissionLevel.ELEVATED: {
|
|
31
|
+
"read_file", "search_files", "list_directory", "write_file",
|
|
32
|
+
"edit_file", "run_shell", "harness_search", "harness_read",
|
|
33
|
+
"web_search", "web_fetch", "read_pdf", "embed_text",
|
|
34
|
+
"model_create", "model_delete", "git_status", "git_diff",
|
|
35
|
+
},
|
|
36
|
+
PermissionLevel.UNRESTRICTED: {"*"},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Which levels can spawn which levels (prevent escalation)
|
|
40
|
+
SPAWN_MATRIX: dict[PermissionLevel, set[PermissionLevel]] = {
|
|
41
|
+
PermissionLevel.READ_ONLY: set(),
|
|
42
|
+
PermissionLevel.STANDARD: {PermissionLevel.READ_ONLY},
|
|
43
|
+
PermissionLevel.ELEVATED: {PermissionLevel.READ_ONLY, PermissionLevel.STANDARD},
|
|
44
|
+
PermissionLevel.UNRESTRICTED: {
|
|
45
|
+
PermissionLevel.READ_ONLY, PermissionLevel.STANDARD,
|
|
46
|
+
PermissionLevel.ELEVATED, PermissionLevel.UNRESTRICTED,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PermissionMode:
|
|
53
|
+
level: PermissionLevel
|
|
54
|
+
allowed_tools: set[str] = field(default_factory=set)
|
|
55
|
+
allowed_paths: list[str] = field(default_factory=list) # glob patterns
|
|
56
|
+
denied_paths: list[str] = field(default_factory=list)
|
|
57
|
+
spawnable_modes: set[PermissionLevel] = field(default_factory=set)
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
if not self.allowed_tools:
|
|
61
|
+
self.allowed_tools = DEFAULT_TOOL_SETS.get(self.level, set()).copy()
|
|
62
|
+
if not self.spawnable_modes:
|
|
63
|
+
self.spawnable_modes = SPAWN_MATRIX.get(self.level, set()).copy()
|
|
64
|
+
|
|
65
|
+
def can_use_tool(self, tool_name: str) -> bool:
|
|
66
|
+
return "*" in self.allowed_tools or tool_name in self.allowed_tools
|
|
67
|
+
|
|
68
|
+
def can_access_path(self, path: str) -> bool:
|
|
69
|
+
import fnmatch
|
|
70
|
+
for denied in self.denied_paths:
|
|
71
|
+
if fnmatch.fnmatch(path, denied):
|
|
72
|
+
return False
|
|
73
|
+
if not self.allowed_paths:
|
|
74
|
+
return True
|
|
75
|
+
return any(fnmatch.fnmatch(path, p) for p in self.allowed_paths)
|
|
76
|
+
|
|
77
|
+
def can_spawn(self, target_level: PermissionLevel) -> bool:
|
|
78
|
+
return target_level in self.spawnable_modes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PermissionManager:
|
|
82
|
+
"""Manage permission modes and enforce structural spawn safety."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, default_level: PermissionLevel = PermissionLevel.STANDARD) -> None:
|
|
85
|
+
self._modes: dict[str, PermissionMode] = {}
|
|
86
|
+
self._default = PermissionMode(level=default_level)
|
|
87
|
+
|
|
88
|
+
def register(self, name: str, mode: PermissionMode) -> None:
|
|
89
|
+
self._modes[name] = mode
|
|
90
|
+
|
|
91
|
+
def get(self, name: str | None) -> PermissionMode:
|
|
92
|
+
if name and name in self._modes:
|
|
93
|
+
return self._modes[name]
|
|
94
|
+
return self._default
|
|
95
|
+
|
|
96
|
+
def validate_spawn(
|
|
97
|
+
self,
|
|
98
|
+
parent_name: str | None,
|
|
99
|
+
child_level: PermissionLevel,
|
|
100
|
+
) -> bool:
|
|
101
|
+
parent = self.get(parent_name)
|
|
102
|
+
return parent.can_spawn(child_level)
|
|
103
|
+
|
|
104
|
+
def list_modes(self) -> dict[str, PermissionMode]:
|
|
105
|
+
return dict(self._modes)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""H32 — Pre-Push Scrubbing Gate.
|
|
2
|
+
|
|
3
|
+
Hard-blocks raw pushes of private working tree; requires explicit env var
|
|
4
|
+
override. Mirrors T3MP3ST ``.githooks/pre-push`` pattern.
|
|
5
|
+
|
|
6
|
+
Source: T3MP3ST ``.githooks/pre-push``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class GateResult:
|
|
16
|
+
"""Result of a pre-push gate check."""
|
|
17
|
+
|
|
18
|
+
allowed: bool
|
|
19
|
+
reason: str
|
|
20
|
+
override_used: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PrePushGate:
|
|
24
|
+
"""Gate that blocks pushes unless an override env var is set."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, override_env_var: str = "ALGO_ALLOW_RAW_PUSH") -> None:
|
|
27
|
+
self.override_env_var = override_env_var
|
|
28
|
+
|
|
29
|
+
def check(
|
|
30
|
+
self,
|
|
31
|
+
env_getter: Callable[[str], Optional[str]] = lambda _k: None,
|
|
32
|
+
allow_override: bool = False,
|
|
33
|
+
) -> GateResult:
|
|
34
|
+
"""Check whether a push should be allowed.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
env_getter: Function that returns env var values (defaults to None).
|
|
38
|
+
allow_override: If True, skip the env var check entirely.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
GateResult with allowed=True if push should proceed.
|
|
42
|
+
"""
|
|
43
|
+
if allow_override:
|
|
44
|
+
return GateResult(
|
|
45
|
+
allowed=True,
|
|
46
|
+
reason="Override explicitly granted by caller.",
|
|
47
|
+
override_used=True,
|
|
48
|
+
)
|
|
49
|
+
env_value = env_getter(self.override_env_var)
|
|
50
|
+
if env_value and env_value.lower() in ("1", "true", "yes"):
|
|
51
|
+
return GateResult(
|
|
52
|
+
allowed=True,
|
|
53
|
+
reason=f"Override env var {self.override_env_var} is set.",
|
|
54
|
+
override_used=True,
|
|
55
|
+
)
|
|
56
|
+
return GateResult(
|
|
57
|
+
allowed=False,
|
|
58
|
+
reason=(
|
|
59
|
+
f"Raw push blocked. Set {self.override_env_var}=1 to override, "
|
|
60
|
+
"or use a scrubbed export path."
|
|
61
|
+
),
|
|
62
|
+
override_used=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def require_override(self, env_getter: Callable[[str], Optional[str]] = lambda _k: None) -> bool:
|
|
66
|
+
"""Return True if an override is required to push."""
|
|
67
|
+
result = self.check(env_getter=env_getter)
|
|
68
|
+
return not result.allowed
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Content Prefetch — predictive cache warming via transition matrix.
|
|
2
|
+
|
|
3
|
+
Borrowed from Windows.Networking.BackgroundTransfer.ContentPrefetchTask.dll:
|
|
4
|
+
Windows allows apps to register prefetch tasks that run before the user opens
|
|
5
|
+
the app — warming the cache with content the user is likely to request.
|
|
6
|
+
|
|
7
|
+
This module learns access patterns ("after reading X, user usually reads Y")
|
|
8
|
+
and pre-loads predicted files into an LRU cache before they're requested.
|
|
9
|
+
|
|
10
|
+
Pattern: B33 in ALGO.md.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from collections import Counter, OrderedDict
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AccessRecord:
|
|
22
|
+
"""A single file access record."""
|
|
23
|
+
path: str
|
|
24
|
+
timestamp: float
|
|
25
|
+
action: str = "read"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PredictivePrefetch:
|
|
29
|
+
"""Predict next likely accesses based on temporal transition patterns.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
cache = PredictivePrefetch(max_cache=32)
|
|
33
|
+
content = cache.get("foo.py", loader=read_file)
|
|
34
|
+
# cache learns: after foo.py, user often reads test_foo.py
|
|
35
|
+
# next time foo.py is read, test_foo.py is pre-warmed
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, max_cache: int = 32, max_history: int = 1000):
|
|
39
|
+
self.max_cache = max_cache
|
|
40
|
+
self.max_history = max_history
|
|
41
|
+
self.access_history: list[AccessRecord] = []
|
|
42
|
+
self.prefetch_cache: OrderedDict[str, Any] = OrderedDict()
|
|
43
|
+
self.transition_counts: Counter = Counter()
|
|
44
|
+
# Per-path access frequency (for cold-start predictions)
|
|
45
|
+
self.access_frequency: Counter = Counter()
|
|
46
|
+
|
|
47
|
+
# --- learning ---------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def record_access(self, path: str, action: str = "read") -> None:
|
|
50
|
+
"""Record a file access for pattern learning."""
|
|
51
|
+
now = time.time()
|
|
52
|
+
# Learn transition: previous access → this access
|
|
53
|
+
if self.access_history:
|
|
54
|
+
prev = self.access_history[-1]
|
|
55
|
+
key = f"{prev.path}|{prev.action}->{path}|{action}"
|
|
56
|
+
self.transition_counts[key] += 1
|
|
57
|
+
self.access_history.append(AccessRecord(path, now, action))
|
|
58
|
+
self.access_frequency[path] += 1
|
|
59
|
+
# Bound history
|
|
60
|
+
if len(self.access_history) > self.max_history:
|
|
61
|
+
self.access_history = self.access_history[-self.max_history // 2 :]
|
|
62
|
+
|
|
63
|
+
# --- prediction -------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def predict_next(self, current_path: str, action: str = "read") -> list[str]:
|
|
66
|
+
"""Predict likely next accesses given current access.
|
|
67
|
+
|
|
68
|
+
Returns paths sorted by transition probability (descending).
|
|
69
|
+
"""
|
|
70
|
+
candidates: Counter = Counter()
|
|
71
|
+
prefix = f"{current_path}|{action}->"
|
|
72
|
+
for key, count in self.transition_counts.items():
|
|
73
|
+
if key.startswith(prefix):
|
|
74
|
+
suffix = key[len(prefix):]
|
|
75
|
+
next_path = suffix.split("|")[0]
|
|
76
|
+
candidates[next_path] = count
|
|
77
|
+
|
|
78
|
+
# If no transitions learned, fall back to most-accessed paths
|
|
79
|
+
if not candidates:
|
|
80
|
+
for path, freq in self.access_frequency.most_common(self.max_cache):
|
|
81
|
+
if path != current_path:
|
|
82
|
+
candidates[path] = freq
|
|
83
|
+
|
|
84
|
+
return [p for p, _ in candidates.most_common(self.max_cache)]
|
|
85
|
+
|
|
86
|
+
# --- cache management -------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def warm_cache(self, paths: list[str], loader: Callable[[str], Any]) -> int:
|
|
89
|
+
"""Pre-load predicted paths into cache. Returns number warmed."""
|
|
90
|
+
warmed = 0
|
|
91
|
+
for path in paths:
|
|
92
|
+
if path not in self.prefetch_cache:
|
|
93
|
+
try:
|
|
94
|
+
self.prefetch_cache[path] = loader(path)
|
|
95
|
+
warmed += 1
|
|
96
|
+
except Exception:
|
|
97
|
+
pass # prefetch failures are silent (best-effort)
|
|
98
|
+
self._evict()
|
|
99
|
+
return warmed
|
|
100
|
+
|
|
101
|
+
def prefetch_for(self, current_path: str, loader: Callable[[str], Any]) -> int:
|
|
102
|
+
"""Predict and warm cache for likely-next accesses."""
|
|
103
|
+
predicted = self.predict_next(current_path)
|
|
104
|
+
return self.warm_cache(predicted, loader)
|
|
105
|
+
|
|
106
|
+
def get(self, path: str, loader: Callable[[str], Any]) -> Any:
|
|
107
|
+
"""Get from cache or load on miss. Records access for learning."""
|
|
108
|
+
if path in self.prefetch_cache:
|
|
109
|
+
self.prefetch_cache.move_to_end(path)
|
|
110
|
+
self.record_access(path, "read")
|
|
111
|
+
return self.prefetch_cache[path]
|
|
112
|
+
result = loader(path)
|
|
113
|
+
self.prefetch_cache[path] = result
|
|
114
|
+
self.record_access(path, "read")
|
|
115
|
+
self._evict()
|
|
116
|
+
# After loading, prefetch likely-next files in background
|
|
117
|
+
# (caller can call prefetch_for explicitly for async warming)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def _evict(self) -> None:
|
|
121
|
+
"""Evict oldest entries if over capacity (LRU)."""
|
|
122
|
+
while len(self.prefetch_cache) > self.max_cache:
|
|
123
|
+
self.prefetch_cache.popitem(last=False)
|
|
124
|
+
|
|
125
|
+
# --- queries ----------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def cache_size(self) -> int:
|
|
128
|
+
return len(self.prefetch_cache)
|
|
129
|
+
|
|
130
|
+
def cache_hit_rate(self) -> float:
|
|
131
|
+
"""Calculate cache hit rate from access history."""
|
|
132
|
+
if not self.access_history:
|
|
133
|
+
return 0.0
|
|
134
|
+
hits = sum(
|
|
135
|
+
1 for rec in self.access_history
|
|
136
|
+
if rec.path in self.prefetch_cache
|
|
137
|
+
)
|
|
138
|
+
return hits / len(self.access_history)
|
|
139
|
+
|
|
140
|
+
def cached_paths(self) -> list[str]:
|
|
141
|
+
return list(self.prefetch_cache.keys())
|
|
142
|
+
|
|
143
|
+
def stats(self) -> dict[str, Any]:
|
|
144
|
+
return {
|
|
145
|
+
"cache_size": self.cache_size(),
|
|
146
|
+
"cache_capacity": self.max_cache,
|
|
147
|
+
"history_size": len(self.access_history),
|
|
148
|
+
"transitions_learned": len(self.transition_counts),
|
|
149
|
+
"unique_paths_accessed": len(self.access_frequency),
|
|
150
|
+
"cache_hit_rate": round(self.cache_hit_rate(), 3),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# --- persistence ------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def export_transitions(self) -> dict[str, int]:
|
|
156
|
+
"""Export transition counts for persistence/analysis."""
|
|
157
|
+
return dict(self.transition_counts)
|
|
158
|
+
|
|
159
|
+
def import_transitions(self, data: dict[str, int]) -> None:
|
|
160
|
+
"""Import previously learned transition counts."""
|
|
161
|
+
for key, count in data.items():
|
|
162
|
+
self.transition_counts[key] += count
|
|
163
|
+
|
|
164
|
+
def export_frequency(self) -> dict[str, int]:
|
|
165
|
+
"""Export access frequency for persistence."""
|
|
166
|
+
return dict(self.access_frequency)
|
|
167
|
+
|
|
168
|
+
def import_frequency(self, data: dict[str, int]) -> None:
|
|
169
|
+
"""Import previously learned access frequency."""
|
|
170
|
+
for path, count in data.items():
|
|
171
|
+
self.access_frequency[path] += count
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""B46. Process Framework for Business Workflows (Semantic Kernel Pattern).
|
|
2
|
+
|
|
3
|
+
Models real-world business processes (bid → permit → install → invoice)
|
|
4
|
+
as state machines with typed steps, conditional transitions, retries,
|
|
5
|
+
and failure fallbacks. Processes are serializable for pause/resume.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProcessState(Enum):
|
|
17
|
+
PENDING = "pending"
|
|
18
|
+
RUNNING = "running"
|
|
19
|
+
WAITING = "waiting"
|
|
20
|
+
COMPLETED = "completed"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
SKIPPED = "skipped"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ProcessStep:
|
|
27
|
+
name: str
|
|
28
|
+
action: Callable[[dict], Any]
|
|
29
|
+
inputs: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
outputs: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
state: ProcessState = ProcessState.PENDING
|
|
32
|
+
condition: Callable[[dict], bool] | None = None
|
|
33
|
+
on_success: str | None = None
|
|
34
|
+
on_failure: str | None = None
|
|
35
|
+
retries: int = 0
|
|
36
|
+
max_retries: int = 3
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"name": self.name,
|
|
41
|
+
"state": self.state.value,
|
|
42
|
+
"retries": self.retries,
|
|
43
|
+
"outputs": self.outputs,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Process:
|
|
48
|
+
"""State-machine workflow with conditional transitions."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, name: str):
|
|
51
|
+
self.name = name
|
|
52
|
+
self.steps: dict[str, ProcessStep] = {}
|
|
53
|
+
self.start_step: str | None = None
|
|
54
|
+
self.context: dict[str, Any] = {}
|
|
55
|
+
self.completed: bool = False
|
|
56
|
+
|
|
57
|
+
def add_step(self, step: ProcessStep, is_start: bool = False) -> None:
|
|
58
|
+
self.steps[step.name] = step
|
|
59
|
+
if is_start:
|
|
60
|
+
self.start_step = step.name
|
|
61
|
+
|
|
62
|
+
def run(self, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
63
|
+
"""Execute the process from start to end (or failure)."""
|
|
64
|
+
if context:
|
|
65
|
+
self.context.update(context)
|
|
66
|
+
current = self.start_step
|
|
67
|
+
while current:
|
|
68
|
+
step = self.steps.get(current)
|
|
69
|
+
if not step:
|
|
70
|
+
break
|
|
71
|
+
# Condition gate
|
|
72
|
+
if step.condition and not step.condition(self.context):
|
|
73
|
+
step.state = ProcessState.SKIPPED
|
|
74
|
+
current = step.on_success
|
|
75
|
+
continue
|
|
76
|
+
step.state = ProcessState.RUNNING
|
|
77
|
+
try:
|
|
78
|
+
result = step.action(self.context)
|
|
79
|
+
step.outputs = result if isinstance(result, dict) else {"result": result}
|
|
80
|
+
self.context.update(step.outputs)
|
|
81
|
+
step.state = ProcessState.COMPLETED
|
|
82
|
+
current = step.on_success
|
|
83
|
+
except Exception as e:
|
|
84
|
+
step.retries += 1
|
|
85
|
+
if step.retries < step.max_retries:
|
|
86
|
+
step.state = ProcessState.WAITING
|
|
87
|
+
continue # retry same step
|
|
88
|
+
step.state = ProcessState.FAILED
|
|
89
|
+
self.context[f"{step.name}_error"] = str(e)
|
|
90
|
+
current = step.on_failure
|
|
91
|
+
self.completed = True
|
|
92
|
+
return self.context
|
|
93
|
+
|
|
94
|
+
def status(self) -> dict[str, Any]:
|
|
95
|
+
"""Return current process status."""
|
|
96
|
+
return {
|
|
97
|
+
"name": self.name,
|
|
98
|
+
"completed": self.completed,
|
|
99
|
+
"steps": {name: step.to_dict() for name, step in self.steps.items()},
|
|
100
|
+
"context_keys": list(self.context.keys()),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def serialize(self) -> str:
|
|
104
|
+
"""Serialize process state for persistence."""
|
|
105
|
+
return json.dumps({
|
|
106
|
+
"name": self.name,
|
|
107
|
+
"start_step": self.start_step,
|
|
108
|
+
"completed": self.completed,
|
|
109
|
+
"context": {k: v for k, v in self.context.items() if isinstance(v, (str, int, float, bool, list, dict))},
|
|
110
|
+
"steps": {name: step.to_dict() for name, step in self.steps.items()},
|
|
111
|
+
}, indent=2)
|
|
112
|
+
|
|
113
|
+
def resume_from(self, step_name: str) -> dict[str, Any]:
|
|
114
|
+
"""Resume execution from a specific step."""
|
|
115
|
+
current = step_name
|
|
116
|
+
while current:
|
|
117
|
+
step = self.steps.get(current)
|
|
118
|
+
if not step:
|
|
119
|
+
break
|
|
120
|
+
if step.state == ProcessState.COMPLETED:
|
|
121
|
+
current = step.on_success
|
|
122
|
+
continue
|
|
123
|
+
if step.condition and not step.condition(self.context):
|
|
124
|
+
step.state = ProcessState.SKIPPED
|
|
125
|
+
current = step.on_success
|
|
126
|
+
continue
|
|
127
|
+
step.state = ProcessState.RUNNING
|
|
128
|
+
try:
|
|
129
|
+
result = step.action(self.context)
|
|
130
|
+
step.outputs = result if isinstance(result, dict) else {"result": result}
|
|
131
|
+
self.context.update(step.outputs)
|
|
132
|
+
step.state = ProcessState.COMPLETED
|
|
133
|
+
current = step.on_success
|
|
134
|
+
except Exception as e:
|
|
135
|
+
step.retries += 1
|
|
136
|
+
if step.retries < step.max_retries:
|
|
137
|
+
step.state = ProcessState.WAITING
|
|
138
|
+
continue
|
|
139
|
+
step.state = ProcessState.FAILED
|
|
140
|
+
self.context[f"{step.name}_error"] = str(e)
|
|
141
|
+
current = step.on_failure
|
|
142
|
+
self.completed = True
|
|
143
|
+
return self.context
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── factories for reusable business workflows ────────────────────────
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def make_bid_process() -> Process:
|
|
150
|
+
"""Create a standard bid workflow."""
|
|
151
|
+
p = Process("bid_process")
|
|
152
|
+
|
|
153
|
+
def site_visit(ctx: dict) -> dict:
|
|
154
|
+
return {"site_visited": True, "notes": ctx.get("notes", "")}
|
|
155
|
+
|
|
156
|
+
def estimate(ctx: dict) -> dict:
|
|
157
|
+
return {"estimate_total": ctx.get("estimate_total", 0)}
|
|
158
|
+
|
|
159
|
+
def send_bid(ctx: dict) -> dict:
|
|
160
|
+
return {"bid_sent": True, "bid_amount": ctx.get("estimate_total", 0)}
|
|
161
|
+
|
|
162
|
+
def follow_up(ctx: dict) -> dict:
|
|
163
|
+
return {"follow_up_sent": True}
|
|
164
|
+
|
|
165
|
+
def won(ctx: dict) -> dict:
|
|
166
|
+
return {"status": "won"}
|
|
167
|
+
|
|
168
|
+
def lost(ctx: dict) -> dict:
|
|
169
|
+
return {"status": "lost"}
|
|
170
|
+
|
|
171
|
+
p.add_step(ProcessStep(name="site_visit", action=site_visit, on_success="estimate"), is_start=True)
|
|
172
|
+
p.add_step(ProcessStep(name="estimate", action=estimate, on_success="send_bid"))
|
|
173
|
+
p.add_step(ProcessStep(name="send_bid", action=send_bid, on_success="follow_up"))
|
|
174
|
+
p.add_step(ProcessStep(name="follow_up", action=follow_up, on_success="check_result"))
|
|
175
|
+
p.add_step(ProcessStep(
|
|
176
|
+
name="check_result",
|
|
177
|
+
action=lambda ctx: {"won": ctx.get("won", False)},
|
|
178
|
+
condition=lambda ctx: ctx.get("won", False),
|
|
179
|
+
on_success="won",
|
|
180
|
+
on_failure="lost",
|
|
181
|
+
))
|
|
182
|
+
p.add_step(ProcessStep(name="won", action=won))
|
|
183
|
+
p.add_step(ProcessStep(name="lost", action=lost))
|
|
184
|
+
return p
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def make_permit_process() -> Process:
|
|
188
|
+
"""Create a standard permit workflow."""
|
|
189
|
+
p = Process("permit_process")
|
|
190
|
+
|
|
191
|
+
def apply(ctx: dict) -> dict:
|
|
192
|
+
return {"permit_submitted": True}
|
|
193
|
+
|
|
194
|
+
def review(ctx: dict) -> dict:
|
|
195
|
+
return {"reviewed": True, "approved": ctx.get("approved", False)}
|
|
196
|
+
|
|
197
|
+
def corrections(ctx: dict) -> dict:
|
|
198
|
+
return {"corrections_made": True}
|
|
199
|
+
|
|
200
|
+
def approval(ctx: dict) -> dict:
|
|
201
|
+
return {"permit_approved": True}
|
|
202
|
+
|
|
203
|
+
def schedule_inspection(ctx: dict) -> dict:
|
|
204
|
+
return {"inspection_scheduled": True}
|
|
205
|
+
|
|
206
|
+
p.add_step(ProcessStep(name="apply", action=apply, on_success="review"), is_start=True)
|
|
207
|
+
p.add_step(ProcessStep(
|
|
208
|
+
name="review",
|
|
209
|
+
action=review,
|
|
210
|
+
condition=lambda ctx: ctx.get("approved", False),
|
|
211
|
+
on_success="approval",
|
|
212
|
+
on_failure="corrections",
|
|
213
|
+
))
|
|
214
|
+
p.add_step(ProcessStep(name="corrections", action=corrections, on_success="review"))
|
|
215
|
+
p.add_step(ProcessStep(name="approval", action=approval, on_success="schedule_inspection"))
|
|
216
|
+
p.add_step(ProcessStep(name="schedule_inspection", action=schedule_inspection))
|
|
217
|
+
return p
|