Commit graph

33 commits

Author SHA1 Message Date
fe0025ebd6 Ambiguity resolution: prompt when multiple local repos match genesis
select_local_repo() replaces direct find_local_repos() calls in
_remap_origin.  It reads/writes [project_origins] in config.toml:
a cached path is returned immediately; false marks a project read-only;
a single match is auto-cached without prompting; multiple matches call
a prompt_fn (default: interactive stdin) and cache the choice.  Stale
config paths bust the discovery cache so the next call rescans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:03:13 +01:00
4d4a76ea34 Remap checkout origin to local repo when discovered
After creating a working checkout, if find_local_repos() finds a local
clone or fork of the same project, rename 'origin' (bare cache) to
'upstream' and add a new 'origin' pointing at the local repo.  Mirrors
the standard fork workflow: fetch from upstream, push to origin (local
repo), then push to the shared remote and open a PR.  If no local repo
is found, origin stays as the bare cache (read-only).  Idempotent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 18:11:47 +01:00
e2d3d12bc1 Add OpenCDE select-documents picker (POST + UI)
POST /documents/1.0/select-documents accepts a callback URL and returns a
select_documents_url pointing to a self-contained HTML picker that browses
Forgejo repos and IFC files, encodes the selection as a document_id, then
redirects back to the callback with document_ids[] appended.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 12:19:01 +01:00
48169ff94b Add OpenCDE Foundation API 1.1 OAuth2 proxy routes
Implements GET /foundation/1.1/oauth2/auth_url, POST /oauth2/token, and
POST /oauth2/token_refresh, proxying to Forgejo's OAuth2 endpoints using
IFCURL_OAUTH2_CLIENT_ID and IFCURL_OAUTH2_CLIENT_SECRET env vars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:08:13 +01:00
7c25e0624a Rewrite BCF export as pure client-side, remove Python /bcf endpoint
applySelector() now returns the resolved GUIDs array in both the simple
(ThatOpen classifier → getGuid()) and complex (/select → GUIDs already
fetched) paths.  generateBcf() reads currentGuids directly and builds the
full BCF 2.1 zip in the browser, including <Selection>/<Exceptions> in
the viewpoint component list.

The Python POST /bcf endpoint, BcfRequest model, and build_bcf()/
_viewpoint_xml()/_markup_xml() functions are removed — the browser had
all the data needed to do this itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 07:30:33 +01:00
5ea60da943 Fix BCF export missing snapshot.png when selector is active
Capture the WebGL canvas client-side before POSTing to /bcf, send the
base64 PNG as a new `snapshot` field, and embed it as snapshot.png in
the zip alongside viewpoint.bcfv.  Also wires up the <Snapshot> element
in markup.bcf when a snapshot is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 06:56:11 +01:00
caf303d3ed Add checkout.py: writable working-tree checkouts from bare cache
Implements ifcurl-5oo. get_checkout() ensures the bare cache is current via
fetch_ifc(), then clones it locally to <cache_dir>/checkout/ (no network),
and places it at the resolved commit as a detached HEAD — forcing branch
creation before any commit. Viewer artefact patterns (*.ifcview, *.rdbview,
etc.) are appended to .git/info/exclude so they are locally ignored without
touching the repo's own .gitignore. When a newer commit is requested, the
checkout fetches from origin (the local bare cache) using the default refspec
to avoid git's "refusing to fetch into checked-out branch" error. Unblocks
the JSON-RPC connector (ifcurl-242).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 23:38:13 +01:00
988eed5a93 Add discover.py: find local repos matching a project by genesis commit
Implements ifcurl-egi. find_local_repos(genesis) scans directories
from [repo_search] paths in ~/.config/ifcurl/config.toml, computes the
genesis commit of each git repo found, and returns paths whose genesis
matches. Results are cached in ~/.cache/ifcurl/genesis_repos/<genesis>.json
for IFCURL_REPO_CACHE_TTL seconds (default 3600). Recursion stops at repo
boundaries and hidden directories; depth capped at 4 levels. Adds tomli
as a conditional dep for Python <3.11. Unblocks origin remapping (ifcurl-oef)
and ambiguity resolution (ifcurl-77u).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 23:05:33 +01:00
8625528514 Add get_genesis_commit: root commit hash as project identity key
Implements ifcurl-4d4. _compute_genesis() runs git rev-list --max-parents=0
HEAD to find the root commit(s); get_genesis_commit() wraps it with a
per-remote disk cache (~/.cache/ifcurl/<hash>/genesis_commit) so the git
command only runs once per repo. For multi-root repos the lexicographically
smallest hexsha is chosen for a stable result. Local repos compute fresh
on each call. Exported from __init__. Unblocks local repo discovery (ifcurl-egi).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:32:54 +01:00
ae143b74b7 Add fetch_ifc_path: materialise IFC blob to stable per-commit cache path
Implements ifcurl-i07. fetch_ifc_path() fetches the IFC file and writes
it to ~/.cache/ifcurl/<url-hash>/<hexsha>/<basename>, returning the path
alongside the is_stale flag. Content-addressed by commit hexsha, so the
file is never rewritten once present. Works for both local and remote
URLs. Underpins the connector and checkout layers (ifcurl-5oo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:26:11 +01:00
03782ec9f7 Fix fetch-on-miss for PR commits: catch ValueError from GitPython directly
GitPython raises a plain ValueError from _parse_object_header when a commit
hash is missing, not BadObject, so the previous message-based retry guard
("not found in repository") never matched. Broaden the except clause to catch
(ValueError, BadObject, BadName) and drop the fragile string check. Extract
the fetch logic into _fetch_all_refs helper and apply the same retry to
diff_text. Add tests covering _fetch_all_refs, the retry-on-BadObject path,
and the no-retry-for-local-transport invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:56:07 +01:00
Claude
8ab6603a44 url.py, test_url.py: add query parameter and visibility=clash
- IfcUrl gains a query field (str | None) for dot-notation
  attribute/property paths (e.g. Pset_WallCommon.FireRating)
- visibility now accepts 'clash' in addition to the existing three modes
- to_string() serialises both new fields
- 10 new tests covering query parsing, pset paths, combined query+view,
  clash visibility, roundtrip serialisation, and the new spec examples

https://claude.ai/code/session_01CKpkhWhzkzR5K5PDSR8N5F
2026-05-09 07:27:35 +00:00
9afeae5d44 bcf_api: add topic filtering; documents_api: add GET /document-metadata
BCF list_topics now maps topic_status, assigned_to, label, modified_after,
modified_before query params to Forgejo issue API params.

documents_api adds GET /document-metadata/{document_id} which decodes the
document_id and proxies Forgejo's contents endpoint for file metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:24:52 +01:00
63a6b2bfa6 documents_api: implement OpenCDE Documents API 1.0
POST /documents/1.0/document-versions resolves document_ids (base64url
encoding of owner/repo/path) to versioned download URLs. Exposes only
git tags + the current HEAD commit — mirrors CDE conventions where only
released versions are visible, while still surfacing the WIP state.

version_number is the tag name for releases and short SHA for HEAD.
Tagged SHAs are deduplicated so HEAD at a tag doesn't create a duplicate.
Download URLs point at Forgejo's existing raw/commit endpoint.

Auth forwarded verbatim. No Go code, no Forgejo recompilation.
Proxy at /documents/ on the Forgejo hostname.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:27:25 +01:00
9e1a10e0b6 bcf_api: implement BCF 3.0 REST API as Python routes in the preview service
Stateless translation layer over Forgejo issues and comments. Auth is
forwarded verbatim (Authorization: Bearer), so Forgejo enforces per-user
permissions with no service token. Viewpoints are ifc:// URLs stored in
comment bodies; GUIDs are deterministically derived from Forgejo IDs and
are reversible without a lookup table.

Endpoints: GET/POST projects, topics, comments, viewpoints; PUT topic;
GET viewpoint snapshot (proxied through preview service).

Also adds ifc_url_to_bcf_viewpoint() to bcf.py (symmetric counterpart of
bcf_viewpoint_to_ifc_url), and fixes rate-limit state leaking between tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:38:34 +01:00
ee57e812e5 service: add GET /foundation/versions OpenCDE discovery endpoint
Returns the Foundation API 1.1 version document listing BCF 3.0 and
Documents 1.0 API base URLs. Public, no auth required. Derives the
public base URL from X-Forwarded-Host/X-Forwarded-Proto headers so it
works correctly behind an nginx/Caddy reverse proxy with no extra config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:20:54 +01:00
930b8b5e32 bcf: add bcf_viewpoint_to_ifc_url() and IfcUrl.to_string()
Implements SPECIFICATION.md §6 BCF→ifc:// direction: parse a BCF 3.0
REST viewpoint dict (camera, clips, selection/visibility) and apply it
to a base IfcUrl to produce an ifc:// URL string.

IfcUrl.to_string() serialises any IfcUrl back to a valid ifc:// URL,
enabling round-trip parse→to_string for all URL forms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:04:04 +01:00
4df69d229d tests: add TestCacheCli covering list, clear, and prune CLI paths
9 tests exercise _cmd_cache() directly with fake cache dirs and a
monkeypatched user_cache_dir, covering output formatting, empty states,
prune with --max-gb arg, prune via IFCURL_CACHE_MAX_GB env, and the
error exit when neither is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:43:43 +01:00
e66460fb13 service+viewer: add /select endpoint for complex selector resolution
New GET+POST /select endpoint resolves an ifc:// URL selector to a JSON
list of GlobalIds, with T3 caching. The viewer now detects non-type-name
selectors (property filters, Name=, pset notation) and falls back to a
/select round-trip, applying the returned GUIDs via getLocalIdsByGuids().

Closes ifcurl-ov6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:20:41 +01:00
6e62f9e51c git.py: log warning and return is_stale flag when mutable-ref fetch fails
When a branch/HEAD fetch fails (network error, auth failure, server down),
_open_remote now logs a warning rather than silently passing.  A boolean
is_stale propagates through _get_repo → fetch_ifc so callers know the data
comes from a stale local cache.

The service adds X-Ifcurl-Cache: stale to /preview and /render_diff responses
when stale data was served, so Forgejo or a browser viewer can surface a
warning to the user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 09:05:13 +01:00
a6206a459e style: ruff --fix + black across ifcurl/ and tests/
Import sorting, quoted type annotation removal, and black reformatting.
No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:34:56 +01:00
96aa1a2377 diff rendering: two-pass IFC diff with green/blue/red colouring
Implements the ifcgit/Bonsai diff algorithm for the preview service:

diff.py: step_ids_from_diff() parses git diff text (regex on +#NNN= / -#NNN=
lines) to classify step IDs as added/modified/removed.  expand_step_ids()
walks IfcShapeRepresentation, IfcObjectPlacement, IfcPropertySet, and
IfcTypeProduct relationships to promote changed sub-entities to the parent
IfcProduct that is visually affected.

render.py: render_diff() does two passes over the same camera.  Pass 1 renders
the head model with green=added, blue=modified, ghost=unchanged.  Pass 2
renders only the removed elements from the base model in red, using the camera
auto-fitted to pass 1.  PIL composites the passes: wherever pass 2 is not
white background it stamps onto pass 1.  Moved elements (modified) appear once
in blue with no doubling artefact.

git.py: diff_text() runs git diff against the cached bare repo clone and
returns the raw text for diff.py to parse.

service.py: GET+POST /render_diff endpoint.  Fetches both commits, gets
diff text, runs the full pipeline sandboxed.  Caches on disk when both refs
are immutable.  Pillow added to render/service extras.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 06:52:01 +01:00
65a62ffc8b sandbox.py: subprocess isolation for ifcopenshell parse + render
All ifcopenshell calls (from_string, filter_elements, geom.iterator) now
run inside a short-lived child process via run_sandboxed().  A SIGSEGV or
SIGABRT in the child returns HTTP 422 instead of killing the service worker.
Configurable timeout (IFCURL_SANDBOX_TIMEOUT, default 120s) and optional
resource limits (IFCURL_SANDBOX_MEMORY_MB, IFCURL_SANDBOX_CPU_SECS).

The T3 GUID cache still works: the sandboxed pipeline returns resolved
GUIDs on selector miss so the parent can populate the cache; on hit the
parent passes cached GUIDs and the child converts them to step IDs.

Tests use a sync_sandbox fixture that runs run_sandboxed synchronously so
mock patches remain visible; four dedicated sandbox tests cover normal
return, exception propagation, segfault detection, and timeout.

Closes ifcurl-vm5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:38:07 +01:00
39a2786872 Private repo previews, BCF import, SERVICE_TOKEN support
- GET /preview now accepts a token= query parameter, matching POST /preview
- Forgejo Go: add SERVICE_TOKEN config field; renderer appends it to <img src>
  preview URLs so a machine-user token can authenticate private repos
- Go tests: verify token appears in preview URL when set, absent when unset
- viewer.html: BCF import via drag-and-drop or Import .bcf button in BCF row;
  parses first viewpoint camera/clips/GUIDs and reloads with the viewpoint
  applied to the current model context
- viewer-url.js, viewer.html: unescape %24 → $ in URL building so GlobalIds
  with $ characters are human-readable in ifc:// URLs
- README: document SERVICE_TOKEN and private-repo preview configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 14:20:17 +01:00
01de9fdcac Fix 7 issues from deep review
- pyproject.toml: add bcf to [service] dependencies (ifcurl-9rk)
- README.md: replace bogus GET /viewer row with POST /bcf (ifcurl-ttb)
- viewer-url.js: fix GitLab host detection (includes → startsWith) (ifcurl-3n4)
- git.py: call _evict_if_needed() after fetch, not only after clone (ifcurl-t1k)
- bcf.py: add description param to build_bcf(); markup.bcf now includes
  <Description> when provided (ifcurl-7wg)
- service.py: pass request URL as description to build_bcf() (ifcurl-7wg)
- viewer.html: BCF export reads visibility= from URL and sets
  DefaultVisibility accordingly (ifcurl-sto)
- viewer.html: applyCameraParam now switches to orthographic projection
  and applies scale when scale= is present in URL (ifcurl-4l7)
- tests: 3 new tests for description field; update JS tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:57:06 +01:00
dae1435519 Add BCF export: /bcf endpoint, build_bcf(), viewer panel
- ifcurl/bcf.py: build_bcf() builds BCF 2.1 zips with perspective/
  orthographic camera, clipping planes, component selection, and
  isolate visibility mode
- ifcurl/service.py: /bcf POST endpoint parses ifc:// URL, resolves
  selector → GUIDs when present, delegates to build_bcf()
- viewer.html: BCF toolbar button + collapsible title/comment form;
  client-side zip generation via JSZip (esm.sh CDN) so the browser
  does not need service access
- tests/test_bcf.py: 21 tests covering build_bcf() unit behaviour and
  the /bcf endpoint including SSRF rejection and selector resolution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:59:22 +01:00
335349db1a git.py: LRU disk cache eviction; service.py: configurable in-memory limits
Disk cache (bare repo clones under ~/.cache/ifcurl/):
- IFCURL_CACHE_MAX_GB env var sets the max total size; when exceeded after a
  new clone the oldest-accessed repos are removed until under the limit
- remote_url stored per clone entry so ifcurl cache list can show the URL
- _touch_cache() updates mtime on every repo open for LRU tracking

In-memory caches (service.py):
- _T2_MAX and _T3_MAX now read from IFCURL_T2_MAX / IFCURL_T3_MAX env vars
  (defaults: 8 IFC blobs, 64 selector results) — configurable without rebuild

New CLI:  ifcurl cache list | prune [--max-gb N] | clear

Tests: 6 new unit tests for _dir_size, _touch_cache, _repo_cache_entries,
_evict_if_needed. 118 → 124 passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:06:56 +01:00
5d5b8c6516 tests: extend coverage — SSRF protection, GET endpoint, LRU eviction, branch ref
- SSRF: test local transport rejection, allowlist enforcement, private/loopback/
  link-local IP blocking, allowlist with port, public host permitted without list
- GET /preview: happy path, invalid scheme, local transport rejection
- Tier 2 LRU: eviction at capacity, LRU promotion, cache miss
- git: heads/<branch> ref form (was only tested via HEAD/tag/hash)
- clear_caches fixture now also resets _allowed_hosts between tests

103 → 118 passing tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 08:38:03 +01:00
d98d4c35d5 viewer: extract URL logic to viewer-url.js, add JS tests
parseIfcUrl, buildIfcUrl, toRawUrl extracted from viewer.html into
viewer-url.js so they can be imported and tested independently.
viewer.html imports from the module. 32 Node test-runner tests in
tests/test_viewer_url.mjs cover all three functions across GitHub,
GitLab, Forgejo/Gitea, SSH user@ prefix, and round-trip cases.

Caught a bug: + (selector union operator) was being URL-encoded as
%2B in buildIfcUrl; fixed by adding .replace(/%2B/g, "+") alongside
the existing comma fix, in both viewer-url.js and syncCameraUrl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 07:31:19 +01:00
cf935a2a49 FIxes 2026-04-22 08:31:24 +01:00
778f0004e7 Fix port preservation and HTTP fallback for local Gitea
- url.py: preserve non-standard ports in host field; ifc://host:3000/...
  now correctly generates https://host:3000/... rather than dropping :3000
- git.py: fall back from https to http on clone/fetch failure, enabling
  local Gitea instances running on plain HTTP; inject tokens into http://
  URLs as well as https://; extract _clone_bare() helper
- tests/test_url.py: three new tests for port preservation in host,
  git_remote_url with non-standard HTTPS port, and SSH with non-standard port
2026-04-15 23:50:54 +01:00
7f92cadc50 Authentication and test coverage
- ifcurl/auth.py: token config (~/.config/ifcurl/tokens.json),
  get_token_for_host(), inject_token() for HTTPS URL credential injection
- ifcurl/git.py: thread token= through fetch_ifc/fetch_ifc_bytes/_open_remote;
  authenticated fetch uses 'git fetch <auth_url> +refs/*:refs/*' so the
  token is never written to on-disk git config
- ifcurl/service.py: optional token field in PreviewRequest; request token
  takes precedence over config-file token; get_token_for_host imported at
  module level for clean monkeypatching in tests
- tests

Generated with the assistance of an AI coding tool.
2026-04-15 23:21:13 +01:00
7e83c00487 Python core library and ifcurl CLI
- URL parser (IfcUrl) handling all ifc:// forms: SSH, HTTPS, local;
  all ref types; camera, selector, clip, visibility parameters
- Git fetcher using GitPython with OS-appropriate bare-clone cache
  (platformdirs) and fetch-on-mutable-ref behaviour
- Renderer adapted from ifcquery: explicit camera, clipping planes,
  highlight/ghost/isolate visibility modes, type entity support
- ifcurl CLI: `ifcurl render "<url>" [-o output.png]`
- LICENSE file (LGPLv3+)

Generated with the assistance of an AI coding tool.
2026-04-15 21:24:35 +01:00