intent-cli-python 1.3.0__tar.gz → 2.0.0__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 (42) hide show
  1. {intent_cli_python-1.3.0/intent_cli_python.egg-info → intent_cli_python-2.0.0}/PKG-INFO +20 -8
  2. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/README.md +19 -7
  3. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/pyproject.toml +3 -8
  4. intent_cli_python-2.0.0/src/intent_cli/__init__.py +38 -0
  5. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0/src/intent_cli_python.egg-info}/PKG-INFO +20 -8
  6. intent_cli_python-2.0.0/src/intent_cli_python.egg-info/SOURCES.txt +22 -0
  7. intent_cli_python-2.0.0/src/intent_cli_python.egg-info/entry_points.txt +2 -0
  8. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0/src}/intent_cli_python.egg-info/top_level.txt +0 -1
  9. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/tests/test_cli.py +28 -3
  10. intent_cli_python-1.3.0/apps/__init__.py +0 -1
  11. intent_cli_python-1.3.0/apps/inthub_api/__init__.py +0 -1
  12. intent_cli_python-1.3.0/apps/inthub_api/__main__.py +0 -4
  13. intent_cli_python-1.3.0/apps/inthub_api/common.py +0 -43
  14. intent_cli_python-1.3.0/apps/inthub_api/db.py +0 -47
  15. intent_cli_python-1.3.0/apps/inthub_api/ingest.py +0 -170
  16. intent_cli_python-1.3.0/apps/inthub_api/queries.py +0 -366
  17. intent_cli_python-1.3.0/apps/inthub_api/server.py +0 -168
  18. intent_cli_python-1.3.0/apps/inthub_api/store.py +0 -31
  19. intent_cli_python-1.3.0/apps/inthub_web/__init__.py +0 -1
  20. intent_cli_python-1.3.0/apps/inthub_web/__main__.py +0 -4
  21. intent_cli_python-1.3.0/apps/inthub_web/server.py +0 -87
  22. intent_cli_python-1.3.0/apps/inthub_web/static/app.js +0 -745
  23. intent_cli_python-1.3.0/apps/inthub_web/static/index.html +0 -123
  24. intent_cli_python-1.3.0/apps/inthub_web/static/styles.css +0 -490
  25. intent_cli_python-1.3.0/intent_cli_python.egg-info/SOURCES.txt +0 -37
  26. intent_cli_python-1.3.0/intent_cli_python.egg-info/entry_points.txt +0 -4
  27. intent_cli_python-1.3.0/src/intent_cli/__init__.py +0 -8
  28. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/LICENSE +0 -0
  29. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/setup.cfg +0 -0
  30. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/__main__.py +0 -0
  31. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/cli.py +0 -0
  32. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/commands/__init__.py +0 -0
  33. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/commands/common.py +0 -0
  34. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/commands/core.py +0 -0
  35. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/commands/hub.py +0 -0
  36. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/hub/__init__.py +0 -0
  37. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/hub/client.py +0 -0
  38. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/hub/payload.py +0 -0
  39. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/hub/runtime.py +0 -0
  40. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/output.py +0 -0
  41. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0}/src/intent_cli/store.py +0 -0
  42. {intent_cli_python-1.3.0 → intent_cli_python-2.0.0/src}/intent_cli_python.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intent-cli-python
3
- Version: 1.3.0
3
+ Version: 2.0.0
4
4
  Summary: Semantic history for agent-driven development. Records what you did and why.
5
5
  Author: Zeng Deyang
6
6
  License-Expression: MIT
@@ -72,16 +72,17 @@ pip install intent-cli-python
72
72
 
73
73
  Requires Python 3.9+ and Git.
74
74
 
75
- ### Run the local IntHub shell
75
+ ### IntHub boundary
76
76
 
77
- The first read-only IntHub shell now ships with the package:
77
+ `pipx install intent-cli-python` installs the CLI only.
78
78
 
79
- ```bash
80
- inthub-api --db-path .inthub/inthub.db
81
- inthub-web --api-base-url http://127.0.0.1:8000
82
- ```
79
+ This repository is the umbrella project for both `Intent` and `IntHub`, but the distribution boundary is narrower than the repository boundary:
83
80
 
84
- If you are running from source instead of an installed package, the same entrypoints are available with:
81
+ - PyPI ships only the `itt` CLI
82
+ - IntHub Web is a separate static frontend and is a good fit for GitHub Pages
83
+ - IntHub API is a separate service and is not part of the PyPI package
84
+
85
+ If you are running IntHub from source inside this repository, the current local entrypoints are:
85
86
 
86
87
  ```bash
87
88
  python -m apps.inthub_api --db-path .inthub/inthub.db
@@ -90,6 +91,17 @@ python -m apps.inthub_web --api-base-url http://127.0.0.1:8000
90
91
 
91
92
  Then use `itt hub login`, `itt hub link`, and `itt hub sync` from a local Intent workspace to populate the read-only IntHub project view.
92
93
 
94
+ ### Versioning and releases
95
+
96
+ `Intent` is the umbrella project and monorepo. It does not maintain one shared project version anymore.
97
+
98
+ Release versions now belong to concrete deliverables:
99
+
100
+ - CLI releases use the PyPI package version from `pyproject.toml` and Git tags like `cli-v2.0.0`
101
+ - IntHub releases use their own track and Git tags like `hub-v0.1.0`
102
+
103
+ Historical bare tags such as `v1.3.0` remain as history, but new releases use deliverable-prefixed tags.
104
+
93
105
  ### Install the skills.sh skill
94
106
 
95
107
  ```bash
@@ -48,16 +48,17 @@ pip install intent-cli-python
48
48
 
49
49
  Requires Python 3.9+ and Git.
50
50
 
51
- ### Run the local IntHub shell
51
+ ### IntHub boundary
52
52
 
53
- The first read-only IntHub shell now ships with the package:
53
+ `pipx install intent-cli-python` installs the CLI only.
54
54
 
55
- ```bash
56
- inthub-api --db-path .inthub/inthub.db
57
- inthub-web --api-base-url http://127.0.0.1:8000
58
- ```
55
+ This repository is the umbrella project for both `Intent` and `IntHub`, but the distribution boundary is narrower than the repository boundary:
59
56
 
60
- If you are running from source instead of an installed package, the same entrypoints are available with:
57
+ - PyPI ships only the `itt` CLI
58
+ - IntHub Web is a separate static frontend and is a good fit for GitHub Pages
59
+ - IntHub API is a separate service and is not part of the PyPI package
60
+
61
+ If you are running IntHub from source inside this repository, the current local entrypoints are:
61
62
 
62
63
  ```bash
63
64
  python -m apps.inthub_api --db-path .inthub/inthub.db
@@ -66,6 +67,17 @@ python -m apps.inthub_web --api-base-url http://127.0.0.1:8000
66
67
 
67
68
  Then use `itt hub login`, `itt hub link`, and `itt hub sync` from a local Intent workspace to populate the read-only IntHub project view.
68
69
 
70
+ ### Versioning and releases
71
+
72
+ `Intent` is the umbrella project and monorepo. It does not maintain one shared project version anymore.
73
+
74
+ Release versions now belong to concrete deliverables:
75
+
76
+ - CLI releases use the PyPI package version from `pyproject.toml` and Git tags like `cli-v2.0.0`
77
+ - IntHub releases use their own track and Git tags like `hub-v0.1.0`
78
+
79
+ Historical bare tags such as `v1.3.0` remain as history, but new releases use deliverable-prefixed tags.
80
+
69
81
  ### Install the skills.sh skill
70
82
 
71
83
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "intent-cli-python"
7
- version = "1.3.0"
7
+ version = "2.0.0"
8
8
  description = "Semantic history for agent-driven development. Records what you did and why."
9
9
  requires-python = ">=3.9"
10
10
  readme = "README.md"
@@ -32,12 +32,7 @@ Repository = "https://github.com/dozybot001/Intent"
32
32
 
33
33
  [project.scripts]
34
34
  itt = "intent_cli.cli:main"
35
- inthub-api = "apps.inthub_api.server:main"
36
- inthub-web = "apps.inthub_web.server:main"
37
35
 
38
36
  [tool.setuptools.packages.find]
39
- where = ["src", "."]
40
- include = ["intent_cli*", "apps*"]
41
-
42
- [tool.setuptools.package-data]
43
- "apps.inthub_web" = ["static/*.html", "static/*.css", "static/*.js"]
37
+ where = ["src"]
38
+ include = ["intent_cli*"]
@@ -0,0 +1,38 @@
1
+ """Intent CLI — semantic history for agent-driven development."""
2
+
3
+ import re
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from pathlib import Path
6
+
7
+
8
+ def _read_source_version():
9
+ pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
10
+ if not pyproject.exists():
11
+ return None
12
+ text = pyproject.read_text(encoding="utf-8")
13
+ in_project = False
14
+ for line in text.splitlines():
15
+ stripped = line.strip()
16
+ if stripped == "[project]":
17
+ in_project = True
18
+ continue
19
+ if in_project and stripped.startswith("["):
20
+ break
21
+ if in_project:
22
+ match = re.match(r'version\s*=\s*"([^"]+)"', stripped)
23
+ if match:
24
+ return match.group(1)
25
+ return None
26
+
27
+
28
+ def _resolve_version():
29
+ source_version = _read_source_version()
30
+ if source_version is not None:
31
+ return source_version
32
+ try:
33
+ return version("intent-cli-python")
34
+ except PackageNotFoundError:
35
+ return "0.0.0"
36
+
37
+
38
+ __version__ = _resolve_version()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intent-cli-python
3
- Version: 1.3.0
3
+ Version: 2.0.0
4
4
  Summary: Semantic history for agent-driven development. Records what you did and why.
5
5
  Author: Zeng Deyang
6
6
  License-Expression: MIT
@@ -72,16 +72,17 @@ pip install intent-cli-python
72
72
 
73
73
  Requires Python 3.9+ and Git.
74
74
 
75
- ### Run the local IntHub shell
75
+ ### IntHub boundary
76
76
 
77
- The first read-only IntHub shell now ships with the package:
77
+ `pipx install intent-cli-python` installs the CLI only.
78
78
 
79
- ```bash
80
- inthub-api --db-path .inthub/inthub.db
81
- inthub-web --api-base-url http://127.0.0.1:8000
82
- ```
79
+ This repository is the umbrella project for both `Intent` and `IntHub`, but the distribution boundary is narrower than the repository boundary:
83
80
 
84
- If you are running from source instead of an installed package, the same entrypoints are available with:
81
+ - PyPI ships only the `itt` CLI
82
+ - IntHub Web is a separate static frontend and is a good fit for GitHub Pages
83
+ - IntHub API is a separate service and is not part of the PyPI package
84
+
85
+ If you are running IntHub from source inside this repository, the current local entrypoints are:
85
86
 
86
87
  ```bash
87
88
  python -m apps.inthub_api --db-path .inthub/inthub.db
@@ -90,6 +91,17 @@ python -m apps.inthub_web --api-base-url http://127.0.0.1:8000
90
91
 
91
92
  Then use `itt hub login`, `itt hub link`, and `itt hub sync` from a local Intent workspace to populate the read-only IntHub project view.
92
93
 
94
+ ### Versioning and releases
95
+
96
+ `Intent` is the umbrella project and monorepo. It does not maintain one shared project version anymore.
97
+
98
+ Release versions now belong to concrete deliverables:
99
+
100
+ - CLI releases use the PyPI package version from `pyproject.toml` and Git tags like `cli-v2.0.0`
101
+ - IntHub releases use their own track and Git tags like `hub-v0.1.0`
102
+
103
+ Historical bare tags such as `v1.3.0` remain as history, but new releases use deliverable-prefixed tags.
104
+
93
105
  ### Install the skills.sh skill
94
106
 
95
107
  ```bash
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/intent_cli/__init__.py
5
+ src/intent_cli/__main__.py
6
+ src/intent_cli/cli.py
7
+ src/intent_cli/output.py
8
+ src/intent_cli/store.py
9
+ src/intent_cli/commands/__init__.py
10
+ src/intent_cli/commands/common.py
11
+ src/intent_cli/commands/core.py
12
+ src/intent_cli/commands/hub.py
13
+ src/intent_cli/hub/__init__.py
14
+ src/intent_cli/hub/client.py
15
+ src/intent_cli/hub/payload.py
16
+ src/intent_cli/hub/runtime.py
17
+ src/intent_cli_python.egg-info/PKG-INFO
18
+ src/intent_cli_python.egg-info/SOURCES.txt
19
+ src/intent_cli_python.egg-info/dependency_links.txt
20
+ src/intent_cli_python.egg-info/entry_points.txt
21
+ src/intent_cli_python.egg-info/top_level.txt
22
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ itt = intent_cli.cli:main
@@ -2,11 +2,11 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import re
5
6
  import subprocess
6
7
  import sys
7
8
  import tempfile
8
9
  import threading
9
- from importlib import metadata
10
10
  from http.server import ThreadingHTTPServer
11
11
  from pathlib import Path
12
12
  from urllib.request import urlopen
@@ -16,6 +16,9 @@ import pytest
16
16
  from apps.inthub_api.server import make_handler as make_inthub_api_handler
17
17
  from apps.inthub_web.server import make_handler as make_inthub_web_handler
18
18
 
19
+ REPO_ROOT = Path(__file__).resolve().parents[1]
20
+ SOURCE_PATHS = [str(REPO_ROOT), str(REPO_ROOT / "src")]
21
+
19
22
 
20
23
  @pytest.fixture
21
24
  def workspace(tmp_path):
@@ -64,9 +67,14 @@ def inthub_web_server(inthub_server):
64
67
 
65
68
  def _run(cwd, *args):
66
69
  """Run itt command and return parsed JSON."""
70
+ env = os.environ.copy()
71
+ existing = env.get("PYTHONPATH")
72
+ env["PYTHONPATH"] = os.pathsep.join(
73
+ SOURCE_PATHS + ([existing] if existing else [])
74
+ )
67
75
  r = subprocess.run(
68
76
  [sys.executable, "-m", "intent_cli", *args],
69
- cwd=cwd, capture_output=True, text=True,
77
+ cwd=cwd, capture_output=True, text=True, env=env,
70
78
  )
71
79
  return json.loads(r.stdout)
72
80
 
@@ -83,6 +91,23 @@ def _get_json(url):
83
91
  return json.loads(resp.read().decode("utf-8"))
84
92
 
85
93
 
94
+ def _expected_cli_version():
95
+ text = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
96
+ in_project = False
97
+ for line in text.splitlines():
98
+ stripped = line.strip()
99
+ if stripped == "[project]":
100
+ in_project = True
101
+ continue
102
+ if in_project and stripped.startswith("["):
103
+ break
104
+ if in_project:
105
+ match = re.match(r'version\s*=\s*"([^"]+)"', stripped)
106
+ if match:
107
+ return match.group(1)
108
+ raise AssertionError("Could not find project.version in pyproject.toml")
109
+
110
+
86
111
  # ---------------------------------------------------------------------------
87
112
  # Global commands
88
113
  # ---------------------------------------------------------------------------
@@ -91,7 +116,7 @@ class TestGlobal:
91
116
  def test_version(self, workspace):
92
117
  r = _run(workspace, "version")
93
118
  assert r["ok"] is True
94
- assert r["result"]["version"] == metadata.version("intent-cli-python")
119
+ assert r["result"]["version"] == _expected_cli_version()
95
120
 
96
121
  def test_init_already_exists(self, workspace):
97
122
  r = _run(workspace, "init")
@@ -1 +0,0 @@
1
- """App packages for the Intent monorepo."""
@@ -1 +0,0 @@
1
- """IntHub API package."""
@@ -1,4 +0,0 @@
1
- from apps.inthub_api.server import main
2
-
3
-
4
- main()
@@ -1,43 +0,0 @@
1
- """Shared helpers for the IntHub API."""
2
-
3
- import uuid
4
- from datetime import datetime, timezone
5
-
6
-
7
- def now_utc():
8
- return datetime.now(timezone.utc).isoformat()
9
-
10
-
11
- def new_id(prefix):
12
- return f"{prefix}_{uuid.uuid4().hex[:12]}"
13
-
14
-
15
- def make_remote_object_id(workspace_id, local_object_id):
16
- return f"{workspace_id}__{local_object_id}"
17
-
18
-
19
- def split_remote_object_id(remote_object_id):
20
- parts = remote_object_id.split("__", 1)
21
- if len(parts) != 2:
22
- raise ValueError("Invalid remote object ID.")
23
- return parts[0], parts[1]
24
-
25
-
26
- class APIError(Exception):
27
- def __init__(self, code, message, status=400, details=None):
28
- super().__init__(message)
29
- self.code = code
30
- self.message = message
31
- self.status = status
32
- self.details = details or {}
33
-
34
-
35
- def require_repo(repo):
36
- required = ("provider", "repo_id", "owner", "name")
37
- missing = [key for key in required if not repo.get(key)]
38
- if missing:
39
- raise APIError(
40
- "INVALID_INPUT",
41
- f"Missing repo fields: {', '.join(missing)}.",
42
- status=400,
43
- )
@@ -1,47 +0,0 @@
1
- """SQLite helpers for the IntHub API."""
2
-
3
- import sqlite3
4
- from pathlib import Path
5
-
6
-
7
- def connect(db_path):
8
- path = Path(db_path)
9
- path.parent.mkdir(parents=True, exist_ok=True)
10
- conn = sqlite3.connect(path, check_same_thread=False)
11
- conn.row_factory = sqlite3.Row
12
- init_db(conn)
13
- return conn
14
-
15
-
16
- def init_db(conn):
17
- conn.executescript(
18
- """
19
- CREATE TABLE IF NOT EXISTS projects (
20
- id TEXT PRIMARY KEY,
21
- name TEXT NOT NULL,
22
- provider TEXT NOT NULL,
23
- repo_id TEXT NOT NULL UNIQUE,
24
- owner TEXT NOT NULL,
25
- repo_name TEXT NOT NULL,
26
- created_at TEXT NOT NULL
27
- );
28
-
29
- CREATE TABLE IF NOT EXISTS workspaces (
30
- id TEXT PRIMARY KEY,
31
- project_id TEXT NOT NULL,
32
- provider TEXT NOT NULL,
33
- repo_id TEXT NOT NULL,
34
- created_at TEXT NOT NULL
35
- );
36
-
37
- CREATE TABLE IF NOT EXISTS sync_batches (
38
- id TEXT PRIMARY KEY,
39
- project_id TEXT NOT NULL,
40
- workspace_id TEXT NOT NULL,
41
- generated_at TEXT NOT NULL,
42
- accepted_at TEXT NOT NULL,
43
- payload_json TEXT NOT NULL
44
- );
45
- """
46
- )
47
- conn.commit()
@@ -1,170 +0,0 @@
1
- """Write-path logic for the IntHub API."""
2
-
3
- import json
4
-
5
- from apps.inthub_api.common import APIError, new_id, now_utc, require_repo
6
- from apps.inthub_api.db import connect
7
-
8
-
9
- def link_project(db_path, project_name, repo, workspace_id):
10
- require_repo(repo)
11
- if repo.get("provider") != "github":
12
- raise APIError(
13
- "PROVIDER_UNSUPPORTED",
14
- f"Unsupported provider '{repo.get('provider')}'.",
15
- status=400,
16
- )
17
-
18
- with connect(db_path) as conn:
19
- project = conn.execute(
20
- "SELECT * FROM projects WHERE provider = ? AND repo_id = ?",
21
- (repo["provider"], repo["repo_id"]),
22
- ).fetchone()
23
-
24
- if project is None:
25
- project_id = new_id("proj")
26
- conn.execute(
27
- """
28
- INSERT INTO projects (id, name, provider, repo_id, owner, repo_name, created_at)
29
- VALUES (?, ?, ?, ?, ?, ?, ?)
30
- """,
31
- (
32
- project_id,
33
- project_name or repo["name"],
34
- repo["provider"],
35
- repo["repo_id"],
36
- repo["owner"],
37
- repo["name"],
38
- now_utc(),
39
- ),
40
- )
41
- else:
42
- project_id = project["id"]
43
-
44
- if not workspace_id:
45
- workspace_id = new_id("wks")
46
-
47
- workspace = conn.execute(
48
- "SELECT * FROM workspaces WHERE id = ?",
49
- (workspace_id,),
50
- ).fetchone()
51
- if workspace is None:
52
- conn.execute(
53
- """
54
- INSERT INTO workspaces (id, project_id, provider, repo_id, created_at)
55
- VALUES (?, ?, ?, ?, ?)
56
- """,
57
- (workspace_id, project_id, repo["provider"], repo["repo_id"], now_utc()),
58
- )
59
- else:
60
- if workspace["project_id"] != project_id or workspace["repo_id"] != repo["repo_id"]:
61
- raise APIError(
62
- "STATE_CONFLICT",
63
- f"Workspace {workspace_id} is already linked to another project or repo.",
64
- status=409,
65
- )
66
-
67
- conn.commit()
68
- return {
69
- "project_id": project_id,
70
- "workspace_id": workspace_id,
71
- "repo_binding": {
72
- "provider": repo["provider"],
73
- "repo_id": repo["repo_id"],
74
- "owner": repo["owner"],
75
- "name": repo["name"],
76
- },
77
- }
78
-
79
-
80
- def store_sync_batch(db_path, payload):
81
- required = ("sync_batch_id", "project_id", "repo", "workspace", "snapshot")
82
- missing = [key for key in required if key not in payload]
83
- if missing:
84
- raise APIError(
85
- "INVALID_INPUT",
86
- f"Missing sync batch fields: {', '.join(missing)}.",
87
- status=400,
88
- )
89
-
90
- repo = payload["repo"]
91
- workspace = payload["workspace"]
92
- require_repo(repo)
93
-
94
- workspace_id = workspace.get("workspace_id")
95
- if not workspace_id:
96
- raise APIError("INVALID_INPUT", "Missing workspace.workspace_id.", status=400)
97
-
98
- with connect(db_path) as conn:
99
- existing = conn.execute(
100
- "SELECT accepted_at FROM sync_batches WHERE id = ?",
101
- (payload["sync_batch_id"],),
102
- ).fetchone()
103
- if existing is not None:
104
- return {
105
- "sync_batch_id": payload["sync_batch_id"],
106
- "project_id": payload["project_id"],
107
- "workspace_id": workspace_id,
108
- "accepted_at": existing["accepted_at"],
109
- "duplicate": True,
110
- }
111
-
112
- project = conn.execute(
113
- "SELECT * FROM projects WHERE id = ?",
114
- (payload["project_id"],),
115
- ).fetchone()
116
- if project is None:
117
- raise APIError(
118
- "OBJECT_NOT_FOUND",
119
- f"Project {payload['project_id']} not found.",
120
- status=404,
121
- )
122
- if project["provider"] != repo["provider"] or project["repo_id"] != repo["repo_id"]:
123
- raise APIError(
124
- "STATE_CONFLICT",
125
- "Sync batch repo does not match the linked project repo.",
126
- status=409,
127
- )
128
-
129
- workspace_row = conn.execute(
130
- "SELECT * FROM workspaces WHERE id = ? AND project_id = ?",
131
- (workspace_id, payload["project_id"]),
132
- ).fetchone()
133
- if workspace_row is None:
134
- raise APIError(
135
- "OBJECT_NOT_FOUND",
136
- f"Workspace {workspace_id} is not linked to project {payload['project_id']}.",
137
- status=404,
138
- )
139
-
140
- snapshot = payload["snapshot"]
141
- if snapshot.get("schema_version") != "1.0":
142
- raise APIError(
143
- "SCHEMA_VERSION_MISMATCH",
144
- f"Unsupported schema_version '{snapshot.get('schema_version')}'.",
145
- status=400,
146
- )
147
-
148
- accepted_at = now_utc()
149
- conn.execute(
150
- """
151
- INSERT INTO sync_batches (id, project_id, workspace_id, generated_at, accepted_at, payload_json)
152
- VALUES (?, ?, ?, ?, ?, ?)
153
- """,
154
- (
155
- payload["sync_batch_id"],
156
- payload["project_id"],
157
- workspace_id,
158
- payload.get("generated_at", accepted_at),
159
- accepted_at,
160
- json.dumps(payload, ensure_ascii=False),
161
- ),
162
- )
163
- conn.commit()
164
- return {
165
- "sync_batch_id": payload["sync_batch_id"],
166
- "project_id": payload["project_id"],
167
- "workspace_id": workspace_id,
168
- "accepted_at": accepted_at,
169
- "duplicate": False,
170
- }