arctx 0.2.0b2__tar.gz

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.
Files changed (106) hide show
  1. arctx-0.2.0b2/.gitignore +30 -0
  2. arctx-0.2.0b2/PKG-INFO +48 -0
  3. arctx-0.2.0b2/README.md +21 -0
  4. arctx-0.2.0b2/pyproject.toml +64 -0
  5. arctx-0.2.0b2/src/arctx/__init__.py +48 -0
  6. arctx-0.2.0b2/src/arctx/core/__init__.py +43 -0
  7. arctx-0.2.0b2/src/arctx/core/_json.py +28 -0
  8. arctx-0.2.0b2/src/arctx/core/append.py +46 -0
  9. arctx-0.2.0b2/src/arctx/core/cuts.py +61 -0
  10. arctx-0.2.0b2/src/arctx/core/graph_view.py +26 -0
  11. arctx-0.2.0b2/src/arctx/core/ids.py +32 -0
  12. arctx-0.2.0b2/src/arctx/core/run/__init__.py +5 -0
  13. arctx-0.2.0b2/src/arctx/core/run/anchor.py +40 -0
  14. arctx-0.2.0b2/src/arctx/core/run/attach.py +49 -0
  15. arctx-0.2.0b2/src/arctx/core/run/cut.py +50 -0
  16. arctx-0.2.0b2/src/arctx/core/run/dump.py +174 -0
  17. arctx-0.2.0b2/src/arctx/core/run/handle.py +163 -0
  18. arctx-0.2.0b2/src/arctx/core/run/outcomes.py +26 -0
  19. arctx-0.2.0b2/src/arctx/core/run/trace.py +63 -0
  20. arctx-0.2.0b2/src/arctx/core/run/transition.py +89 -0
  21. arctx-0.2.0b2/src/arctx/core/run/view.py +34 -0
  22. arctx-0.2.0b2/src/arctx/core/run_graph.py +241 -0
  23. arctx-0.2.0b2/src/arctx/core/schema/__init__.py +28 -0
  24. arctx-0.2.0b2/src/arctx/core/schema/graph.py +40 -0
  25. arctx-0.2.0b2/src/arctx/core/schema/payloads.py +288 -0
  26. arctx-0.2.0b2/src/arctx/core/schema/requirements.py +22 -0
  27. arctx-0.2.0b2/src/arctx/core/schema/snapshots.py +21 -0
  28. arctx-0.2.0b2/src/arctx/core/schema/work.py +79 -0
  29. arctx-0.2.0b2/src/arctx/core/schema/work_helpers.py +297 -0
  30. arctx-0.2.0b2/src/arctx/core/sync/__init__.py +18 -0
  31. arctx-0.2.0b2/src/arctx/core/sync/local.py +405 -0
  32. arctx-0.2.0b2/src/arctx/core/sync/records.py +88 -0
  33. arctx-0.2.0b2/src/arctx/core/sync/shared_store.py +65 -0
  34. arctx-0.2.0b2/src/arctx/core/types.py +30 -0
  35. arctx-0.2.0b2/src/arctx/ext/__init__.py +83 -0
  36. arctx-0.2.0b2/src/arctx/ext/base.py +108 -0
  37. arctx-0.2.0b2/src/arctx/ext/command/__init__.py +49 -0
  38. arctx-0.2.0b2/src/arctx/ext/command/payloads.py +77 -0
  39. arctx-0.2.0b2/src/arctx/ext/command/verbs/__init__.py +1 -0
  40. arctx-0.2.0b2/src/arctx/ext/command/verbs/run.py +135 -0
  41. arctx-0.2.0b2/src/arctx/ext/enabled.py +73 -0
  42. arctx-0.2.0b2/src/arctx/ext/git/__init__.py +158 -0
  43. arctx-0.2.0b2/src/arctx/ext/git/events.py +132 -0
  44. arctx-0.2.0b2/src/arctx/ext/git/helpers/__init__.py +1 -0
  45. arctx-0.2.0b2/src/arctx/ext/git/helpers/attach.py +93 -0
  46. arctx-0.2.0b2/src/arctx/ext/git/helpers/finish.py +353 -0
  47. arctx-0.2.0b2/src/arctx/ext/git/helpers/repo.py +299 -0
  48. arctx-0.2.0b2/src/arctx/ext/git/helpers/session.py +180 -0
  49. arctx-0.2.0b2/src/arctx/ext/git/helpers/start.py +120 -0
  50. arctx-0.2.0b2/src/arctx/ext/git/payloads.py +288 -0
  51. arctx-0.2.0b2/src/arctx/ext/git/queries.py +44 -0
  52. arctx-0.2.0b2/src/arctx/ext/git/verbs/__init__.py +1 -0
  53. arctx-0.2.0b2/src/arctx/ext/git/verbs/_forward_transition.py +244 -0
  54. arctx-0.2.0b2/src/arctx/ext/git/verbs/cherry_pick.py +168 -0
  55. arctx-0.2.0b2/src/arctx/ext/git/verbs/commit.py +92 -0
  56. arctx-0.2.0b2/src/arctx/ext/git/verbs/merge.py +178 -0
  57. arctx-0.2.0b2/src/arctx/ext/git/verbs/reset.py +172 -0
  58. arctx-0.2.0b2/src/arctx/ext/git/verbs/revert.py +205 -0
  59. arctx-0.2.0b2/src/arctx/ext/git/verbs/rewrite.py +105 -0
  60. arctx-0.2.0b2/src/arctx/ext/git/verbs/verify.py +169 -0
  61. arctx-0.2.0b2/src/arctx/paths.py +149 -0
  62. arctx-0.2.0b2/src/arctx/payload_builder.py +153 -0
  63. arctx-0.2.0b2/src/arctx/session/__init__.py +157 -0
  64. arctx-0.2.0b2/src/arctx/storage/__init__.py +14 -0
  65. arctx-0.2.0b2/src/arctx/storage/_cache.py +103 -0
  66. arctx-0.2.0b2/src/arctx/storage/base.py +42 -0
  67. arctx-0.2.0b2/src/arctx/storage/jsonl.py +385 -0
  68. arctx-0.2.0b2/src/arctx/storage/sqlite.py +355 -0
  69. arctx-0.2.0b2/tests/__init__.py +1 -0
  70. arctx-0.2.0b2/tests/core/__init__.py +0 -0
  71. arctx-0.2.0b2/tests/core/run/__init__.py +0 -0
  72. arctx-0.2.0b2/tests/core/run/test_adopt_rewrite.py +236 -0
  73. arctx-0.2.0b2/tests/core/run/test_cherry_pick.py +199 -0
  74. arctx-0.2.0b2/tests/core/run/test_command_extension.py +42 -0
  75. arctx-0.2.0b2/tests/core/run/test_commit.py +201 -0
  76. arctx-0.2.0b2/tests/core/run/test_merge.py +315 -0
  77. arctx-0.2.0b2/tests/core/run/test_parallel_session_guard.py +377 -0
  78. arctx-0.2.0b2/tests/core/run/test_reset.py +422 -0
  79. arctx-0.2.0b2/tests/core/run/test_revert.py +268 -0
  80. arctx-0.2.0b2/tests/core/run/test_verify.py +372 -0
  81. arctx-0.2.0b2/tests/core/schema/__init__.py +0 -0
  82. arctx-0.2.0b2/tests/core/schema/test_branch_payload.py +75 -0
  83. arctx-0.2.0b2/tests/core/schema/test_cherry_pick_payload.py +104 -0
  84. arctx-0.2.0b2/tests/core/schema/test_command_payload.py +28 -0
  85. arctx-0.2.0b2/tests/core/schema/test_merge_payload.py +140 -0
  86. arctx-0.2.0b2/tests/core/schema/test_reset_event.py +76 -0
  87. arctx-0.2.0b2/tests/core/schema/test_revert_payload.py +80 -0
  88. arctx-0.2.0b2/tests/core/schema/test_work_helpers.py +197 -0
  89. arctx-0.2.0b2/tests/core/test_ancestors_of.py +89 -0
  90. arctx-0.2.0b2/tests/core/test_branch_members.py +126 -0
  91. arctx-0.2.0b2/tests/core/test_current_sha.py +117 -0
  92. arctx-0.2.0b2/tests/core/test_cuts.py +98 -0
  93. arctx-0.2.0b2/tests/core/test_dump.py +107 -0
  94. arctx-0.2.0b2/tests/core/test_run_api.py +190 -0
  95. arctx-0.2.0b2/tests/core/test_run_graph.py +157 -0
  96. arctx-0.2.0b2/tests/core/test_schema.py +228 -0
  97. arctx-0.2.0b2/tests/ext/__init__.py +0 -0
  98. arctx-0.2.0b2/tests/ext/test_enabled_persist.py +32 -0
  99. arctx-0.2.0b2/tests/ext/test_extension_base.py +30 -0
  100. arctx-0.2.0b2/tests/ext/test_registry.py +71 -0
  101. arctx-0.2.0b2/tests/fixtures/__init__.py +1 -0
  102. arctx-0.2.0b2/tests/fixtures/dummy_ext.py +43 -0
  103. arctx-0.2.0b2/tests/storage/test_cache.py +69 -0
  104. arctx-0.2.0b2/tests/storage/test_jsonl_store.py +175 -0
  105. arctx-0.2.0b2/tests/storage/test_payload_registry.py +81 -0
  106. arctx-0.2.0b2/tests/storage/test_sqlite_store.py +137 -0
@@ -0,0 +1,30 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .coverage
9
+ htmlcov/
10
+
11
+ # ARCTX generated run/output files
12
+ runs/
13
+ arctx_output/
14
+ .arctx/
15
+ state_round_*.json
16
+ request_*.json
17
+ attempts.jsonl
18
+ decisions.jsonl
19
+ findings.jsonl
20
+ requirements.json
21
+ run.json
22
+
23
+ # Package build artifacts
24
+ dist/
25
+ *.egg-info/
26
+
27
+ # IDE / Agent state directories
28
+ .claude/
29
+ .antigravitycli/
30
+
arctx-0.2.0b2/PKG-INFO ADDED
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: arctx
3
+ Version: 0.2.0b2
4
+ Summary: ARCTX: append-only DAG for thought, work context, and parallel exploration
5
+ Project-URL: Homepage, https://github.com/takumiecd/stag
6
+ Project-URL: Repository, https://github.com/takumiecd/stag
7
+ Author: Takumi Ishida
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: typing-extensions>=4.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: black>=23.0; extra == 'dev'
20
+ Requires-Dist: mypy>=1.0; extra == 'dev'
21
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
22
+ Requires-Dist: pytest>=7.0; extra == 'dev'
23
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
24
+ Provides-Extra: fast
25
+ Requires-Dist: orjson>=3.9; extra == 'fast'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # arctx
29
+
30
+ Python API for ARCTX (Arc + Context) — records the process of optimization and problem-solving.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install arctx
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ import arctx as arctx
42
+
43
+ handle = arctx.init(arctx.Requirement(text="Solve the problem"))
44
+ ```
45
+
46
+ ## Package layout
47
+
48
+ This package provides the core API, storage, and extension framework. The `arctx` command-line tool is in the separate `arctx-cli` package.
@@ -0,0 +1,21 @@
1
+ # arctx
2
+
3
+ Python API for ARCTX (Arc + Context) — records the process of optimization and problem-solving.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install arctx
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import arctx as arctx
15
+
16
+ handle = arctx.init(arctx.Requirement(text="Solve the problem"))
17
+ ```
18
+
19
+ ## Package layout
20
+
21
+ This package provides the core API, storage, and extension framework. The `arctx` command-line tool is in the separate `arctx-cli` package.
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "arctx"
7
+ version = "0.2.0b2"
8
+ description = "ARCTX: append-only DAG for thought, work context, and parallel exploration"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Takumi Ishida"},
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+ dependencies = [
25
+ "typing-extensions>=4.0.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ fast = [
30
+ "orjson>=3.9",
31
+ ]
32
+ dev = [
33
+ "pytest>=7.0",
34
+ "pytest-asyncio>=0.21",
35
+ "black>=23.0",
36
+ "ruff>=0.1.0",
37
+ "mypy>=1.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/takumiecd/stag"
42
+ Repository = "https://github.com/takumiecd/stag"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/arctx"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ pythonpath = ["src"]
50
+
51
+ [tool.black]
52
+ line-length = 100
53
+ target-version = ["py310"]
54
+
55
+ [tool.ruff]
56
+ line-length = 100
57
+ select = ["E", "F", "W", "I", "N", "D", "UP", "B", "C4", "SIM"]
58
+ ignore = ["D100", "D104", "D203", "D213"]
59
+
60
+ [tool.mypy]
61
+ python_version = "3.10"
62
+ warn_return_any = true
63
+ warn_unused_configs = true
64
+ disallow_untyped_defs = true
@@ -0,0 +1,48 @@
1
+ """arctx: records the process of optimization and problem-solving."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from arctx.core.graph_view import GraphView
6
+ from arctx.core.run import RunHandle
7
+ from arctx.core.run import init as _core_init
8
+ from arctx.core.run_graph import RunGraph
9
+ from arctx.core.schema import (
10
+ CutPayload,
11
+ Node,
12
+ NodePayload,
13
+ Payload,
14
+ PayloadBase,
15
+ Requirement,
16
+ TraceContext,
17
+ Transition,
18
+ TransitionPayload,
19
+ register_payload_class,
20
+ )
21
+ from arctx.core.types import (
22
+ TargetKind,
23
+ )
24
+
25
+ __version__ = "0.2.0b2"
26
+
27
+
28
+ def init(requirement: Requirement, *, run_id: str | None = None) -> RunHandle:
29
+ """Create a core run handle without enabling extensions."""
30
+ return _core_init(requirement, run_id=run_id)
31
+
32
+ __all__ = [
33
+ "CutPayload",
34
+ "GraphView",
35
+ "Node",
36
+ "NodePayload",
37
+ "Payload",
38
+ "PayloadBase",
39
+ "Requirement",
40
+ "RunGraph",
41
+ "RunHandle",
42
+ "TargetKind",
43
+ "TraceContext",
44
+ "Transition",
45
+ "TransitionPayload",
46
+ "init",
47
+ "register_payload_class",
48
+ ]
@@ -0,0 +1,43 @@
1
+ """Core graph model."""
2
+
3
+ from arctx.core.graph_view import GraphView
4
+ from arctx.core.ids import opaque_id, sequential_id, slugify, timestamp_id
5
+ from arctx.core.run import RunHandle, init
6
+ from arctx.core.run_graph import RunGraph
7
+ from arctx.core.schema import (
8
+ CutPayload,
9
+ Node,
10
+ NodePayload,
11
+ Payload,
12
+ PayloadBase,
13
+ Requirement,
14
+ TraceContext,
15
+ Transition,
16
+ TransitionPayload,
17
+ register_payload_class,
18
+ )
19
+ from arctx.core.types import (
20
+ TargetKind,
21
+ )
22
+
23
+ __all__ = [
24
+ "CutPayload",
25
+ "GraphView",
26
+ "Node",
27
+ "NodePayload",
28
+ "Payload",
29
+ "PayloadBase",
30
+ "Requirement",
31
+ "RunGraph",
32
+ "RunHandle",
33
+ "TargetKind",
34
+ "TraceContext",
35
+ "Transition",
36
+ "TransitionPayload",
37
+ "init",
38
+ "opaque_id",
39
+ "register_payload_class",
40
+ "sequential_id",
41
+ "slugify",
42
+ "timestamp_id",
43
+ ]
@@ -0,0 +1,28 @@
1
+ """Fast JSON helpers with orjson fallback.
2
+
3
+ Used by storage hot paths. CLI output formatting should keep using
4
+ the stdlib `json` module directly for ensure_ascii / indent control.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ try:
9
+ import orjson as _orjson
10
+
11
+ def loads(s: str | bytes) -> object:
12
+ return _orjson.loads(s)
13
+
14
+ def dumps(obj: object) -> str:
15
+ return _orjson.dumps(obj, option=_orjson.OPT_SORT_KEYS).decode("utf-8")
16
+
17
+ HAVE_ORJSON: bool = True
18
+
19
+ except ImportError:
20
+ import json as _json
21
+
22
+ def loads(s: str | bytes) -> object: # type: ignore[misc]
23
+ return _json.loads(s)
24
+
25
+ def dumps(obj: object) -> str: # type: ignore[misc]
26
+ return _json.dumps(obj, ensure_ascii=False, sort_keys=True)
27
+
28
+ HAVE_ORJSON = False
@@ -0,0 +1,46 @@
1
+ """Append-only storage batches for concurrent writers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal, Union
7
+
8
+ from arctx.core.graph_view import GraphView
9
+ from arctx.core.schema.graph import Node, Transition
10
+ from arctx.core.schema.payloads import PayloadBase
11
+ from arctx.core.schema.work import WorkEvent, WorkSession
12
+
13
+ GraphRecordKind = Literal["node", "transition", "payload", "view"]
14
+ GraphRecord = Union[Node, Transition, PayloadBase, GraphView]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class GraphRecordEnvelope:
19
+ """A graph record plus the table/category it belongs to."""
20
+
21
+ record_kind: GraphRecordKind
22
+ record_id: str
23
+ record: GraphRecord
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class AppendBatch:
28
+ """One atomic append unit for a run."""
29
+
30
+ run_id: str
31
+ user_id: str
32
+ work_session_id: str
33
+ records: tuple[GraphRecordEnvelope, ...]
34
+ work_session: WorkSession
35
+ events: tuple[WorkEvent, ...]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class AppendResult:
40
+ """Result returned after an append batch is committed."""
41
+
42
+ event_id: str
43
+ event_seq: int
44
+ record_ids: tuple[str, ...]
45
+ event_ids: tuple[str, ...] = ()
46
+ event_seqs: tuple[int, ...] = ()
@@ -0,0 +1,61 @@
1
+ """Read-time computation of inactive transitions and nodes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from arctx.core.run_graph import RunGraph
6
+ from arctx.core.schema.payloads import CutPayload
7
+
8
+
9
+ def _cut_payloads(graph: RunGraph) -> list[CutPayload]:
10
+ return [p for p in graph.payloads.values() if isinstance(p, CutPayload)]
11
+
12
+
13
+ def cut_transition_ids(graph: RunGraph) -> set[str]:
14
+ return {p.target_id for p in _cut_payloads(graph) if p.target_kind == "transition"}
15
+
16
+
17
+ def cut_node_ids(graph: RunGraph) -> set[str]:
18
+ return {p.target_id for p in _cut_payloads(graph) if p.target_kind == "node"}
19
+
20
+
21
+ def _compute_inactive(graph: RunGraph) -> tuple[set[str], set[str]]:
22
+ inactive_transitions: set[str] = set(cut_transition_ids(graph))
23
+ inactive_nodes: set[str] = set(cut_node_ids(graph))
24
+
25
+ frontier_nodes = list(inactive_nodes)
26
+ frontier_transitions = list(inactive_transitions)
27
+
28
+ while frontier_nodes or frontier_transitions:
29
+ while frontier_transitions:
30
+ transition_id = frontier_transitions.pop()
31
+ out = graph.transition_output(transition_id)
32
+ if out and out not in inactive_nodes:
33
+ inactive_nodes.add(out)
34
+ frontier_nodes.append(out)
35
+
36
+ while frontier_nodes:
37
+ node_id = frontier_nodes.pop()
38
+ for transition_id in graph.transitions_from_node(node_id):
39
+ if transition_id not in inactive_transitions:
40
+ inactive_transitions.add(transition_id)
41
+ frontier_transitions.append(transition_id)
42
+
43
+ return inactive_transitions, inactive_nodes
44
+
45
+
46
+ def inactive_transition_ids(graph: RunGraph) -> set[str]:
47
+ inactive_transitions, _ = _compute_inactive(graph)
48
+ return inactive_transitions
49
+
50
+
51
+ def inactive_node_ids(graph: RunGraph) -> set[str]:
52
+ _, inactive_nodes = _compute_inactive(graph)
53
+ return inactive_nodes
54
+
55
+
56
+ def is_active_node(graph: RunGraph, node_id: str) -> bool:
57
+ return node_id not in inactive_node_ids(graph)
58
+
59
+
60
+ def is_inactive_transition(graph: RunGraph, transition_id: str) -> bool:
61
+ return transition_id in inactive_transition_ids(graph)
@@ -0,0 +1,26 @@
1
+ """GraphView: a named label anchored to a root node in RunGraph.
2
+
3
+ The contents of a view are determined at read time by reachability from
4
+ root_node_id via output transitions. No membership sets are stored.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+ from arctx.core.types import JSONValue, to_jsonable
12
+
13
+
14
+ @dataclass
15
+ class GraphView:
16
+ """A named label for a subgraph rooted at a single node."""
17
+
18
+ view_id: str
19
+ name: str
20
+ root_node_id: str
21
+ metadata: dict[str, JSONValue] = field(default_factory=dict)
22
+
23
+ def to_dict(self) -> dict[str, JSONValue]:
24
+ d = to_jsonable(self)
25
+ assert isinstance(d, dict)
26
+ return d # type: ignore[return-value]
@@ -0,0 +1,32 @@
1
+ """Identifier helpers for run and transition records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+
9
+
10
+ _SAFE_ID = re.compile(r"[^a-zA-Z0-9_.-]+")
11
+
12
+
13
+ def slugify(value: str, fallback: str = "item") -> str:
14
+ """Return a stable filesystem-safe slug."""
15
+ slug = _SAFE_ID.sub("_", value.strip()).strip("._-").lower()
16
+ return slug or fallback
17
+
18
+
19
+ def timestamp_id(prefix: str, now: datetime | None = None) -> str:
20
+ """Return a timestamp-based id."""
21
+ current = now or datetime.now(timezone.utc)
22
+ return f"{slugify(prefix)}_{current.strftime('%Y%m%d_%H%M%S')}"
23
+
24
+
25
+ def sequential_id(prefix: str, index: int, width: int = 4) -> str:
26
+ """Return ids such as ``state_0001``."""
27
+ return f"{slugify(prefix)}_{index:0{width}d}"
28
+
29
+
30
+ def opaque_id(prefix: str) -> str:
31
+ """Return a collision-resistant opaque id with a readable kind prefix."""
32
+ return f"{slugify(prefix)}_{uuid.uuid4().hex}"
@@ -0,0 +1,5 @@
1
+ """Run handle and initialization."""
2
+
3
+ from arctx.core.run.handle import RunHandle, init
4
+
5
+ __all__ = ["RunHandle", "init"]
@@ -0,0 +1,40 @@
1
+ """RunHandle.anchor implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from arctx.core.schema.graph import Node
6
+ from arctx.core.schema.payloads import TransitionPayload
7
+
8
+
9
+ def anchor_impl(
10
+ self,
11
+ from_node_id: str,
12
+ label: str,
13
+ *,
14
+ metadata: dict | None = None,
15
+ user_id: str | None = None,
16
+ work_session_id: str | None = None,
17
+ ) -> Node:
18
+ """Create a lightweight scope anchor node from an existing node.
19
+
20
+ An anchor is a Transition with type="anchor" and a generated output node.
21
+ The output node can then be used as a shared branching point for experiments.
22
+ """
23
+ meta = dict(metadata or {})
24
+ meta.setdefault("kind", "anchor")
25
+ meta.setdefault("label", label)
26
+
27
+ payload = TransitionPayload(
28
+ payload_id="pending",
29
+ target_id="pending",
30
+ type="anchor",
31
+ content={"label": label},
32
+ metadata=meta,
33
+ )
34
+ transition = self.transition(
35
+ [from_node_id],
36
+ payload,
37
+ user_id=user_id,
38
+ work_session_id=work_session_id,
39
+ )
40
+ return self.run_graph.nodes[transition.output_node_id]
@@ -0,0 +1,49 @@
1
+ """RunHandle.attach implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from arctx.core.schema.payloads import PayloadBase
6
+ from arctx.core.run.transition import _clone_payload
7
+
8
+
9
+ def attach_impl(
10
+ self,
11
+ node_id: str,
12
+ payload: PayloadBase,
13
+ *,
14
+ user_id: str | None = None,
15
+ work_session_id: str | None = None,
16
+ ) -> PayloadBase:
17
+ """Attach a node-targeting payload to a node.
18
+
19
+ *payload* must be a node-targeting payload (target_kind="node").
20
+ Returns the attached payload (with a freshly minted payload_id).
21
+ """
22
+ if payload.target_kind != "node":
23
+ raise ValueError(
24
+ f"attach() requires a node-targeting payload "
25
+ f"(target_kind='node'), got {payload.target_kind!r}"
26
+ )
27
+ if node_id not in self.run_graph.nodes:
28
+ raise KeyError(f"unknown node_id: {node_id}")
29
+
30
+ cloned = _clone_payload(payload, self._next_id("pl"), node_id)
31
+ self.run_graph.attach_payload(cloned)
32
+ self.record_work_event(
33
+ user_id=user_id,
34
+ work_session_id=work_session_id,
35
+ event_type="payload_attached",
36
+ target_kind="node",
37
+ target_id=node_id,
38
+ created_records=(cloned.payload_id,),
39
+ summary=_node_payload_summary(cloned),
40
+ )
41
+ return cloned
42
+
43
+
44
+ def _node_payload_summary(payload: PayloadBase) -> str | None:
45
+ for attr in ("type", "text"):
46
+ val = getattr(payload, attr, None)
47
+ if isinstance(val, str) and val:
48
+ return val
49
+ return None
@@ -0,0 +1,50 @@
1
+ """RunHandle.cut implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from arctx.core.cuts import cut_node_ids, cut_transition_ids
8
+ from arctx.core.schema.payloads import CutPayload
9
+
10
+
11
+ def cut_impl(
12
+ self,
13
+ target_id: str,
14
+ *,
15
+ target_kind: Literal["node", "transition"],
16
+ reason: str | None = None,
17
+ user_id: str | None = None,
18
+ work_session_id: str | None = None,
19
+ ) -> CutPayload:
20
+ """Append a CutPayload to mark a Node or Transition inactive."""
21
+ if target_kind == "node":
22
+ if target_id not in self.run_graph.nodes:
23
+ raise KeyError(f"unknown node_id: {target_id}")
24
+ if target_id in cut_node_ids(self.run_graph):
25
+ raise ValueError(f"node already cut: {target_id}")
26
+ elif target_kind == "transition":
27
+ if target_id not in self.run_graph.transitions:
28
+ raise KeyError(f"unknown transition_id: {target_id}")
29
+ if target_id in cut_transition_ids(self.run_graph):
30
+ raise ValueError(f"transition already cut: {target_id}")
31
+ else:
32
+ raise ValueError(f"invalid target_kind: {target_kind!r}")
33
+
34
+ cut = CutPayload(
35
+ payload_id=self._next_id("pl"),
36
+ target_id=target_id,
37
+ target_kind=target_kind,
38
+ reason=reason,
39
+ )
40
+ self.run_graph.attach_payload(cut)
41
+ self.record_work_event(
42
+ user_id=user_id,
43
+ work_session_id=work_session_id,
44
+ event_type="cut_added",
45
+ target_kind=target_kind,
46
+ target_id=target_id,
47
+ created_records=(cut.payload_id,),
48
+ summary=reason,
49
+ )
50
+ return cut