# Project cache index: format and build procedure (`project-cache-index/1`)

Installed by FTR-15 (Project Cache) as `admin\feature-prompts\project-cache-index.md`. Read it before building, checking or changing `INDEX.json` or `INDEX.md`.

## A.1 Purpose and files

An agent cannot read a whole project cache and should not have to. This spec defines the generated index that tells it what each cached file covers and when to open it.

| File | Location | For | Notes |
|---|---|---|---|
| `INDEX.json` | `project_cache\` | agents, scripts, checks | Full record. UTF-8 without BOM. Do not drag into Project Files. |
| `INDEX.md` | `project_cache\` | agents and people | Generated from `INDEX.json`, never from memory. Upload with the other cached files. |

Both are generated and never edited by hand. Neither is a manifest row. Unknown fields found in an existing `INDEX.json` are preserved when rewriting.

## A.2 INDEX.json top level

| Field | Type | Meaning |
|---|---|---|
| `schema` | string | `project-cache-index/1`. Additive changes keep `/1`; a breaking change becomes `/2`. |
| `project`, `project_code` | string | From the project instructions. |
| `cache_dir` | string | `project_cache`, relative to the project root. |
| `built` | ISO-8601 | When this index was last written. |
| `built_by` | string | Who or what wrote it. |
| `counts` | object | `files`, `est_tokens`, `tier0`, `tier1`, `tier2`, `stale`, `missing`. |
| `items` | array | One entry per indexed file (A.3). Sorted by `id`. |
| `branches` | array, optional | Topic groups: `{id, title, purpose, when, items:[ids]}`. Same shape as the plugin index. May be empty. |
| `not_shipped` | array | What is deliberately or necessarily absent: `{name, reason, where}`. Always lists `admin\AGENT_INSTRUCTIONS.md` and `CLAUDE.md` (excluded by rule). Also lists manifest rows that are Not Found or Not Yet Copied, and non-text files with no sidecar. |

## A.3 Item fields

Facts a script extracts (always current; recomputed on every build):

| Field | Meaning |
|---|---|
| `id` | Lowercase file name without extension, non-alphanumerics replaced by `-`. Unique. Stable for the life of the file; a rename is a new id. |
| `path` | The readable text view, relative to `project_cache\`, forward slashes. The file itself if it is text; otherwise its sidecar. |
| `kind` | `text` or `sidecar-backed`. |
| `original` | Only when `kind` is `sidecar-backed`: `{path, bytes, sha256}` of the real file (the .xlsx, .pdf, .docx). |
| `title` | First level-1 heading, else the file name. |
| `bytes`, `sha256` | Of the text view at `path`. SHA-256 as lowercase hex. |
| `est_tokens` | `bytes` of the text view divided by 4, rounded. |
| `outline` | `[{line, level, heading}]` for every markdown heading, with its line number, so a large file is read by line range. For a sidecar-backed spreadsheet, one entry per sheet with its row count. |
| `source` | Source path from the manifest. The source of truth for the file. |
| `origin` | `ours`, `upstream` (an original from someone else), `derived`, or `unregistered` when the manifest has no row. |
| `type`, `priority`, `copied`, `status` | From the manifest (FTR-15 columns Type, Priority, Last Copied, Status). |

Facts the model writes once per file version (stored with the hash they describe):

| Field | Rule |
|---|---|
| `summary` | What the file contains, 60 words at most, faithful to the file. No opinion, nothing not in the file. If the file carries a binding rule (a standard, a precedence statement), the first sentence says so. |
| `read_when` | Up to 3 tasks or questions that need this file, written the way an agent would be given them. |
| `topics` | Up to 8 terms an agent would search for. |
| `covers` | Optional: specific names a reader would look up (section numbers, document ids, party or account names that are in the file). Never a secret. |
| `related` | Other item ids that go with this one. |
| `tier` | 0, 1 or 2 (A.5). |
| `summary_sha256` | The `sha256` the summary was written from. |
| `summary_state` | `current` when `summary_sha256` equals `sha256`; `stale` when the file changed; `missing` when never written. |
| `summary_basis` | `full read`, `outline-based` (Tier 2 files), or `sidecar`. |
| `summarized_at`, `summarized_by` | Date, and the model or conversation that wrote it. |

Example (placeholders, not real values):

```json
{
  "schema": "project-cache-index/1",
  "project": "Example Project",
  "project_code": "EXP",
  "cache_dir": "project_cache",
  "built": "2026-10-25T16:30:00-04:00",
  "built_by": "claude: build cache index",
  "counts": {"files": 2, "est_tokens": 4100, "tier0": 1, "tier1": 1, "tier2": 0, "stale": 0, "missing": 0},
  "items": [
    {
      "id": "tracker-index",
      "path": "tracker_index.md",
      "kind": "text",
      "title": "Tracker index",
      "bytes": 594,
      "sha256": "<64 lowercase hex characters>",
      "est_tokens": 149,
      "outline": [{"line": 1, "level": 1, "heading": "Tracker index"}],
      "source": "trackers\\tracker_index.md",
      "origin": "ours",
      "type": "Index",
      "priority": "Medium",
      "copied": "2026-10-24",
      "status": "Current",
      "summary": "Lists each tracker workbook in the project and what it tracks.",
      "read_when": ["Finding which tracker holds a given kind of record"],
      "topics": ["trackers", "registry"],
      "covers": [],
      "related": ["account-records"],
      "tier": 0,
      "summary_sha256": "<same value as sha256>",
      "summary_state": "current",
      "summary_basis": "full read",
      "summarized_at": "2026-10-25",
      "summarized_by": "claude"
    },
    {
      "id": "account-records",
      "path": "account-records_sidecar.md",
      "kind": "sidecar-backed",
      "original": {"path": "account-records.xlsx", "bytes": 13600, "sha256": "<64 lowercase hex characters>"},
      "title": "Account records",
      "bytes": 9800,
      "sha256": "<64 lowercase hex characters>",
      "est_tokens": 2450,
      "outline": [{"sheet": "Sheet1", "rows": 120}],
      "source": "evidence\\account-records.xlsx",
      "origin": "upstream",
      "type": "Record",
      "priority": "High",
      "copied": "2026-10-24",
      "status": "Current",
      "summary": "Transaction rows for one account, as returned by the source organization.",
      "read_when": ["Answering a question about an individual transaction"],
      "topics": ["transactions", "account"],
      "covers": [],
      "related": ["tracker-index"],
      "tier": 1,
      "summary_sha256": "<same value as sha256>",
      "summary_state": "current",
      "summary_basis": "sidecar",
      "summarized_at": "2026-10-25",
      "summarized_by": "claude"
    }
  ],
  "branches": [],
  "not_shipped": [
    {"name": "admin\\AGENT_INSTRUCTIONS.md", "reason": "excluded by rule; reaches conversations through the Project Instructions field", "where": "admin\\AGENT_INSTRUCTIONS.md"},
    {"name": "CLAUDE.md", "reason": "excluded by rule; read from disk by Cowork and Claude Code", "where": "CLAUDE.md"}
  ]
}
```

## A.4 INDEX.md layout

```text
# Project cache index -- <project>
Generated <date> from INDEX.json. <N> files, about <T> tokens. <S> stale, <M> missing. Do not edit.
Full record: project_cache\INDEX.json. Cache files are copies; edit the source, then refresh.
How to use: find the row for your task, read that file (large files by the line ranges in INDEX.json).

## Tier 0 -- know these every session
<file> -- <one line>

## Tier 1 -- read when the task matches
| File | Tokens | Covers | Read when |

## Tier 2 -- look up by search or heading only
| File | Tokens | Covers | Outline pointer |

## Not in the cache
<name> -- <reason>
```

Budget: about 70 tokens per file in the tables. Over budget means shorten summaries, never drop files.

## A.5 Tiers

| Tier | Rule |
|---|---|
| 0 | Needed in nearly every session and small (under about 1,500 tokens), or the index line alone is enough. |
| 1 | Read whole when the task matches its `read_when`. |
| 2 | Over about 20,000 tokens, or reference material to look things up in. Never read whole; use heading and line ranges. |

## A.6 Writing summaries

- Read the whole text view before summarizing. For a Tier 2 file, read the outline and the opening and set `summary_basis` to `outline-based`.
- State what is in the file, not what it means for the project. Judgment belongs in procedures.
- Unreadable or uncertain parts: write "unclear" and flag the entry. Never guess.
- Never put a secret, key, token or credential value in any field. If a text view contains one, do not index the file; list it under `not_shipped` and tell the person (count and file only).
- Never overwrite a `current` summary. A changed file keeps its old summary marked `stale` until rewritten, so the index is never blank and never silently wrong.

## A.7 Non-text files

A non-text file (.xlsx, .pdf, .docx) is indexed only through its markdown sidecar (FTR-12; .xlsx uses its approved raw zip/XML method with UTF-8 stated on every read). The sidecar is registered in the manifest and copied to `project_cache\` like any other file. If no sidecar exists, do not index the file; list it under `not_shipped` with the reason "no text view; offer a sidecar" and offer to create one. The original is never modified. If FTR-12 (Markdown Sidecars) is not installed, offer to install it; until then non-text files stay under `not_shipped`.

## A.8 Build procedure

Use the access methods the project already has (PowerShell via the Windows MCP). Always read and write text with UTF-8 stated explicitly, and write files as UTF-8 without BOM.

1. Preconditions: `project_cache\` exists and is flat (flag any subdirectory); the manifest is readable (the FTR-36 workbook if present, otherwise the markdown manifest at `admin\Project_Cache_Manifest.md`).
2. Inventory the files at the root of `project_cache\`, excluding `INDEX.md` and `INDEX.json`. Join each to its manifest row by file name. A file with no row is indexed with `origin: unregistered` and reported. A manifest row that is Not Found or Not Yet Copied goes to `not_shipped`.
3. For each text view compute `bytes`, `sha256`, `est_tokens`, `title` and `outline`. Illustrative, untested:
   - `(Get-FileHash -LiteralPath $f -Algorithm SHA256).Hash.ToLower()`
   - `Select-String -LiteralPath $f -Pattern '^#{1,6}\s' -Encoding UTF8` gives each heading with `LineNumber`
4. Load the existing `INDEX.json` if there is one. Same `sha256` as before: keep the model-written fields. Changed: keep them, set `summary_state` to `stale`. New file: set `missing`.
5. For every `stale` or `missing` entry, read the text view and write `summary`, `read_when`, `topics`, `covers`, `related`, `tier`, `summary_basis`, and the `summary_*` fields. For a first build over about 20 files or more, offer Cowork (FTR-20).
6. Before overwriting, back up the previous `INDEX.json` following FTR-36's backup convention, outside `project_cache\` (the cache stays flat and is dragged whole).
7. Write `INDEX.json` (UTF-8 no BOM, two-space indent, items sorted by `id`, keys in the order of A.2 and A.3 so diffs stay readable), then generate `INDEX.md` from it.
8. Verify: re-read both files; run the check in A.9; report "Cache index: N entries · N current · N stale · N missing · about T tokens".

## A.9 Check, and plugin compatibility

"check cache index" passes only when: `INDEX.json` parses and has a supported `schema`; every file at the root of `project_cache\` (except `INDEX.*` and non-text files listed in `not_shipped`) has exactly one entry; every entry's file exists and its `sha256` matches; no `summary_state` is `stale` or `missing`; `AGENT_INSTRUCTIONS.md` and `CLAUDE.md` are absent from `items`; `INDEX.md` matches what would be generated from `INDEX.json`.

Compatibility: the format deliberately reuses the plugin index field names (`items`, `id`, `path`, `title`, `summary`, `topics`, `covers`, `bytes`, `sha256`, `branches`, `not_shipped`). The unchanged plugin `ContextStore` (stdlib only, `context_index.py`) can load it and answer `search`, `where`, `index` and `read` against `project_cache\`. One known difference: `ContextStore.verify()` flags any file in the folder that is not an item's `path`, so a cached non-text original is reported as "not in the index". Until `verify()` learns about `original`, the check above is authoritative for caches that hold non-text files.

## A.10 Projects without a manifest

A project with no manifest (for example one that tracks its cache in a CSV) joins on file name only; the missing manifest columns are left out of those entries.
