AGENTS.md@infrastructure/fonds · git:20260705.480f0e5 · 2026-07-05 · sha256 bede1de947fd3eab

AGENTS.md@infrastructure/fonds git:20260705.480f0e5A

Immutable. This exact content is served forever at /api/v1/blob/bede1de947fd3eab.

# infrastructure/fonds/ — Agent Notes

## Purpose

Discovery, validation, scope, and private-sidecar symlink sync for the
top-level `fonds/` directory — a parallel workspace to `projects/` for
**archival collection units** (fonds in the archival sense: stable pools of
reusable research resources — bibliographies, contact registries, dataset
catalogs — distinct from executable research projects).

The design mirrors `infrastructure/project/` adapted for passive data stores:

| Fonds module | Project analog | Role |
|---|---|---|
| `discovery.py` | `project/discovery.py` | `discover_fonds`, `resolve_fond_root`, subfolder constants |
| `fonds_info.py` | `project/discovery.py` (partial) | `FondInfo` dataclass + `build_fond_info` |
| `validation.py` | `project/discovery.py` (partial) | `validate_fond_structure` |
| `public_scope.py` | `project/public_scope.py` | `PUBLIC_FOND_NAMES`, helpers |
| `linking.py` | `project/linking.py` | Private sidecar symlink sync |

## Module Map

| File | Exports |
|------|---------|
| `discovery.py` | `discover_fonds`, `resolve_fond_root`, `NON_RENDERED_FOND_SUBDIRS`, `RENDERED_FOND_SUBDIRS` |
| `fonds_info.py` | `FondInfo`, `build_fond_info` |
| `validation.py` | `validate_fond_structure` |
| `public_scope.py` | `PUBLIC_FOND_NAMES`, `public_fond_infos`, `public_fond_names`, `public_fond_data_paths` |
| `linking.py` | `sync_private_fond_links`, `sync_active_fond_links`, `private_fonds_root`, `is_managed_symlink`, `LIFECYCLE_LINK_DIRS`, `LinkSyncResult` |
| `__init__.py` | Re-exports: `FondInfo`, `build_fond_info`, `discover_fonds`, `resolve_fond_root`, `validate_fond_structure` |

## Public API

### `discovery.py` (re-exported from `infrastructure.fonds`)

- `discover_fonds(repo_root) -> list[FondInfo]` — scan `fonds/` for valid
  fonds; skips `NON_RENDERED_FOND_SUBDIRS` (`working`, `archive`). Supports
  program directories (non-fond parent containing multiple fonds) and category
  groupings (child dirs starting with `_`, one level deep).
- `resolve_fond_root(repo_root, fond_name) -> Path` — resolve a fond by
  qualified name. Qualified `<head>/<name>` (head in `templates/`, `working/`,
  `archive/`) resolves directly under `fonds/<head>/<name>`. Bare names prefer
  `fonds/templates/<name>`, then `fonds/<name>`.
- `NON_RENDERED_FOND_SUBDIRS: frozenset[str]` — `{"working", "archive"}`.
- `RENDERED_FOND_SUBDIRS: frozenset[str]` — `{"templates"}`.

```python
from infrastructure.fonds import discover_fonds, resolve_fond_root
from pathlib import Path

fonds = discover_fonds(Path("."))
for f in fonds:
    print(f.qualified_name, f.fond_type, f.is_valid)

fond_path = resolve_fond_root(Path("."), "templates/template_bibliography")
```

### `fonds_info.py` (re-exported from `infrastructure.fonds`)

- `FondInfo` — dataclass for a discovered fond.
- `build_fond_info(fond_dir, program="") -> FondInfo` — build from a directory;
  loads `fonds.yaml` manifest.

```python
@dataclass
class FondInfo:
    name: str              # Directory name
    path: Path             # Absolute path
    fond_type: str         # "type" field from fonds.yaml, or "generic"
    has_data: bool         # Has data/ directory
    has_manuscript: bool   # Has manuscript/ directory
    metadata: dict         # Raw fonds.yaml content
    program: str           # Parent program dir ("" for standalone)

    @property
    def qualified_name(self) -> str: ...  # "name" or "program/name"
    @property
    def is_valid(self) -> bool: ...        # True iff has_data
```

### `validation.py` (re-exported from `infrastructure.fonds`)

- `validate_fond_structure(fond_dir) -> tuple[bool, str]` — gate on required
  layout: `fonds.yaml` manifest + `data/` directory.

```python
is_valid, message = validate_fond_structure(Path("fonds/my_bibliography"))
# (True, "Valid fond structure")
# (False, "Missing required file: fonds.yaml")
# (False, "Missing required directory: data")
```

**Required:** `fonds.yaml`, `data/`  
**Optional:** `manuscript/`, `scripts/`, `tests/`

### `public_scope.py`

- `PUBLIC_FOND_NAMES: tuple[str, ...]` — CI-safe roster of tracked exemplar
  fonds. Currently: `("templates/template_bibliography", "templates/template_contacts", "templates/template_datasets")`.
- `public_fond_infos(repo_root) -> list[FondInfo]` — discovered fonds in the
  public roster present in this checkout.
- `public_fond_names(repo_root) -> list[str]` — sorted qualified names present.
- `public_fond_data_paths(repo_root) -> list[Path]` — `data/` paths for public
  fonds (repo-relative; for CI lint/type-check scope).

### `linking.py`

- `sync_private_fond_links(repo_root, private_root=None, *, prune=True, dry_run=False) -> LinkSyncResult` —
  mirror private lifecycle folders into `fonds/working/` and `fonds/archive/`
  via symlinks. No-op when no private root is found.
- `sync_active_fond_links(...)` — compatibility alias.
- `private_fonds_root(repo_root) -> Path | None` — resolve private sidecar
  root: `$TEMPLATE_FONDS_ROOT` → `<repo_root>/.fonds_root` → sibling `../fonds`
  (requires `working/` + `archive/`).
- `is_managed_symlink(path, private_root) -> bool` — True iff path is a syncer-
  managed symlink (points into expected private lifecycle subtree).
- `LIFECYCLE_LINK_DIRS: dict[str, str]` — `{"working": "fonds/working", "archive": "fonds/archive"}`.
- `PROTECTED_NAMES: frozenset[str]` — bare exemplar names; syncer never touches.
- `ENV_VAR = "TEMPLATE_FONDS_ROOT"`, `SKIP_ENV_VAR = "TEMPLATE_SKIP_FOND_LINK_SYNC"`.

```python
@dataclass
class LinkSyncResult:
    created: list[str]; updated: list[str]; removed: list[str]; skipped: list[str]
    private_root: Path | None
    @property
    def changed(self) -> bool: ...
    def summary(self) -> str: ...
```

**CLI:**

```bash
uv run python -m infrastructure.fonds.linking --dry-run
uv run python -m infrastructure.fonds.linking
uv run python -m infrastructure.fonds.linking --no-prune
uv run python -m infrastructure.fonds.linking --private-root /path/to/fonds
```

## Fond Structure Requirements

A valid fond must have:
- `fonds.yaml` — manifest (required by `validate_fond_structure`)
- `data/` — resource data (required)

Optional: `manuscript/`, `scripts/`, `tests/`

Unlike projects, no Python code is required — fonds are passive data stores.

## Discovery Scope

```
fonds/
├── templates/               ← RENDERED_FOND_SUBDIRS (discovered by discover_fonds)
│   ├── template_bibliography/   ← public exemplar
│   ├── template_contacts/
│   └── template_datasets/
├── working/                 ← NON_RENDERED_FOND_SUBDIRS (skipped, sidecar links)
│   └── myfond -> /private/fonds/working/myfond
└── archive/                 ← NON_RENDERED_FOND_SUBDIRS (skipped, sidecar links)
    └── old_fond -> /private/fonds/archive/old_fond
```

Category groupings (`_<category>/`) nest one level deep inside any directory.

## Boundaries

- **No project-pipeline coupling.** Does not participate in
  `discover_projects()` or `run.sh` stage execution.
- **`fonds/` vs `projects/`.** Orthogonal top-level namespaces.
- **Sidecar links are local-only.** Only `PUBLIC_FOND_NAMES` is CI-safe.
- **Safety invariants (never violated by linker):**
  - Real directories and unmanaged symlinks are never touched.
  - `PROTECTED_NAMES` (public exemplars) are always skipped.
  - No-op when the private sidecar is absent.

## Environment Variables

| Variable | Purpose | Default |
|----------|---------|---------|
| `TEMPLATE_FONDS_ROOT` | Override private fonds root | unset |
| `TEMPLATE_SKIP_FOND_LINK_SYNC` | Skip auto-sync in orchestration CLI | unset |

## See Also

- [`../project/AGENTS.md`](../project/AGENTS.md) — canonical project-layer analog
- [`../AGENTS.md`](../AGENTS.md) — infrastructure layer overview
- [`README.md`](README.md) — quick reference
- [`SKILL.md`](SKILL.md) — agent routing contract