argus-survey 0.1.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 (92) hide show
  1. argus_survey-0.1.0/.copier-answers.yml +21 -0
  2. argus_survey-0.1.0/.cursor/rules/agent-coding-packs.mdc +29 -0
  3. argus_survey-0.1.0/.dockerignore +19 -0
  4. argus_survey-0.1.0/.env.example +36 -0
  5. argus_survey-0.1.0/.github/workflows/ci.yml +87 -0
  6. argus_survey-0.1.0/.github/workflows/release.yml +114 -0
  7. argus_survey-0.1.0/.github/workflows/suite.yml +11 -0
  8. argus_survey-0.1.0/.gitignore +18 -0
  9. argus_survey-0.1.0/AGENTS.md +24 -0
  10. argus_survey-0.1.0/CHANGELOG.md +46 -0
  11. argus_survey-0.1.0/CLAUDE.md +101 -0
  12. argus_survey-0.1.0/Dockerfile +87 -0
  13. argus_survey-0.1.0/LICENSE +21 -0
  14. argus_survey-0.1.0/Makefile +62 -0
  15. argus_survey-0.1.0/PKG-INFO +253 -0
  16. argus_survey-0.1.0/README.md +217 -0
  17. argus_survey-0.1.0/SPEC.md +194 -0
  18. argus_survey-0.1.0/TODO.md +7 -0
  19. argus_survey-0.1.0/compose.yaml +76 -0
  20. argus_survey-0.1.0/data/in/.gitkeep +0 -0
  21. argus_survey-0.1.0/docs/atlas-showcase.md +30 -0
  22. argus_survey-0.1.0/docs/data-sources.md +86 -0
  23. argus_survey-0.1.0/docs/img/viewer.png +0 -0
  24. argus_survey-0.1.0/docs/runbook.md +100 -0
  25. argus_survey-0.1.0/env/geo-linux-64.lock +201 -0
  26. argus_survey-0.1.0/env/geo.yml +15 -0
  27. argus_survey-0.1.0/env/lock.sh +13 -0
  28. argus_survey-0.1.0/env/proj-grids.sha256 +5 -0
  29. argus_survey-0.1.0/examples/viewer.html +1 -0
  30. argus_survey-0.1.0/pyproject.toml +66 -0
  31. argus_survey-0.1.0/schema/survey-wire.schema.json +3567 -0
  32. argus_survey-0.1.0/scripts/fetch-usgs-sample.sh +17 -0
  33. argus_survey-0.1.0/src/argus_survey/__init__.py +16 -0
  34. argus_survey-0.1.0/src/argus_survey/_version.py +24 -0
  35. argus_survey-0.1.0/src/argus_survey/accuracy.py +353 -0
  36. argus_survey-0.1.0/src/argus_survey/change.py +198 -0
  37. argus_survey-0.1.0/src/argus_survey/cli.py +799 -0
  38. argus_survey-0.1.0/src/argus_survey/config.py +76 -0
  39. argus_survey-0.1.0/src/argus_survey/contours.py +88 -0
  40. argus_survey-0.1.0/src/argus_survey/crs.py +357 -0
  41. argus_survey-0.1.0/src/argus_survey/export.py +181 -0
  42. argus_survey-0.1.0/src/argus_survey/grid.py +69 -0
  43. argus_survey-0.1.0/src/argus_survey/ground.py +159 -0
  44. argus_survey-0.1.0/src/argus_survey/ingest.py +537 -0
  45. argus_survey-0.1.0/src/argus_survey/jobs.py +261 -0
  46. argus_survey-0.1.0/src/argus_survey/models.py +776 -0
  47. argus_survey-0.1.0/src/argus_survey/overlays.py +127 -0
  48. argus_survey-0.1.0/src/argus_survey/pipeline.py +61 -0
  49. argus_survey-0.1.0/src/argus_survey/profile.py +149 -0
  50. argus_survey-0.1.0/src/argus_survey/py.typed +1 -0
  51. argus_survey-0.1.0/src/argus_survey/qa.py +373 -0
  52. argus_survey-0.1.0/src/argus_survey/rasters.py +300 -0
  53. argus_survey-0.1.0/src/argus_survey/report.py +698 -0
  54. argus_survey-0.1.0/src/argus_survey/scale.py +175 -0
  55. argus_survey-0.1.0/src/argus_survey/server/__init__.py +5 -0
  56. argus_survey-0.1.0/src/argus_survey/server/app.py +488 -0
  57. argus_survey-0.1.0/src/argus_survey/server/static/viewer.html +408 -0
  58. argus_survey-0.1.0/src/argus_survey/surfaces.py +309 -0
  59. argus_survey-0.1.0/src/argus_survey/synthetic.py +389 -0
  60. argus_survey-0.1.0/src/argus_survey/tiling.py +194 -0
  61. argus_survey-0.1.0/src/argus_survey/tools.py +389 -0
  62. argus_survey-0.1.0/src/argus_survey/vectors.py +169 -0
  63. argus_survey-0.1.0/src/argus_survey/volume.py +525 -0
  64. argus_survey-0.1.0/tests/conftest.py +139 -0
  65. argus_survey-0.1.0/tests/data/proj_verbose_utm50.txt +33 -0
  66. argus_survey-0.1.0/tests/data/projinfo_gda94_gda2020.txt +39 -0
  67. argus_survey-0.1.0/tests/data/projjson_gda2020_3d.json +49 -0
  68. argus_survey-0.1.0/tests/data/projjson_mga50_ahd.json +146 -0
  69. argus_survey-0.1.0/tests/data/projjson_oregon_ft.json +151 -0
  70. argus_survey-0.1.0/tests/data/projjson_pcg2020.json +125 -0
  71. argus_survey-0.1.0/tests/test_config.py +43 -0
  72. argus_survey-0.1.0/tests/test_crs_operations.py +64 -0
  73. argus_survey-0.1.0/tests/test_crs_records.py +128 -0
  74. argus_survey-0.1.0/tests/test_export.py +80 -0
  75. argus_survey-0.1.0/tests/test_ingest.py +227 -0
  76. argus_survey-0.1.0/tests/test_ingest_units.py +76 -0
  77. argus_survey-0.1.0/tests/test_jobs.py +89 -0
  78. argus_survey-0.1.0/tests/test_m4_pointcloud.py +212 -0
  79. argus_survey-0.1.0/tests/test_m4_units.py +147 -0
  80. argus_survey-0.1.0/tests/test_qa_ground.py +129 -0
  81. argus_survey-0.1.0/tests/test_qa_units.py +22 -0
  82. argus_survey-0.1.0/tests/test_report.py +73 -0
  83. argus_survey-0.1.0/tests/test_scale_units.py +61 -0
  84. argus_survey-0.1.0/tests/test_server.py +168 -0
  85. argus_survey-0.1.0/tests/test_surfaces_contours.py +138 -0
  86. argus_survey-0.1.0/tests/test_tiling_units.py +45 -0
  87. argus_survey-0.1.0/tests/test_tools.py +104 -0
  88. argus_survey-0.1.0/tests/test_version.py +8 -0
  89. argus_survey-0.1.0/tests/test_volumes_analytic.py +156 -0
  90. argus_survey-0.1.0/tests/test_wire.py +44 -0
  91. argus_survey-0.1.0/tests/test_workers_units.py +17 -0
  92. argus_survey-0.1.0/uv.lock +565 -0
@@ -0,0 +1,21 @@
1
+ # Managed by Copier — do not edit by hand.
2
+ # Run `copier update` in this repo to pull template changes.
3
+ _commit: '9252058'
4
+ _src_path: ../argus-pkg-template
5
+ author: smk762
6
+ cli_name: argus-survey
7
+ description: 'Point-cloud toolkit for land surveyors: QA, ground, DTM/DSM, contours,
8
+ volumes, change and accuracy with recorded datums'
9
+ dockerfile: Dockerfile
10
+ github_owner: smk762
11
+ has_cli: true
12
+ has_server: true
13
+ image_name: argus-survey
14
+ package_name: argus_survey
15
+ post_test: ''
16
+ project_name: argus-survey
17
+ python_min: '3.11'
18
+ ruff_pin: 0.15.16
19
+ test_extras: dev
20
+ version_build_arg: false
21
+
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: Fetch ai-code-auditor coding packs for argus-survey before changing code
3
+ alwaysApply: true
4
+ ---
5
+
6
+ <!-- ai-audit:agent-packs:start -->
7
+ # Agent coding packs
8
+
9
+ Do not wait for a pasted brief — fetch the audit packs for `argus-survey` yourself.
10
+
11
+ The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in the local `.env`
12
+ (see `.env.example`, placeholder `http://<AUDIT_HOST>:8765`); read it there, don't guess.
13
+
14
+ 1. **Bundle (one call):** `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/bundle?repo=argus-survey`
15
+ (world model + ranked specialist packs + token-capped `briefs_markdown`;
16
+ optional `&token_budget=2500&q=<keyword>&limit_specialists=5`).
17
+ CLI equivalent: `ai-audit packs --bundle --repo argus-survey`.
18
+ 2. **Claim** the specialist pack you pick:
19
+ `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
20
+ (send `X-API-Token: $AI_AUDIT_API_TOKEN`).
21
+ 3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
22
+ HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
23
+ or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
24
+ 4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-survey","patch":"<diff>","pack_id":"<id>"}`
25
+ — a passing run marks the pack `resolved`.
26
+
27
+ World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
28
+ Full flow: ai-code-auditor `AGENTS.md`.
29
+ <!-- ai-audit:agent-packs:end -->
@@ -0,0 +1,19 @@
1
+ # NOTE: .git is deliberately *not* ignored -- hatch-vcs reads the version from
2
+ # git history at build time.
3
+ .github
4
+ .venv
5
+ venv
6
+ dist
7
+ build
8
+ *.egg-info
9
+ **/__pycache__
10
+ **/*.py[cod]
11
+ .pytest_cache
12
+ .ruff_cache
13
+ .coverage
14
+ htmlcov
15
+
16
+ # Local data and job output: mounted at run time, never baked into the image
17
+ .env
18
+ data
19
+ jobs
@@ -0,0 +1,36 @@
1
+ # Copy to .env. compose.yaml reads it (`cp .env.example .env`); outside compose, export what you need
2
+ # (not by sourcing the whole file: UID is read-only in bash, so `. .env` fails on it).
3
+
4
+ # ── Compose host ─────────────────────────────────────────────────────
5
+ # Your uid/gid (`id -u`, `id -g`), so job files on the mounts are yours, not root's.
6
+ UID=1000
7
+ GID=1000
8
+ # Host directories compose mounts at /data/in (read-only: point clouds, polygons,
9
+ # check points, design surfaces) and /data/jobs (job output). Defaults: ./data/{in,jobs}.
10
+ #SURVEY_IN_HOST=/srv/survey/incoming
11
+ #SURVEY_JOBS_HOST=/srv/survey/jobs
12
+ # Host address the API binds to (127.0.0.1 keeps it off the network).
13
+ #SURVEY_BIND=127.0.0.1
14
+ # Tag compose gives the image it builds from this checkout.
15
+ #SURVEY_IMAGE=argus-survey:local
16
+
17
+ # ── Server ───────────────────────────────────────────────────────────
18
+ # Host port (suite: lens 8100, curator 8101, quarry 8102, forge 8103, proof 8104,
19
+ # atlas 8105, safelight/darkroom 8106).
20
+ SURVEY_PORT=8107
21
+ # Browser origins allowed by `argus-survey serve --cors` (comma-separated).
22
+ #SURVEY_CORS_ORIGINS=http://localhost:3000
23
+
24
+ # ── Safety posture ───────────────────────────────────────────────────
25
+ # The API is read-only unless this is 1/true/yes/on: POST /jobs/{id}/{step}
26
+ # returns 403 and GET /health reports read_only=true. The CLI needs no flag.
27
+ #SURVEY_ALLOW_PROCESS=1
28
+
29
+ # ── Storage (container paths; the image sets these, compose mounts onto them) ──
30
+ SURVEY_DATA_DIR=/data/in
31
+ SURVEY_JOBS_DIR=/data/jobs
32
+
33
+ # ── Agent tooling (not read by this service) ──
34
+ # ai-code-auditor audit API base that AGENTS.md tells coding agents to use.
35
+ # Put the real host/IP in your local .env, never here.
36
+ # AI_AUDIT_PUBLIC_BASE_URL=http://<AUDIT_HOST>:8765
@@ -0,0 +1,87 @@
1
+ name: CI
2
+
3
+ # Standalone rather than the shared smk762/argus-ci python-ci.yml: the suite
4
+ # shells out to PDAL/GDAL/PROJ, which no distro packages consistently any more
5
+ # (PDAL is absent from Ubuntu 24.04/26.04 and Debian). So the tool-backed tests
6
+ # run *inside the image* — the same conda-forge lockfile and datum grids users
7
+ # get — with SURVEY_TEST_REQUIRE_TOOLS=1 turning every tool skip into a failure.
8
+ # The pure-Python suite also runs on the oldest and newest supported Pythons.
9
+
10
+ on:
11
+ push:
12
+ branches: [main, dev]
13
+ pull_request:
14
+ branches: [main, dev]
15
+
16
+ concurrency:
17
+ group: ci-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ lint:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: astral-sh/setup-uv@v6
26
+ with:
27
+ enable-cache: true
28
+ - name: ruff check
29
+ run: uvx ruff@0.15.16 check src/ tests/
30
+ - name: ruff format --check
31
+ run: uvx ruff@0.15.16 format --check src/ tests/
32
+
33
+ unit:
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ matrix:
37
+ python-version: ["3.11", "3.14"]
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ with:
41
+ fetch-depth: 0 # hatch-vcs derives the dev version from git history
42
+ - uses: astral-sh/setup-uv@v6
43
+ with:
44
+ enable-cache: true
45
+ - run: uv sync --locked --all-extras --python ${{ matrix.python-version }}
46
+ - run: uv run --no-sync pytest --tb=short -q
47
+
48
+ image-test:
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ with:
53
+ fetch-depth: 0
54
+ - uses: docker/setup-buildx-action@v3
55
+ - name: Build the test image (runs `doctor --strict` inside)
56
+ uses: docker/build-push-action@v6
57
+ with:
58
+ context: .
59
+ build-args: EXTRAS=server,cli,dev
60
+ tags: argus-survey:test
61
+ load: true
62
+ cache-from: type=gha
63
+ cache-to: type=gha,mode=max
64
+ # Same container setup as compose's `test` service, but run directly so the image
65
+ # just built from the GHA layer cache is used (compose's pull_policy: build would rebuild).
66
+ - name: Full suite against real PDAL/GDAL/PROJ
67
+ run: |
68
+ docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/src" -w /src \
69
+ -e HOME=/tmp -e PYTHONPATH=/src/src -e PYTHONDONTWRITEBYTECODE=1 -e SURVEY_TEST_REQUIRE_TOOLS=1 \
70
+ argus-survey:test python -m pytest -q -p no:cacheprovider
71
+
72
+ build:
73
+ runs-on: ubuntu-latest
74
+ needs: [lint, unit, image-test]
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ with:
78
+ fetch-depth: 0
79
+ - uses: astral-sh/setup-uv@v6
80
+ with:
81
+ enable-cache: true
82
+ - run: uv build
83
+ - uses: actions/upload-artifact@v4
84
+ with:
85
+ name: dist
86
+ path: dist/
87
+ retention-days: 14
@@ -0,0 +1,114 @@
1
+ name: Release to PyPI
2
+
3
+ # Top-level (non-reusable) on purpose: PyPI trusted publishing cannot run inside
4
+ # a reusable workflow. Keep this file in sync across the suite via `copier update`.
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+ - uses: astral-sh/setup-uv@v6
22
+ with:
23
+ enable-cache: true
24
+ - run: uv build
25
+ - uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ test-install:
31
+ runs-on: ubuntu-latest
32
+ needs: build
33
+ strategy:
34
+ matrix:
35
+ python-version: ["3.11", "3.12"]
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: astral-sh/setup-uv@v6
42
+ with:
43
+ enable-cache: true
44
+ - name: Install wheel and smoke-test import
45
+ run: |
46
+ uv venv --python ${{ matrix.python-version }}
47
+ uv pip install dist/*.whl
48
+ uv run --no-sync python -c "import argus_survey; print(f'argus-survey {argus_survey.__version__} OK')"
49
+
50
+ publish-testpypi:
51
+ runs-on: ubuntu-latest
52
+ needs: test-install
53
+ environment: testpypi
54
+ steps:
55
+ - uses: actions/download-artifact@v4
56
+ with:
57
+ name: dist
58
+ path: dist/
59
+ - uses: pypa/gh-action-pypi-publish@release/v1
60
+ with:
61
+ repository-url: https://test.pypi.org/legacy/
62
+
63
+ publish-pypi:
64
+ runs-on: ubuntu-latest
65
+ # Ordered *after* the TestPyPI rehearsal so its outcome is visible first, but
66
+ # deliberately not gated on it. A TestPyPI-only fault — a missing trusted
67
+ # publisher, an index outage — must not silently withhold the real release:
68
+ # that failure mode shipped argus-proof images for two tags with no wheel on
69
+ # PyPI, and nothing surfaced it because publish-image is independent.
70
+ # Only the build + install gate is required; a TestPyPI failure still marks
71
+ # the whole run failed, so the problem stays loud.
72
+ needs: [test-install, publish-testpypi]
73
+ if: "!cancelled() && needs.test-install.result == 'success'"
74
+ environment: pypi
75
+ steps:
76
+ - uses: actions/download-artifact@v4
77
+ with:
78
+ name: dist
79
+ path: dist/
80
+ - uses: pypa/gh-action-pypi-publish@release/v1
81
+
82
+ publish-image:
83
+ runs-on: ubuntu-latest
84
+ needs: test-install
85
+ permissions:
86
+ contents: read
87
+ packages: write
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ with:
91
+ fetch-depth: 0 # hatch-vcs reads the tag from git history
92
+ - uses: docker/setup-buildx-action@v3
93
+ - uses: docker/login-action@v3
94
+ with:
95
+ registry: ghcr.io
96
+ username: ${{ github.actor }}
97
+ password: ${{ secrets.GITHUB_TOKEN }}
98
+ - id: meta
99
+ uses: docker/metadata-action@v5
100
+ with:
101
+ images: ghcr.io/${{ github.repository_owner }}/argus-survey
102
+ tags: |
103
+ type=semver,pattern={{version}}
104
+ type=semver,pattern={{major}}.{{minor}}
105
+ type=raw,value=latest
106
+ - uses: docker/build-push-action@v6
107
+ with:
108
+ context: .
109
+ file: ./Dockerfile
110
+ push: true
111
+ tags: ${{ steps.meta.outputs.tags }}
112
+ labels: ${{ steps.meta.outputs.labels }}
113
+ cache-from: type=gha
114
+ cache-to: type=gha,mode=max
@@ -0,0 +1,11 @@
1
+ name: Suite
2
+ on:
3
+ push:
4
+ branches: [dev, main]
5
+ tags: ["v*"]
6
+ pull_request:
7
+ branches: [dev, main]
8
+
9
+ jobs:
10
+ suite:
11
+ uses: smk762/argus-ci/.github/workflows/suite-check.yml@v1
@@ -0,0 +1,18 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ # hatch-vcs writes this at build time (version derived from git tags)
10
+ src/argus_survey/_version.py
11
+
12
+ # Local job output
13
+ jobs/
14
+
15
+ # Docker Compose: host settings and the default mount dirs (kept by their .gitkeep)
16
+ .env
17
+ data/*/*
18
+ !data/*/.gitkeep
@@ -0,0 +1,24 @@
1
+ <!-- ai-audit:agent-packs:start -->
2
+ ## Agent coding packs (ai-code-auditor)
3
+
4
+ Do not wait for a pasted brief — fetch the audit packs for `argus-survey` yourself.
5
+
6
+ The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in the local `.env`
7
+ (see `.env.example`, placeholder `http://<AUDIT_HOST>:8765`); read it there, don't guess.
8
+
9
+ 1. **Bundle (one call):** `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/bundle?repo=argus-survey`
10
+ (world model + ranked specialist packs + token-capped `briefs_markdown`;
11
+ optional `&token_budget=2500&q=<keyword>&limit_specialists=5`).
12
+ CLI equivalent: `ai-audit packs --bundle --repo argus-survey`.
13
+ 2. **Claim** the specialist pack you pick:
14
+ `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
15
+ (send `X-API-Token: $AI_AUDIT_API_TOKEN`).
16
+ 3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
17
+ HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
18
+ or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
19
+ 4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-survey","patch":"<diff>","pack_id":"<id>"}`
20
+ — a passing run marks the pack `resolved`.
21
+
22
+ World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
23
+ Full flow: ai-code-auditor `AGENTS.md`.
24
+ <!-- ai-audit:agent-packs:end -->
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/) (tags are the source of truth via hatch-vcs).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-13
10
+
11
+ First release, published to PyPI and GHCR.
12
+
13
+ ### Changed
14
+ - Accuracy follows the text of ASPRS Positional Accuracy Standards, Edition 2, Version 2 (2024): §7.16.1 statement wording (with the reduced-checkpoint statement below 30 points), min/max/median (§7.16), bias flagged above 25 % of the target RMSE and blunders beyond 3 × it (§7.2), a horizontal-accuracy note for photogrammetry (§7.6) (#3).
15
+
16
+ ### Added
17
+ - `export --points las|e57 [--polygon P] [--thin S]` (and `POST /jobs/{id}/export`): the classified cloud as uncompressed LAS 1.4 (CRS in the header) or double-precision E57 for ReCap / Civil 3D, with a notes file stating the CRS, vertical datum, grid-to-ground factor and ASPRS class counts; streaming, count-reconciled; recorded in `job.json` `exports` (#8).
18
+ - Grid-to-ground: ingest records `scale_factors` (point scale k and convergence from `proj -V`, k's range over the extent, height factor R/(R+h) from the ellipsoidal height via the explicit geoid operation, combined factor); volume and cut/fill reports add ground-scaled areas and volumes (#12).
19
+ - E57 ingest (terrestrial scanners) via `readers.e57`; `--s-srs` required (E57 has no CRS); new source type `terrestrial` (#20).
20
+ - `ground --preset stockpile` (SMRF slope 1.0) keeps piles at the angle of repose as ground (#18).
21
+ - `surfaces --chm-method radius`: highest point within res·√2, for sparse canopy at fine resolution (#19).
22
+ - Wire contract for UIs: `argus-survey schema [--check]` writes `schema/survey-wire.schema.json` (job.json, digests, health, every request body and SSE event) with `argus_cortex.wire`; the API's request bodies moved to `models.py`, and `/health` and the SSE payloads are built from the models. [argus-sightline](https://github.com/smk762/argus-sightline) generates its types from it.
23
+ - `volume` and `cutfill` over the API take a polygon drawn on the map (`polygon_geojson`, a WGS 84 GeoJSON geometry, plus an optional zone `name`) instead of a file, the way `profile` takes a drawn line.
24
+ - `argus-survey synth DIR --kit` (`make demo-kit`): every demo input with known answers: two epochs, check points, stockpile and pad polygons and a design surface, plus `kit-truth.json`.
25
+ - Viewer (`/viewer`): MapLibre map of surfaces, density, change, contours and coverage; the classified cloud streamed from the COPC (copc.js + laz-perf + three.js) coloured by elevation/class/intensity; job CRS and warnings; profile drawing when processing is allowed.
26
+ - Docs: README walkthrough on public USGS 3DEP data (`make sample`), WA datum table, runbook, data sources, argus-atlas showcase. `make demo`/`up`/`down`.
27
+ - Ingest refuses geographic and Web Mercator working CRSs (Mercator metres are not ground metres).
28
+ - API on :8107 (`argus-survey serve`, compose `serve`): health, doctor, inputs, jobs, products by kind with HTTP Range (COPC streams), web map views (Web Mercator PNG + corners, WGS 84 GeoJSON), and `POST /jobs/{id}/{step}` for every step streaming Server-Sent Events. Read-only unless `SURVEY_ALLOW_PROCESS`; inputs are names under `SURVEY_DATA_DIR`.
29
+ - Docs: README API route table, step names and body models (pointing at the wire schema); how check points are split into NVA/VVA; the 0.10 m default LoD; `synth DIR --kit` syntax; which commands take `--job`/`--out`. SPEC.md marked as the historical v0 brief, with where it diverges.
30
+
31
+ ## [0.1.0rc1] - 2026-10-31
32
+
33
+ First release candidate: a usable CLI (milestones 1–5 of the build plan).
34
+
35
+ ### Added
36
+ - `report`: one self-contained, printable `report.html` per job (no JavaScript, embedded thumbnails), with a red banner when the vertical datum was assumed, every product's SHA256, figures linked to the commands that produced them, and the full command log. `run` now ends with it.
37
+ - Scaffold from argus-pkg-template; Docker image on Ubuntu 26.04 with PDAL 2.10 / GDAL 3.13 / PROJ 9.9 from a pinned conda-forge lockfile, and datum grids (AUSGeoid2020, AUSGeoid09, GDA94→GDA2020 conformal+distortion, AGQG, GEOID18) verified by SHA256.
38
+ - `argus-survey doctor`: tool versions, PDAL/GDAL drivers and algorithms, datum grids, and the surveyor transformations (GDA2020/GDA94 → MGA + AHD, NAD83(2011) → NAVD88) resolved without ballpark fallbacks.
39
+ - One subprocess runner that records every command, and explicit coordinate-operation selection that refuses rather than falling back to a ballpark transform.
40
+ - `info` and `ingest`: CRS records (compound, ellipsoidal, user-supplied, assumed), `--t-srs` through an explicit PROJ operation with axis-order compensation, crop polygons with CRS sanity checks, streaming ingest + untwine COPC for drone-scale clouds, point-count reconciliation, LiDAR/photogrammetry detection.
41
+ - `qa`: density (mean/p5/p50/p95), class and return histograms (ASPRS names), outlier count, coverage boundary, density gaps.
42
+ - `ground`: SMRF (default) or CSF after statistical outlier flagging, per buffered tile with an exact partition check; every point kept.
43
+ - `surfaces`: DTM/DSM/CHM on a snapped grid, void masks and void % of the covered area, optional void fill, hillshade, resolution warning from ground density.
44
+ - `contours`: GPKG + GeoJSON with index contours. `run`: the whole chain.
45
+ - `volume` (stockpiles: plane / lowest-perimeter / surface base), `cutfill` (design vs surface), `change` (DoD with LoD, optional polygon), `accuracy` (ASPRS ed. 2: NVA/VVA RMSE, check-point accuracy, ≥30 rule, transformed check points), `profile` (CSV + SVG). One volume engine with fractional boundary cells; analytic tests (cone on a tilted plane at three resolutions, wedge, cut/fill signs, voids).
46
+ - `synth`: synthetic survey scenes with analytically known answers (used by the tests and for demos).
@@ -0,0 +1,101 @@
1
+ # CLAUDE.md — argus-survey
2
+
3
+ Guidance for AI agents working in this repo. Human-facing usage lives in [README.md](README.md); this file is the orientation an agent needs to change code safely.
4
+
5
+ ## What this is
6
+
7
+ A point-cloud toolkit for **land surveyors** (first audience: Western Australian civil surveyors — MGA2020 zones 49–51, PCG2020 and the other WA LSG2020 grids, AHD via AUSGeoid2020). Input is LiDAR, drone photogrammetry or terrestrial-scanner points (LAS/LAZ/COPC, E57); output is what a surveyor hands over: QA report, ground classification, DTM/DSM/CHM, contours, stockpile volumes, cut/fill, profiles, change between epochs and an accuracy statement against check points (ASPRS ed. 2, NVA + VVA). Every run is traceable: every datum, operation, pipeline and argv is recorded in `job.json`.
8
+
9
+ It is judged on **correct datums, honest accuracy numbers and reproducible outputs**, not visuals. Prefer one deliverable done properly over three done loosely. It is a standalone peer in the Argus suite (port **8107**); its rasters are plain GeoTIFFs that `argus-atlas controls --dem` can consume. No code dependency on atlas.
10
+
11
+ ## Argus suite
12
+ <!-- argus-suite: keep this block; argus-ci checks for it -->
13
+ This repo is part of the Argus suite. The suite map below lists every sibling and what it owns; check it before
14
+ building something a sibling may already do. **On every release, update this repo's row and capabilities in
15
+ argus-cortex's SUITE.md** (a PR to argus-cortex in the same release).
16
+
17
+ @../argus-core/SUITE.md
18
+
19
+ Not checked out? Read https://github.com/smk762/argus-cortex/blob/main/SUITE.md
20
+
21
+ ## Toolchain (read before touching Docker/CI)
22
+
23
+ - **PDAL, GDAL and PROJ come from conda-forge**, pinned by `env/geo-linux-64.lock` (explicit URLs + md5), generated from `env/geo.yml` by `make geo-lock`. PDAL is not packaged by Debian or Ubuntu (24.04 and 26.04 both lack it), so "apt install pdal" is not an option; one conda-forge stack also guarantees PDAL links the same GDAL/PROJ whose CLIs we call. The image is `ubuntu:26.04` + that env at `/opt/geo` + Ubuntu's Python 3.14 in `/opt/venv`.
24
+ - **Never pip-install PDAL/GDAL/rasterio/laspy/pyproj.** The package only shells out to `pdal`, `gdal*`, `ogr2ogr`, `projinfo`. The unified `gdal` CLI (≥ 3.11) supplies `raster calc`, `raster zonal-stats`, `raster pixel-info -r bilinear` and `raster fill-nodata`, so no Python GDAL bindings are needed (the spec's `gdal_calc.py`/`gdallocationinfo` are replaced by these on purpose).
25
+ - **Datum grids** are fetched at image build from cdn.proj.org and verified against `env/proj-grids.sha256` (AUSGeoid2020, AUSGeoid09, GDA94→GDA2020 conformal+distortion, AGQG/AVWS, GEOID18). At run time `PROJ_NETWORK=OFF`, and `tools.run` forces it off for every child.
26
+ - `argus-survey doctor --strict` runs as the last image build step, so an image without a working toolchain cannot be built.
27
+
28
+ ## Layout
29
+
30
+ `src/argus_survey/`:
31
+
32
+ - `models.py` — Pydantic v2 wire types. **`JobSidecar` (`job.json`) is the API contract.** `SURVEY_VERSION` is its schema version. The API request bodies, `HealthInfo` and the SSE event payloads live here too; `WIRE_MODELS` → `schema/survey-wire.schema.json` (`argus-survey schema`; `tests/test_wire.py` fails when it is stale). [argus-sightline](../argus-sightline) generates its TypeScript types from that file, so a wire change means: regenerate, commit, then `npm run wire:local ../argus-survey` (or `wire:pull` after a tag) there.
33
+ - `config.py` — `SURVEY_*` env knobs, `process_allowed()`, `resolve_under_root()` (the API's path fence, ported from atlas).
34
+ - `tools.py` — **the one subprocess runner** (`run`, `run_json`, `pdal_pipeline`), `versions()`, PROJ search paths/grid lookup, `doctor()`.
35
+ - `crs.py` — `select_operation()`: explicit coordinate-operation choice (see gotchas); `record_from_projjson()` (compound/3D/BoundCRS/foot units → `CrsRecord`), `pdal_coord_op()` (axis-order compensation), `promote_3d()`, `require_metric()`, `lonlat_bbox()`.
36
+ - `jobs.py` — `JobStore`, strict id charset, and `Job`: `sink` (runner → sidecar, persisted per command), `add_product` (SHA256), `step()` (timings), `require_crs(height=)` (rule 1's refusal).
37
+ - `ingest.py` — `info_report`, `ingest`: pin inputs, resolve source CRS (`--s-srs`, `--assume-vertical EPSG:x|ellipsoidal`), resolve target (`--t-srs`; a 2D target keeps the source heights explicitly), select the operation, then per input a **streaming** PDAL pipeline (projpipeline → crop → stats → LAZ) and **untwine** → `points.copc.laz`; counts reconciled, LiDAR/photogrammetry detected from returns/RGB.
38
+ - `synthetic.py` — seeded analytic scenes (tilted plane, cone stockpile πr²h/3, building, trees; truth in `UserData`) and check points; `argus-survey synth`. The test fixtures *and* the demo data. `write_kit` (`synth DIR --kit`) writes every input the UI's guided demo needs plus `kit-truth.json`. Stockpiles and change are measured on the **DSM**: ground filtering (correctly) removes a pile from the DTM, which then reads ~10 % low on the kit's cone.
39
+ - `grid.py` — `snap()` (origin = floor(min/res)·res, so epochs share cells), `same_grid`/`aligned`, writers.gdal and `-te/-tr` options.
40
+ - `rasters.py` — `gdalinfo -json` facts, exact `stats()` (with `GDAL_PAM_ENABLED=NO`), `read_values()` (ENVI raw → `array`, for modest rasters), `calc()` (`gdal raster calc`), `sample()` (`gdal raster pixel-info -r bilinear`, batch), `set_crs()`.
41
+ - `tiling.py` — buffered tiles for non-streamable PDAL filters; cores are half-open so they partition the plane, and parts **must** add up to the source count before untwine merges them.
42
+ - `qa.py` — streaming stats + hexbin boundary (edge sized from density) + count raster; density percentiles, gaps (25% of median by default), outliers counted per tile (3σ).
43
+ - `ground.py` — per tile: reset classes → outliers (class 7) → SMRF/CSF ignoring 7; photogrammetry returns normalised 1-of-1 (recorded); ground density/spacing.
44
+ - `surfaces.py` — DTM (IDW r=res·√2 of class 2), DSM (binned max; first returns for LiDAR), CHM (tiled hag_nn → binned max), void masks (1/0/255) and void % of coverage, optional fill re-masked to coverage, hillshade.
45
+ - `contours.py` — gdal_contour → GPKG with `idx` (index contours) → GeoJSON.
46
+ - `vectors.py` — polygons/lines into the job CRS (refuses a CRS-less file, or GeoJSON whose "WGS 84" extent is plainly projected), WKT rings/bbox, `densify`, `write_zones` (GPKG for zonal-stats).
47
+ - `volume.py` — `difference_volumes` (the engine: one 6-band `gdal raster calc` + `gdal raster zonal-stats --pixels fractional`), `volume` (stockpile: plane / lowest-perimeter / surface base), `cutfill` (design vs surface; fill = design above), `check_raster_crs`, `align_raster` (exact copy if aligned, else refuse unless `--resample`).
48
+ - `change.py` — DoD later − earlier on the overlap (aligned grids, same CRS), LoD given or 1.96·√(σ₁²+σ₂²), optional polygon; both sidecars pinned by SHA256.
49
+ - `accuracy.py` — ASPRS ed. 2: RMSE only (no 95 %), ≥30 checks, product RMSE = √(data² + checkpoints²), NVA pass/fail vs `--class-cm`, VVA reported as found; check points transformed with `cct` through the selected operation.
50
+ - `profile.py` — densified line → bilinear samples → CSV + pure-Python SVG (voids break the line).
51
+ - `pipeline.py` — `run_all` (`argus-survey run`).
52
+ - `report.py` — self-contained printable `report.html` (no JS; PNG thumbnails as data URIs; figures link to `#cmd-N`).
53
+ - `overlays.py` — web views for the viewer (EPSG:3857 PNG + lon/lat corners; WGS 84 GeoJSON), cached in `<job>/.web/`, **not** recorded in the sidecar (a GET must not change `job.json`).
54
+ - `server/app.py` — FastAPI :8107. `WriteGuard` refuses every POST unless `SURVEY_ALLOW_PROCESS`; steps run in a thread and stream SSE (`start`, `step`, `command`, `warning`, `done`|`error`) via `Job.listeners`; one step per job at a time (409). `server/static/viewer.html` is the viewer (`examples/viewer.html` links to it).
55
+ - `cli.py` — Typer CLI.
56
+
57
+ `tests/` — `conftest.py` marks tool-backed tests `tools`; they skip without the tools unless `SURVEY_TEST_REQUIRE_TOOLS=1`. `tests/data/` holds captured *text* tool output for pure-Python parser tests (no binary fixtures, ever).
58
+
59
+ ## Commands
60
+
61
+ ```bash
62
+ make install # uv sync --locked --all-extras; `make lock` after changing dependencies
63
+ make lint # ruff 0.15.16 check + format --check
64
+ make test # host pytest: pure-Python tests run, tool tests SKIP (no PDAL on the host)
65
+ make test-docker # the full suite in the image against this checkout, tools required
66
+ make image # build the runtime image (doctor --strict inside)
67
+ make geo-lock # re-resolve env/geo-linux-64.lock after editing env/geo.yml
68
+ ```
69
+
70
+ The dev host has no PDAL/GDAL; `make test-docker` is the real gate. Don't `docker run` the image as root against the checkout (root-owned `__pycache__`); compose runs as `${UID}:${GID}`.
71
+
72
+ ## Conventions (non-negotiable)
73
+
74
+ 1. **No silent datums.** Every job records horizontal CRS, vertical CRS (or `null` + an explicit `vertical_assumed` reason), geoid model, epoch where relevant, and units. Without a vertical CRS, height-dependent steps (volumes, contours, accuracy) refuse unless `--assume-vertical EPSG:xxxx` is given, and the assumption is recorded and shown in red in the report.
75
+ 2. **Record, don't assume.** Every PDAL pipeline JSON and every GDAL/PROJ argv goes into the sidecar in run order, with tool versions. Every input is pinned by SHA256 and size.
76
+ 3. **Paths from clients are names under a root.** The API resolves inputs under `SURVEY_DATA_DIR` (`resolve_under_root`); artifacts are addressed by `(job_id, kind)`; job ids match `^[A-Za-z0-9][A-Za-z0-9._-]*$`. No route takes a filesystem path.
77
+ 4. **Numbers come with their uncertainty.** A volume carries the surface resolution, each surface's void %, and the method. An accuracy statement says how many check points, how many fell outside coverage, and which standard/edition.
78
+ 5. **Never overwrite inputs.** Outputs go into the job dir; inputs are read-only (compose mounts `/data/in` read-only).
79
+ 6. **One subprocess runner.** All PDAL/GDAL/PROJ calls go through `tools.run` (logs, times, captures stderr, appends to the sidecar — failed commands too); a non-zero exit raises `ToolError` with the argv and stderr tail.
80
+
81
+ ## Gotchas
82
+
83
+ - **Never let PROJ pick a transformation at run time.** `crs.select_operation` asks `projinfo --hide-ballpark` for candidates, takes the first whose `+grids=` files are all on disk (or the one `--operation` names), and returns its PROJ string; PDAL then runs that exact string through `filters.projpipeline`. `filters.reprojection` with EPSG codes would let PROJ silently fall back to a ballpark operation when a geoid grid is missing (ellipsoidal heights labelled AHD — ~30 m wrong in Perth). `projinfo --grid-check discard_missing` was tested and does **not** reliably drop such candidates; the on-disk check is ours. A `@grid` (optional) marker is treated as required.
84
+ - **GDA94 → GDA2020 has several valid operations.** PROJ ranks the 7-parameter Helmert first; legacy WA cadastral/control-derived data often warrants the ICSM conformal + distortion grid. Record all candidates; let the user pin one (`--operation "GDA94 to GDA2020 (2)"`).
85
+ - **PDAL exits 0 when every point fails to reproject** (it logs "falls outside grid" per point and writes garbage). Ingest passes `fatal_stderr=TRANSFORM_FATAL` to the runner and reconciles counts and bounds afterwards. Keep both.
86
+ - `pdal info --summary` on a `.copc.laz` (readers.copc) has no LAS header block; COPC-ness comes from `reader`.
87
+ - Drone scale: every ingest stage is streamable (`pdal pipeline --stream`); `writers.copc` is **not** used because it buffers the whole cloud — untwine builds the COPC out of core.
88
+ - **writers.gdal's default radius mode smears** each point into neighbouring cells (radius res·√2): fine for IDW, wrong for max/count. DSM, density and CHM use `binmode: true`.
89
+ - `gdal_rasterize -a_srs` drops the vertical part of a compound CRS, and `gdal raster calc` then refuses to mix it with a PDAL raster ("Input spatial reference systems are inconsistent"). Rasters carry the job's full compound CRS; stamp it with `rasters.set_crs` after rasterizing.
90
+ - **`isnodata()` in `gdal raster calc` is always false when the output has no nodata (`--nodata none`)** — voids then count as zero volume, silently. The volume engine writes `--nodata nan` (no band can produce NaN). With an output nodata, nodata *inputs* arrive as NaN in arithmetic, so guard with `isnodata(...) ? … : …` first. And an expression naming no input (`"1"`) is rejected: write `isnodata(U) ? 1 : 1`.
91
+ - Volumes use `--pixels fractional`: the default cell-centre rule got a 5.5 × 4.25 m test polygon's area 28 % wrong at 1 m cells.
92
+ - The DSM defaults to IDW. A binned max reads high on slopes by ~slope × res/2 (≈ +6 % on a 1:2 stockpile at 0.25 m); `--dsm-method max` exists for roofs/canopy.
93
+ - `gdal raster pixel-info` can't always write GeoJSON to `/vsistdout/`; write a temp file.
94
+ - PDAL's automatic hexbin edge can be several times too coarse (coverage area doubled on the test scene); `qa.hexbin_edge` sizes it from density.
95
+ - **Viewer libraries**: MapLibre **5.24.0** UMD, not 6.x — 6 is ESM-only and its separate module worker can't start from a cross-origin CDN (GeoJSON sources silently never render; a blob-import wrapper didn't help). copc 0.0.9 + laz-perf 0.0.7 (web build via jsDelivr `+esm`, `locateFile` for the wasm) + three 0.186.1. Check the viewer in headless Chrome after any bump (`zenika/alpine-chrome` with `--use-angle=swiftshader`, `?points=1` auto-loads the cloud).
96
+ - The `serve` container runs the package installed in the image: `docker compose up -d --build serve` after changing the viewer or server.
97
+ - **Photogrammetry clouds** usually have `ReturnNumber = 0` and no classes: "first returns" selects nothing. Detect and record the source type; use all points for the DSM.
98
+ - The conda lock includes an x86-64-v3 (AVX2) microarch marker; very old CPUs can't run the image.
99
+ - **Branches:** `dev` is the default branch and takes feature PRs; branch from `origin/dev` and never push to `main` directly, which must never be ahead of `dev`. A release fast-forwards `main` to a `dev` commit that passed CI (`git push origin origin/dev:main`), then tags it. If `main` is protected, the release is a reviewed PR from `dev` into `main`; its merge commit stays on `main` alone, one commit ahead of `dev` with an identical tree. That is the one exception to the rule: leave it and don't merge `main` back into `dev`. Anything else on `main` that `dev` lacks is a mistake (`git diff origin/dev origin/main` must be empty), and once `main` carries such a commit, later releases are PRs too. Tags `vX.Y.ZrcN` / `vX.Y.Z`, each with a CHANGELOG heading.
100
+ - Versioning is git-tag-derived (`hatch-vcs`); `_version.py` is generated. `structlog`, Pydantic v2, ruff line-length 120. Scaffolded from `argus-pkg-template`; CI is standalone (`.github/workflows/ci.yml`).
101
+ - Publishing (GitHub repo, PyPI, GHCR) is the owner's (smk762) job — never do it from here. Ask before adding a pip dependency beyond pydantic/structlog/typer/rich/fastapi/uvicorn/argus-cortex, or any network fetch in the server.
@@ -0,0 +1,87 @@
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # argus-survey: Ubuntu 26.04 + one consistent PDAL/GDAL/PROJ stack.
4
+ #
5
+ # PDAL is not packaged by Debian or Ubuntu (24.04 and 26.04 both lack it), so the
6
+ # geospatial toolchain comes from conda-forge, installed from the explicit
7
+ # lockfile env/geo-linux-64.lock (exact URLs + md5) — PDAL links the very
8
+ # GDAL/PROJ whose CLIs the package calls. No Python GIS bindings anywhere: the
9
+ # package only shells out to `pdal`, `gdal*` and `proj*` (CLAUDE.md, rule 6).
10
+ #
11
+ # The datum grids surveyors need are fetched once at build time from the PROJ
12
+ # CDN and checked against env/proj-grids.sha256; at run time PROJ_NETWORK=OFF,
13
+ # so a transform can never silently download (or silently skip) a grid.
14
+
15
+ # ── Stage 1: the geo toolchain ────────────────────────────────────────────────
16
+ FROM ubuntu:26.04 AS geo
17
+
18
+ ARG MICROMAMBA_VERSION=2.3.3
19
+ ENV DEBIAN_FRONTEND=noninteractive MAMBA_ROOT_PREFIX=/opt/mamba
20
+
21
+ RUN apt-get update \
22
+ && apt-get install -y --no-install-recommends ca-certificates bzip2 curl \
23
+ && rm -rf /var/lib/apt/lists/* \
24
+ && curl -fsSL "https://micro.mamba.pm/api/micromamba/linux-64/${MICROMAMBA_VERSION}" \
25
+ | tar -xj -C /usr/local bin/micromamba
26
+
27
+ COPY env/geo-linux-64.lock /tmp/geo.lock
28
+ RUN micromamba create -y -q -p /opt/geo -f /tmp/geo.lock \
29
+ && micromamba clean -y -a -q \
30
+ # Headers, static libs and docs are build-time only; conda-meta stays (doctor reads versions from it).
31
+ && rm -rf /opt/geo/include /opt/geo/share/doc /opt/geo/share/man /opt/geo/man \
32
+ && find /opt/geo/lib -name '*.a' -delete \
33
+ # conda-forge's PDAL ships optional plugins (HDF, icebridge) whose libraries the
34
+ # package doesn't depend on; drop any plugin that can't load so every `pdal` call
35
+ # isn't prefixed by load errors on stderr.
36
+ && for so in /opt/geo/lib/libpdal_plugin_*.so; do \
37
+ if LD_LIBRARY_PATH=/opt/geo/lib ldd "$so" | grep -q 'not found'; then echo "drop $so"; rm "$so"; fi; \
38
+ done
39
+
40
+ COPY env/proj-grids.sha256 /tmp/proj-grids.sha256
41
+ RUN cd /opt/geo/share/proj \
42
+ && cut -d' ' -f3 /tmp/proj-grids.sha256 \
43
+ | xargs -I{} curl -fsSL --retry 3 -o {} "https://cdn.proj.org/{}" \
44
+ && sha256sum -c /tmp/proj-grids.sha256 \
45
+ && cp /tmp/proj-grids.sha256 /opt/geo/share/proj/argus-survey-grids.sha256
46
+
47
+ # ── Stage 2: runtime ──────────────────────────────────────────────────────────
48
+ FROM ubuntu:26.04
49
+
50
+ COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /bin/
51
+
52
+ # hatch-vcs derives the version from git history, so the build context includes
53
+ # .git and the image needs git. Ubuntu's ca-certificates lags: distrust emSign
54
+ # ECC Root CA - G3 and the Comodo AAA cross-sign root, as the rest of the suite does.
55
+ RUN apt-get update \
56
+ && apt-get install -y --no-install-recommends python3 git ca-certificates \
57
+ && sed -i \
58
+ -e 's|^mozilla/emSign_ECC_Root_CA_-_G3|!mozilla/emSign_ECC_Root_CA_-_G3|' \
59
+ -e 's|^mozilla/Comodo_AAA|!mozilla/Comodo_AAA|' \
60
+ /etc/ca-certificates.conf \
61
+ && update-ca-certificates --fresh >/dev/null \
62
+ && rm -rf /var/lib/apt/lists/*
63
+
64
+ COPY --from=geo /opt/geo /opt/geo
65
+
66
+ # The conda env is used without activation, so name its data dirs explicitly.
67
+ ENV PROJ_DATA=/opt/geo/share/proj \
68
+ PROJ_NETWORK=OFF \
69
+ GDAL_DATA=/opt/geo/share/gdal \
70
+ PATH="/opt/venv/bin:${PATH}:/opt/geo/bin"
71
+
72
+ WORKDIR /app
73
+ COPY . .
74
+
75
+ # Extras baked into the image, synced from uv.lock (--locked fails the build if
76
+ # the lock is stale). The package lives in its own venv on Ubuntu's Python.
77
+ ARG EXTRAS="server,cli"
78
+ ENV UV_PROJECT_ENVIRONMENT=/opt/venv
79
+ RUN uv sync --locked --no-editable --no-cache --python /usr/bin/python3 \
80
+ $(echo "${EXTRAS}" | tr ',' '\n' | sed 's/^/--extra /') \
81
+ && argus-survey doctor --strict
82
+
83
+ ENV SURVEY_DATA_DIR=/data/in \
84
+ SURVEY_JOBS_DIR=/data/jobs
85
+
86
+ EXPOSE 8107
87
+ CMD ["argus-survey", "serve", "--host", "0.0.0.0", "--port", "8107"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smk762
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.