Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
74d0600
feat(filesystem): add PageIndex filesystem shell
BukeLy May 25, 2026
574125d
fix(filesystem): add pifs ask and chat commands
BukeLy May 26, 2026
e074bf1
fix(filesystem): package pifs cli entrypoint
BukeLy May 26, 2026
3625a59
fix(filesystem): stream pifs chat by default
BukeLy May 26, 2026
3d62293
fix(filesystem): avoid duplicate pifs chat output
BukeLy May 26, 2026
b9ee711
fix(filesystem): require target-first cat syntax
BukeLy May 26, 2026
5a78131
fix(filesystem): suppress chat input echo while streaming
BukeLy May 26, 2026
144e8ba
feat(pifs): add find maxdepth traversal limit
BukeLy May 26, 2026
06d8553
fix(filesystem): clarify folder path metadata errors
BukeLy May 26, 2026
a80b84d
fix(filesystem): remove session ref aliases from cli output
BukeLy May 26, 2026
b208971
refactor(filesystem): remove reference id concept
BukeLy May 26, 2026
cb1bfb0
fix(filesystem): reuse agent session in pifs chat
BukeLy May 26, 2026
beed216
fix(filesystem): remove chat request started noise
BukeLy May 26, 2026
c12c2de
fix(filesystem): guide structural node reads
BukeLy May 26, 2026
ec96812
fix(filesystem): scope pifs chat identity
BukeLy May 26, 2026
8ae94ad
fix(filesystem): render root folder path correctly
BukeLy May 26, 2026
5c948d0
fix(filesystem): require metadata evidence in chat
BukeLy May 26, 2026
7e70b58
fix(filesystem): show generated metadata in stat
BukeLy May 26, 2026
fc0be1a
fix(filesystem): restore summary vector search in cli
BukeLy May 26, 2026
7c021a7
refactor(filesystem): unify pifs metadata state
BukeLy May 26, 2026
de1992d
refactor(filesystem): make pifs providers configurable
BukeLy May 26, 2026
30830fc
refactor(filesystem): remove pre-release store migrations
BukeLy May 26, 2026
b658735
fix(demo): register all example documents
BukeLy May 26, 2026
c421626
fix(filesystem): constrain pifs agent tool identity
BukeLy May 26, 2026
75acf5d
fix(filesystem): generalize pifs tool identity prompt
BukeLy May 26, 2026
fe1c4ae
fix(filesystem): search workspace topic questions before clarifying
BukeLy May 26, 2026
3519adf
fix(filesystem): preserve sqlite catalog durability
BukeLy May 26, 2026
ad45f96
fix(filesystem): use summary projection for default semantic search
BukeLy May 26, 2026
2297453
fix(filesystem): avoid replaying failed agent runs
BukeLy May 26, 2026
cb9db0b
fix(filesystem): guard summary index dimensions
BukeLy May 26, 2026
9734bf6
fix(filesystem): enforce pifs shell command limits
BukeLy May 26, 2026
70eece5
fix(filesystem): require real grep source lines
BukeLy May 26, 2026
e945372
fix(filesystem): lazy-load pifs import surface
BukeLy May 26, 2026
8e0f295
fix(filesystem): detect ambiguous virtual paths
BukeLy May 26, 2026
112ef99
fix(filesystem): escape sqlite like wildcards
BukeLy May 26, 2026
c86d572
fix(filesystem): preserve pageindex registration errors
BukeLy May 26, 2026
d139181
fix(filesystem): delay register side effects until insert
BukeLy May 26, 2026
7104602
refactor(filesystem): simplify semantic search result fields
BukeLy May 26, 2026
4a158f9
fix(filesystem): label semantic search paths in cli output
BukeLy May 26, 2026
2b69719
feat(filesystem): support space-separated cat node ids
BukeLy May 26, 2026
cd571dd
fix(filesystem): relax structural read limits
BukeLy May 26, 2026
311c7b5
fix(filesystem): preserve lazy filesystem exports
BukeLy May 26, 2026
346eb0a
chore(filesystem): remove uv project files
BukeLy May 26, 2026
c13cb20
ci(codeql): add python analysis job
BukeLy May 26, 2026
0243732
fix(filesystem): clarify agent evidence recovery
BukeLy May 26, 2026
977f596
fix(filesystem): return stable semantic search targets
BukeLy May 26, 2026
4ff184f
feat(filesystem): add pifs workspace default
BukeLy May 27, 2026
a70fc67
chore(filesystem): ignore local pifs artifacts
BukeLy May 27, 2026
21c83b6
fix(filesystem): run pifs from local venv
BukeLy May 27, 2026
8071889
chore(filesystem): remove duplicate pifs cli wrapper
BukeLy May 27, 2026
ba821a7
feat(pifs): add semantic browse command
BukeLy May 31, 2026
3562d47
fix(pifs): scope browse vector search before paging
BukeLy May 31, 2026
c63ebe5
feat(pifs): render browse results as records
BukeLy May 31, 2026
27071cb
refactor(pifs): converge semantic retrieval on browse
BukeLy May 31, 2026
95e5717
fix(pifs): route agent retrieval through browse
BukeLy May 31, 2026
b5cc404
refactor(pifs): remove legacy command prompt filter
BukeLy May 31, 2026
58409d1
feat(filesystem): default embeddings to 1024 dimensions
BukeLy May 31, 2026
decfe29
fix(filesystem): avoid eager vector imports for embedding defaults
BukeLy May 31, 2026
e293814
fix(filesystem): reject mismatched existing projection indexes
BukeLy May 31, 2026
20b2225
fix(pifs): surface projection dimension mismatches
BukeLy May 31, 2026
8f87cee
fix(pifs): avoid vector imports for empty workspaces
BukeLy May 31, 2026
7096ba1
feat(pifs): add workspace file import command
BukeLy May 31, 2026
1c5ed63
fix(pifs): keep add imports semantically atomic
BukeLy May 31, 2026
8cdddb5
fix(pifs): roll back add-created folders
BukeLy May 31, 2026
eca5edd
fix(pifs): roll back add pageindex cache
BukeLy May 31, 2026
2d55fd2
fix(pifs): clean partial add pageindex cache
BukeLy May 31, 2026
d9ce184
Merge Goal 2: semantic browse command
BukeLy May 31, 2026
32fce16
Merge Goal 3: browse output contract
BukeLy May 31, 2026
889db8c
Merge Goal 1: converge semantic retrieval on browse
BukeLy May 31, 2026
a7a1165
Merge Goal 4: route agent retrieval through browse
BukeLy May 31, 2026
01af0c6
Merge Goal 6: default embeddings to 1024 dimensions
BukeLy May 31, 2026
d3034fa
Merge Goal 5: workspace file import command
BukeLy May 31, 2026
0f71da3
refactor(pifs): remove dead semantic retrieval code (#33)
BukeLy May 31, 2026
1c6c2ca
Merge branch 'VectifyAI:main' into feat/pageindex-filesystem
BukeLy May 31, 2026
34fa8f7
refactor(filesystem): inline embedding dimension default
BukeLy May 31, 2026
e368562
refactor(filesystem): consolidate semantic projection modules
BukeLy May 31, 2026
d0c0c67
fix(filesystem): remove cat node reads
BukeLy May 31, 2026
b9e3095
Remove unused PIFS text window commands
BukeLy May 31, 2026
dc4de31
refactor: remove source paths from PIFS
BukeLy May 31, 2026
b19322d
fix(filesystem): tighten PIFS grep and page-read policy
BukeLy Jun 1, 2026
07aee66
add PIFS Semantic Folder build (#34)
BukeLy Jun 1, 2026
a4009cf
fix(filesystem): make find output shell-like
BukeLy Jun 1, 2026
f4fde16
fix(filesystem): harden PIFS review regressions
BukeLy Jun 2, 2026
e83fbe0
revert(filesystem): remove PIFS Semantic Folder v1 (#36)
BukeLy Jun 3, 2026
87b790a
cleanup(pifs): align bashlike command surface (#37)
BukeLy Jun 27, 2026
cb6c8f9
chore(gitignore): ignore repo docs and examples
BukeLy Jun 27, 2026
17004ee
refactor(pifs): simplify metadata handling (#38)
BukeLy Jun 27, 2026
9a57804
feat(pifs): add metadata scope paths (#40)
BukeLy Jun 28, 2026
18bccbe
Merge branch 'VectifyAI:main' into feat/pageindex-filesystem
BukeLy Jun 28, 2026
15a3181
test(pifs): verify metadata virtual node reachability (#41)
BukeLy Jun 28, 2026
0f12bdb
test(pifs): lock dynamic flattening agent policy (#42)
BukeLy Jun 28, 2026
eb1d867
fix(pifs): honor CLI embedding config (#43)
BukeLy Jun 28, 2026
96e939c
fix(pifs): load embedding runtime config (#44)
BukeLy Jun 28, 2026
34b6056
fix(pifs): hide browse internals (#45)
BukeLy Jun 28, 2026
21b163b
fix(filesystem): align tree depth with chat defaults
BukeLy Jun 29, 2026
6a77a49
fix(filesystem): stop clamping explicit tree depth
BukeLy Jun 29, 2026
4f0023b
fix(filesystem): merge main into filesystem branch
BukeLy Jul 4, 2026
6481551
fix(filesystem): round-trip scope-qualified file locators (#46)
BukeLy Jul 8, 2026
a938937
fix(filesystem): prefer canonical evidence checks
BukeLy Jul 9, 2026
f4fb9d7
refactor(filesystem): simplify PIFS core surface (#47)
BukeLy Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ permissions:
actions: read

jobs:
analyze:
analyze-actions:
name: Analyze (actions)
runs-on: ubuntu-latest
timeout-minutes: 30
Expand All @@ -32,3 +32,22 @@ jobs:
uses: github/codeql-action/analyze@v4
with:
category: /language:actions

analyze-python:
name: Analyze (python)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: python
build-mode: none

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: /language:python
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ __pycache__
.DS_Store
.env*
.venv/
.claude/
.codex/
logs/
examples/pifs_workspace/
examples/
/CONTEXT.md
/docs/pifs-core-alignment/
/pyproject.toml
/uv.lock
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ python3 run_pageindex.py --pdf_path /path/to/your/document.pdf
You can customize the processing with additional optional arguments:

```
--model LLM model to use (default: gpt-4o-2024-11-20)
--model LLM model to use (default: gpt-5.4)
--toc-check-pages Pages to check for table of contents (default: 20)
--max-pages-per-node Max pages per node (default: 10)
--max-tokens-per-node Max tokens per node (default: 20000)
Expand Down
46 changes: 46 additions & 0 deletions examples/pifs_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""A minimal PageIndex FileSystem CLI walkthrough.

Configure the Summary Embedding Profile in your PIFS config and provide its API
key through ``PIFS_EMBEDDING_API_KEY`` before running this example.
"""

from __future__ import annotations

import argparse
import subprocess
from pathlib import Path


def run(workspace: Path, document: Path, query: str) -> int:
target = f"/documents/{document.name}"
commands = [
["add", str(document), "/documents"],
["tree", "/documents", "-L", "1"],
["browse", "/documents", query],
["stat", target],
["cat", target, "--structure"],
["cat", target, "--page", "1"],
["grep", query, target],
]
for command in commands:
print(f"\n$ pifs {' '.join(command)}")
result = subprocess.run(
["pifs", "--workspace", str(workspace), *command],
check=False,
)
if result.returncode:
return result.returncode
return 0


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("document", type=Path, help="PDF or Markdown document")
parser.add_argument("--workspace", type=Path, default=Path("pifs-workspace"))
parser.add_argument("--query", default="key findings")
return parser.parse_args()


if __name__ == "__main__":
args = parse_args()
raise SystemExit(run(args.workspace, args.document, args.query))
26 changes: 22 additions & 4 deletions pageindex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
from .page_index import *
from .page_index_md import md_to_tree
from .retrieve import get_document, get_document_structure, get_page_content
from .client import PageIndexClient
import os

os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "true")

_OPTIONAL_CORE_IMPORTS = {"litellm", "openai", "PyPDF2", "pymupdf"}

try:
from .page_index import *
from .page_index_md import md_to_tree
from .retrieve import get_document, get_document_structure, get_page_content
from .client import PageIndexClient
except ModuleNotFoundError as exc:
if exc.name not in _OPTIONAL_CORE_IMPORTS:
raise


def __getattr__(name: str):
if name == "PageIndexFileSystem":
from .filesystem import PageIndexFileSystem

return PageIndexFileSystem
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
4 changes: 2 additions & 2 deletions pageindex/config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
model: "gpt-4o-2024-11-20"
model: "gpt-5.4"
# model: "anthropic/claude-sonnet-4-6"
retrieve_model: "gpt-5.4" # defaults to `model` if not set
toc_check_page_num: 20
Expand All @@ -7,4 +7,4 @@ max_token_num_each_node: 20000
if_add_node_id: "yes"
if_add_node_summary: "yes"
if_add_doc_description: "no"
if_add_node_text: "no"
if_add_node_text: "no"
3 changes: 3 additions & 0 deletions pageindex/filesystem/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .core import PageIndexFileSystem

__all__ = ["PageIndexFileSystem"]
33 changes: 33 additions & 0 deletions pageindex/filesystem/_embedding_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from urllib.parse import urlsplit, urlunsplit


DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"


def normalize_base_url(value: str | None) -> str:
raw = str(value or DEFAULT_OPENAI_BASE_URL).strip()
parsed = urlsplit(raw)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("embedding base URL must be an absolute HTTP(S) URL")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError(
"embedding base URL must not contain credentials, query parameters, or a fragment"
)
return urlunsplit(
(
parsed.scheme.lower(),
parsed.netloc.lower(),
parsed.path.rstrip("/"),
"",
"",
)
)


def normalize_model(value: str | None) -> str:
model = str(value or "").strip()
if not model:
raise ValueError("embedding model must not be empty")
return model
29 changes: 29 additions & 0 deletions pageindex/filesystem/_projection_topology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

from pathlib import Path


def projection_database_paths(index_dir: str | Path) -> tuple[Path, Path]:
index_dir = Path(index_dir).expanduser()
return index_dir / "summary.sqlite", index_dir / "embedding_cache.sqlite"


def projection_database_path_present(path: str | Path) -> bool:
path = Path(path)
return path.exists() or path.is_symlink()


def projection_database_pair(
index_dir: str | Path,
) -> tuple[Path, Path] | None:
summary_path, cache_path = projection_database_paths(index_dir)
summary_exists = projection_database_path_present(summary_path)
cache_exists = projection_database_path_present(cache_path)
if not summary_exists and not cache_exists:
return None
if not summary_exists or not cache_exists:
raise RuntimeError(
"PIFS Summary Projection topology is incomplete; migrate this workspace with "
"pifs-data/scripts/migrate_pifs_workspace.py before opening it."
)
return summary_path, cache_path
75 changes: 75 additions & 0 deletions pageindex/filesystem/_sqlite_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import sqlite3
from typing import Any


def regular_table_names(connection: sqlite3.Connection) -> set[str]:
return {
str(row[0])
for row in connection.execute(
"SELECT name FROM sqlite_master "
"WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
)
}


def sqlite_schema_signature(
connection: sqlite3.Connection,
tables: set[str],
) -> dict[str, Any]:
columns = {
table: tuple(
tuple(row)
for row in connection.execute(f'PRAGMA table_xinfo("{table}")')
)
for table in sorted(tables)
}
indexes: dict[str, tuple[tuple[Any, ...], ...]] = {}
foreign_keys: dict[str, tuple[tuple[Any, ...], ...]] = {}
for table in sorted(tables):
table_indexes = []
for row in connection.execute(f'PRAGMA index_list("{table}")'):
name = str(row[1])
origin = str(row[3])
index_columns = tuple(
str(column[2])
for column in connection.execute(f'PRAGMA index_info("{name}")')
)
table_indexes.append(
(
name if origin == "c" else None,
int(row[2]),
origin,
int(row[4]),
index_columns,
)
)
indexes[table] = tuple(sorted(table_indexes, key=repr))
foreign_keys[table] = tuple(
sorted(
(
str(row[2]),
str(row[3]),
str(row[4]),
str(row[5]),
str(row[6]),
str(row[7]),
)
for row in connection.execute(f'PRAGMA foreign_key_list("{table}")')
)
)
return {
"tables": tables,
"columns": columns,
"indexes": indexes,
"foreign_keys": foreign_keys,
}


def normalized_table_sql(connection: sqlite3.Connection, table: str) -> str:
row = connection.execute(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
(table,),
).fetchone()
return " ".join(str(row[0] if row else "").lower().split())
102 changes: 102 additions & 0 deletions pageindex/filesystem/_workspace_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from __future__ import annotations

import sqlite3
from pathlib import Path


def _readonly_connection(path: Path) -> sqlite3.Connection:
connection = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True)
connection.row_factory = sqlite3.Row
return connection


def _inconsistent_workspace_error(detail: str) -> RuntimeError:
return RuntimeError(
f"Inconsistent PIFS workspace: {detail}; migrate this workspace with "
"pifs-data/scripts/migrate_pifs_workspace.py before opening it."
)


def validate_catalog_root(catalog_path: str | Path) -> None:
try:
with _readonly_connection(Path(catalog_path)) as catalog:
root = catalog.execute(
"SELECT folder_id, parent_id, name FROM folders WHERE path = '/'"
).fetchone()
except sqlite3.Error as exc:
raise _inconsistent_workspace_error(
"the catalog canonical root folder could not be read"
) from exc
if (
root is None
or root["folder_id"] != "folder_root"
or root["parent_id"] is not None
or root["name"] != "/"
):
raise _inconsistent_workspace_error(
"the catalog is missing its canonical root folder"
)


def validate_workspace_consistency(
catalog_path: str | Path,
summary_path: str | Path,
) -> None:
catalog_path = Path(catalog_path)
validate_catalog_root(catalog_path)
try:
with _readonly_connection(catalog_path) as catalog:
active_file_refs = {
str(row[0])
for row in catalog.execute(
"SELECT file_ref FROM files WHERE deleted_at IS NULL"
)
}
with _readonly_connection(Path(summary_path)) as summary:
projected_file_refs = {
str(row[0])
for row in summary.execute(
"SELECT file_ref FROM semantic_index_docs"
)
}
except sqlite3.Error as exc:
raise _inconsistent_workspace_error(
"catalog and Summary Projection references could not be read"
) from exc
orphaned = sorted(projected_file_refs - active_file_refs)
if orphaned:
shown = ", ".join(orphaned[:5])
raise _inconsistent_workspace_error(
"Summary Projection file_ref values do not reference active catalog files: "
f"{shown}"
)
missing = sorted(active_file_refs - projected_file_refs)
if missing:
shown = ", ".join(missing[:5])
raise _inconsistent_workspace_error(
"active catalog file_ref values are missing from the Summary Projection: "
f"{shown}"
)


def validate_catalog_without_projection(catalog_path: str | Path) -> None:
catalog_path = Path(catalog_path)
validate_catalog_root(catalog_path)
try:
with _readonly_connection(catalog_path) as catalog:
active_file_refs = sorted(
str(row[0])
for row in catalog.execute(
"SELECT file_ref FROM files WHERE deleted_at IS NULL"
)
)
except sqlite3.Error as exc:
raise _inconsistent_workspace_error(
"active catalog references could not be read"
) from exc
if active_file_refs:
shown = ", ".join(active_file_refs[:5])
raise _inconsistent_workspace_error(
"active catalog files require a complete Summary Projection: "
f"{shown}"
)
Loading
Loading