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>
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>
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>
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>
- 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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
- 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>
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>
- 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>
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>
- 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
- 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.