ci: keep internal documents out of tracked files

This commit is contained in:
Haozhe Wu committed 2026-10-15 21:14:09 +08:00
1 parent b116a5e094
commit 4da7579065
7 files changed
+199 -273

No files matched your search

+2 -2
View File
@@ -116,8 +116,8 @@ jobs:
- name: Repository safety gates - name: Repository safety gates
run: bash tools/ci_grep_gates.sh run: bash tools/ci_grep_gates.sh
- name: Test environment variable gate - name: Test repository safety gates
run: pytest tools/test_ci_env_var_gate.py -q run: pytest tools/test_ci_env_var_gate.py tools/test_ci_private_files_gate.py -q
- name: Syntax check - name: Syntax check
run: | run: |
+21
View File
@@ -55,6 +55,27 @@ For AI-assisted or automation-assisted contributions, also see
local checks, higher-risk broker/MCP/credential surfaces, and the expected PR local checks, higher-risk broker/MCP/credential surfaces, and the expected PR
risk notes for agent-authored changes. risk notes for agent-authored changes.
## Keep internal documents local
The repository-root `docs/` directory holds local working notes and must not
be committed. Local agent instructions and memory files (`AGENTS.md`,
`AGENTS.override.md`, `CLAUDE.md`, `CLAUDE.local.md`, `MEMORY.md`,
`CODEX_MEMORY.md`, and `NOTES.md`) must also remain untracked at any depth.
Publish project documentation in `wiki/docs/` or the relevant source directory.
Before committing, run:
```bash
python tools/ci_private_files_gate.py
```
This check also runs through `bash tools/ci_grep_gates.sh` in CI on pushes and
pull requests. It reads the Git index, so force-adding an ignored document is
still rejected; ignored local files and public documentation are allowed.
It prints filenames only, never document contents. If a file was accidentally
staged, use `git rm --cached -- <path>` to stop tracking it while preserving the
local copy. This does not erase copies in previous commits.
## Developer Certificate of Origin (DCO) ## Developer Certificate of Origin (DCO)
Every commit in a community pull request MUST carry a `Signed-off-by:` Every commit in a community pull request MUST carry a `Signed-off-by:`
@@ -1,85 +0,0 @@
# Specialized analysis reliability and interaction
User scope: implement all proposed options/factor/correlation improvements and investigate repeated missing-data feedback. Baseline: local commit `3bb01ebe`; no remote publishing requested.
## Confirmed before changes
- Options: invalid edits leave old metrics visible; request generation changes only at dispatch, leaving a debounce race. Charts dispose/init on every data change. At 1280 px the leg editor horizontally clips columns.
- Correlation: matrix and regime fetch separately; Promise.all discards the successful result if either fails. Input edits clear results; no restored page state or observed sample coverage.
- Factors: full-zoo default, unworkable single-asset BTC universe, progress begins after data loading, job id is lost on navigation even while the backend continues. Job stores are process-local.
- Shared market data: empty DataFrames were counted as successful partial results and removed from the fallback queue. US class-share symbols did not match source routing; concatenated crypto could route to China loaders.
## Implemented
- Shared market data treats empty/None frames, including empty resampled frames,
as missing and continues fallback only for unresolved symbols. US class-share
routing and USDT alias normalization preserve requested identities; substituted
providers are attempted once and diagnostics name the actual provider.
Sanitized attempts and recovery guidance distinguish unavailable sources,
failed requests and no bars. Explicit-only currency/fallback boundaries remain.
- S&P 500 factor evaluation uses that shared fallback with observed DataFrames.
Selected fundamental factors load only their declared SEC fields at filing
dates, preserve missing values and reject unsupported universes before price
downloads. Both agent/MCP and shared CLI/API paths use this preparation.
- Factor UI evaluates one selected factor or compares a few, checks configured
readiness, reports loading/computing phases and observed coverage, and warns
when no factor was evaluable. BTC's single-asset pool is unavailable for these
cross-sectional evaluations. Request-ID replay avoids duplicate accepted jobs;
snapshots/SSE and tab-local state recover progress after navigation or reload.
- Correlation matrix and regime use one price snapshot, respect source priority,
normalize duplicate identities, show actual overlapping observations/dates,
and keep successful output when the other analysis lacks sufficient data.
- Options and correlation cancel superseded requests and keep previous results
explicitly labeled. Options charts use the captured result parameters, chart
instances are reused, advanced inputs are collapsed, and the contract chain
loads on expansion with 15 contracts per side around the actual quote.
- Drafts/results persist in the browser tab. All new UI strings cover nine locales.
## Verified
- Frontend: 76 test files / 725 tests pass, production build passes, including
locale parity. Log: `/tmp/vibe-trading-uiux-20261008/analysis-frontend-full.log`.
- Related backend suite: 335 tests pass. After the final empty-resampling fix,
the 91-test market-data/reliability subset passes (overlapping, not additive).
Logs: `analysis-backend-final.log` and `final-fallback-tests.log` in the same
temporary directory. Changed Python files pass Ruff; `git diff --check` passes.
- Actual MarketDataTool requests returned bars for AAPL.US, BRK.B.US,
600519.SH, 0700.HK and BTCUSDT through Yahoo, Tencent and OKX. The combined
correlation path returned AAPL/SPY/BRK.B with 30 common observations for
2026-08-26 through 2026-10-07 and a regime result. These are live provider
probes, separate from mocked regressions. A final request to the restarted
latest API returned HTTP 200, 30 common observations from 2026-08-27 through
2026-10-08, a regime result and no errors.
- Actual single-factor alpha101_001 evaluation for sp500 / 2025-2025 completed:
503 requested instruments, 500 observed, one evaluated factor. Missing:
FDXF.US, HONA.US, VYLR.US. The UI listed these and current-roster survivorship
bias. Navigation/reload recovered the result. After restarting the isolated
backend with the latest source, the UI still read the saved result; a new
job `4a84ae13fdf948ffaebb8650e309bec3` reused the panel and completed with the
same coverage and factor result. Initial progress was 0/1, terminal progress 1/1.
- Actual PIT SEC panel for AAPL.US/MSFT.US returned net_income and
shares_diluted. This verifies real field retrieval, not a full 503-company
fundamental benchmark. Health requests stayed responsive during the large
price fetch (0.5–13.1 ms in five observations).
- Local browser QA: late payoff response/edit races, result restoration,
correlation partial results, chain 15/15 versus full 60/56 rows, desktop leg
editor at 1280 px, and no whole-page overflow at 390 px. Stored Chinese/dark
preferences were preserved. Screenshot: `factor-final.png` in the temporary
directory. Preview: http://127.0.0.1:5899/.
## Remaining operational limits
- First large-universe download still takes minutes because real public-source
calls are rate-limited. Scope selection and reuse avoid unnecessary work;
source throttling and scientific sample gates were not reduced.
- Historical absence, credentials, optional dependencies and provider outages
can still prevent data retrieval. Diagnostics expose these instead of
fabricating values. Configured readiness does not promise live availability.
- Job stores remain process-local; restart loses unfinished jobs. Completed
snapshots are pruned after one hour, while completed tab-local results remain
readable. New submissions after lost process state can start fresh jobs.
- Current S&P 500 constituents carry survivorship bias. The pre-existing static
constituent-source date was not independently refreshed in this task.
- No protected agent/session/provider code, live broker action or remote
publishing was performed. Runtime caches and live probe outputs stayed outside
the checkout under `/tmp/vibe-trading-uiux-20261008/`.
@@ -1,183 +0,0 @@
# Options strategies worth automating: candidate backtests
Goal: backtest a small set of options strategies with the existing engine
(`agent/backtest/engines/options_portfolio.py`, unchanged) and decide which, if
any, to carry forward to paper trading.
Code: `agent/src/skills/options-strategy/example_signal_engine.py` (engines),
`agent/scripts/options_candidates_backtest.py` (this study; reruns end to end
with `PYTHONPATH=agent python agent/scripts/options_candidates_backtest.py`),
`agent/tests/test_options_strategy_engines.py`.
## Bottom line
- **No premium-selling strategy qualifies.** Cash-secured puts, iron condors
and short strangles made between -4% and +4% a year out of sample, against
15–19% for buy-and-hold. The model has no volatility risk premium: options
are priced at trailing realized vol, and that premium is where these
strategies' edge comes from in practice. **This backtest cannot validate them
either way.** They need real option-chain history first.
- **Carry forward one: a 0.20-delta, ~35-DTE covered call on SPY (QQQ as a
second underlying).** It was the only strategy that matched or beat
buy-and-hold on risk-adjusted terms in both periods for the index ETFs:
- SPY out of sample: Sharpe 1.10 vs 0.94, max drawdown -19% vs -25%, CAGR
about the same.
- QQQ: Sharpe about equal, drawdown -29% vs -35%, about 2 points a year less
CAGR.
Its result doesn't depend on the missing premium: real calls are richer than
modeled here, which would help it. It lagged buy-and-hold on AAPL and MSFT
(CAGR 4–5 points a year lower), so it is for index ETFs only.
- **Don't carry the protective put forward, even though it looks good.** Puts
are priced at trailing realized vol plus a mild skew, which makes them
several vol points cheaper than real index puts. Its strong in-sample
result (Sharpe 1.2 against 0.86 for SPY) mostly comes from the cheap
pre-COVID puts in February 2020, priced at about 12% vol. In reality those
puts cost far more.
- The vol gate helped the strangle in every run: an "elevated" regime (30-day
realized vol above its 1-year median) beat no gate in sample. Out of sample
it kept drawdowns to 8–19% but still earned ~0–1% a year. The gate's benefit
here comes from realized vol reverting to its mean. Real implied vol already
prices that in, so don't expect the benefit to carry over.
## Setup
| Item | Value |
|---|---|
| Underlyings | SPY, QQQ (index ETFs); AAPL, MSFT (single names). yfinance closes, dividend- and split-adjusted |
| In-sample | 2016-01-04 → 2020-12-31 (includes 2020 crash), history from 2015 used for vol warm-up |
| Out-of-sample | 2021-01-04 → 2026-10-08 (includes 2022 bear market), parameters frozen |
| Tuning | one parameter per strategy, best mean Sharpe over SPY+QQQ in sample (grid in the script) |
| Pricing | Black-Scholes on 30-day historical vol, `iv_skew=-0.3` smile, r = 2%, European, next-bar-close fills |
| Costs | commission = 1% of premium per side for ETFs, 2% for single names (≈ half-spread + fees) |
| Margin | engine's CBOE-style naked margin for cash-secured puts and strangles; off for covered call, protective put and condor (see caveats) |
| Capital | $100k, size compounds on each strategy's own realized P&L |
Strategies (chosen parameters in bold):
| Strategy | Rules |
|---|---|
| Covered call | shares + short call **0.20Δ** (vs 0.30), ~35 DTE, hold to expiry, shares re-sized to equity each cycle |
| Cash-secured put | short put **0.30Δ** (vs 0.20), ~35 DTE, hold to expiry, notional = capital (fully secured) |
| Iron condor | short **0.16Δ** (vs 0.10/0.25) put+call, 0.05Δ wings, 45 DTE, close at 50% of credit or 21 DTE; max loss 10% of capital per trade |
| Vol-gated strangle | short 0.16Δ put+call, 45 DTE, 50% / 21 DTE; gate **elevated** (vs none / contracting); notional = capital |
| Protective put | shares + long put **0.15Δ** (vs 0.30), ~60 DTE, rolled at 21 DTE |
## Out-of-sample results (2021-01 → 2026-10)
CAGR+bill is CAGR after crediting the 13-week T-bill rate on cash (the engine
pays 0% on cash). It matters for cash-heavy short-premium books.
| Strategy | Code | CAGR | CAGR+bill | Sharpe | MaxDD | Worst month | Worst day | CVaR 5% (daily) | Skew | Win rate | Trades |
|---|---|---|---|---|---|---|---|---|---|---|---|
| buy & hold | SPY | 15.3% | | 0.94 | -24.5% | -9.2% | -5.9% | -2.4% | 0.28 | | |
| covered call | SPY | 15.5% | 15.5% | **1.10** | -19.4% | -8.3% | -5.6% | -2.1% | -0.00 | 81% | 59 |
| cash-secured put | SPY | 2.9% | 5.8% | 0.42 | -13.9% | -7.3% | -4.8% | -1.3% | -0.16 | 85% | 59 |
| iron condor | SPY | -1.4% | 1.6% | -0.18 | -14.8% | -4.2% | -2.8% | -1.2% | -0.51 | 64% | 90 |
| vol-gated strangle | SPY | 0.0% | 3.0% | 0.02 | -8.4% | -3.2% | -3.1% | -0.7% | -1.61 | 72% | 53 |
| protective put | SPY | 15.1% | 15.1% | 1.07 | -20.5% | -8.2% | -3.8% | -1.9% | 0.75 | 14% | 49 |
| buy & hold | QQQ | 17.2% | | 0.82 | -35.1% | -13.6% | -6.2% | -3.2% | 0.15 | | |
| covered call | QQQ | 15.4% | 15.4% | 0.86 | -29.2% | -12.5% | -5.9% | -2.8% | -0.06 | 73% | 59 |
| cash-secured put | QQQ | 3.7% | 6.4% | 0.42 | -21.4% | -7.2% | -4.9% | -1.7% | -0.58 | 88% | 59 |
| iron condor | QQQ | -3.1% | 0.1% | -0.46 | -22.9% | -5.1% | -2.7% | -1.1% | -1.42 | 62% | 90 |
| vol-gated strangle | QQQ | 1.3% | 4.3% | 0.29 | -10.5% | -6.1% | -2.8% | -0.9% | -2.37 | 77% | 52 |
| protective put | QQQ | 16.7% | 16.7% | 0.92 | -31.0% | -12.6% | -4.1% | -2.5% | 0.48 | 18% | 49 |
| buy & hold | AAPL | 18.9% | | 0.77 | -33.4% | -12.2% | -9.2% | -3.9% | 0.34 | | |
| covered call | AAPL | 13.7% | 13.8% | 0.67 | -32.0% | -11.4% | -8.7% | -3.5% | 0.27 | 71% | 59 |
| cash-secured put | AAPL | 3.4% | 6.3% | 0.32 | -23.9% | -6.0% | -6.1% | -2.3% | 0.62 | 78% | 59 |
| iron condor | AAPL | -4.3% | -0.8% | -0.59 | -28.7% | -8.7% | -4.3% | -1.3% | -1.45 | 66% | 90 |
| vol-gated strangle | AAPL | -0.4% | 2.8% | -0.01 | -15.1% | -9.1% | -4.2% | -1.4% | -1.25 | 75% | 52 |
| protective put | AAPL | 17.2% | 17.2% | 0.79 | -29.0% | -12.9% | -6.2% | -3.1% | 0.67 | 16% | 49 |
| buy & hold | MSFT | 17.4% | | 0.73 | -37.1% | -17.2% | -10.0% | -3.7% | 0.54 | | |
| covered call | MSFT | 12.7% | 12.9% | 0.67 | -28.7% | -13.5% | -9.2% | -3.2% | -0.20 | 78% | 59 |
| cash-secured put | MSFT | 4.1% | 7.1% | 0.42 | -18.8% | -7.9% | -6.1% | -2.0% | -0.88 | 86% | 59 |
| iron condor | MSFT | -1.5% | 1.7% | -0.19 | -13.6% | -4.1% | -3.9% | -1.2% | -1.71 | 67% | 91 |
| vol-gated strangle | MSFT | 0.7% | 4.0% | 0.13 | -18.5% | -7.0% | -6.6% | -1.2% | -3.53 | 72% | 47 |
| protective put | MSFT | 13.7% | 13.7% | 0.65 | -33.1% | -14.8% | -10.0% | -3.1% | 1.03 | 22% | 49 |
Win rate is per structure (all legs of one entry), excluding the share leg.
No opens were rejected for margin in any run.
## In-sample (2016 → 2020), chosen parameters
| Strategy | SPY CAGR / Sharpe / MaxDD | QQQ CAGR / Sharpe / MaxDD |
|---|---|---|
| buy & hold | 15.5% / 0.86 / -33.7% | 24.5% / 1.11 / -28.6% |
| covered call 0.20Δ | 13.7% / 0.85 / -32.7% | 20.2% / 1.07 / -27.6% |
| cash-secured put 0.30Δ | -1.2% / -0.02 / -33.1% | 1.1% / 0.15 / -26.8% |
| iron condor 0.16Δ | -2.8% / -0.32 / -26.9% | -3.1% / -0.38 / -26.5% |
| strangle, elevated gate | 1.0% / 0.18 / -18.2% | 3.1% / 0.48 / -12.3% |
| protective put 0.15Δ | 15.8% / 1.20 / -14.4% | 23.0% / 1.31 / -18.9% |
The full in-sample grid (every parameter, worst month/day, CVaR, skew) is
printed by the script.
## Tail behavior
- Short premium has the expected left tail. Every short-premium book has
negative daily skew, worst at -5.3 for the ungated SPY strangle in sample.
In 2020 the SPY cash-secured put lost 16–17% in its worst month, and the
ungated SPY strangle lost 14%. Win rates of 80–90% sit alongside negative CAGR,
so a high win rate is no evidence of an edge here.
- The covered call keeps almost all of the underlying's crash exposure: its
in-sample max drawdown is -33% against -34% for SPY. It mainly gives up
upside, which is why it lags on strongly trending single names.
- The protective put is the only strategy with positive skew, and it cut
the worst day on SPY, QQQ and AAPL. That benefit is overstated for the reason given in the bottom
line.
## Sensitivity to the smile (SPY, out of sample)
`iv_skew` is the only setting the engine has for how rich OTM options are.
Changing it barely moves the ranking:
| iv_skew | covered call Sharpe | CSP CAGR+bill | condor CAGR+bill | strangle CAGR+bill | protective put Sharpe |
|---|---|---|---|---|---|
| 0.0 | 1.15 | 5.1% | 0.9% | 3.9% | 1.18 |
| -0.3 (base) | 1.10 | 5.8% | 1.6% | 3.0% | 1.07 |
| -0.6 | 1.04 | 6.4% | 1.5% | 2.7% | 0.95 |
## Caveats
1. **Prices come from a model, not from real option chains.** Every price is
Black-Scholes on trailing 30-day realized vol plus a fixed quadratic smile:
- The volatility risk premium is effectively **zero**. Real index implied
vol has averaged a few points above later realized vol. That gap is the
return premium sellers collect and buyers pay. Here, premium selling
looks worse than it is and option buying looks better.
- Real implied vol jumps the day a crash starts. Here it lags by up to 30
days: options are cheap going into a crash and expensive after it.
- The bid/ask spread is modeled as a flat 1–2% of premium. Real spreads
widen sharply in a crisis, the exact days that matter for short-premium
strategies.
2. **There is no early assignment and no share delivery.** Options are
European and settle in cash. The covered call and protective put hold a
"share replica" (a call struck at $0.01) and re-size it to equity each
cycle, which approximates assignment and re-buying. The cash-secured put
settles in cash, so it is not a full wheel (no calls written on assigned
shares). Dividends reach the results only through adjusted closes.
3. **Some margin was switched off.** The engine margins every short leg as
naked. That is right for cash-secured puts and strangles. It overcharges
covered calls and defined-risk condors, and it rejected single condor legs
in a trial run, which left the rest of the condor naked. So those three
were run with `margin_enabled: false`:
- the condor is sized so its maximum loss is 10% of capital, which matches
the Reg-T spread requirement;
- the share-based strategies hold about zero cash.
4. **Cash earns 0% in the engine.** The CAGR+bill column adds T-bill interest
after the fact, as simple interest on the engine's cash balance.
5. **This is a small study.** It covers four underlyings, about 11 years, and
~50–90 trades per strategy per period. The parameter grid is deliberately
small (2–3 values), so the in-sample choice gives only a little room for
overfitting. A Sharpe ratio measured over ~5 years has a standard error of about
0.45, so none of the Sharpe gaps above are statistically significant.
## Next steps
- Paper-trade the SPY 0.20Δ / 35-DTE covered call (QQQ optional). Record real
fills against the model's mid price to measure the actual spread cost.
- Before paper-trading any short-premium strategy, replace the synthetic
pricing with historical chain IV, for example by loading an IV series in
place of `historical_volatility`. That is the input that decides whether
the cash-secured put or the vol-gated strangle has an edge. This study
can't answer that.
+12 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ci_grep_gates.sh — repo-wide safety floor enforced in CI. # ci_grep_gates.sh — repo-wide safety floor enforced in CI.
# #
# Four gates run sequentially; any failure exits non-zero and names the # Gates run sequentially; any failure exits non-zero and names the
# offending files. Run locally before pushing: # offending files. Run locally before pushing:
# #
# bash tools/ci_grep_gates.sh # bash tools/ci_grep_gates.sh
@@ -20,6 +20,9 @@
# (e) No raw `os.getenv` / `os.environ.get` / `os.environ["KEY"]` reads # (e) No raw `os.getenv` / `os.environ.get` / `os.environ["KEY"]` reads
# outside the centralized config layer (`agent/src/config/`). # outside the centralized config layer (`agent/src/config/`).
# AST-based; uses `tools/ci_env_var_gate.py`. # AST-based; uses `tools/ci_env_var_gate.py`.
# (f) No process-wide os patches in tests.
# (g) No internal documents in the Git index, even if force-added.
# Uses `tools/ci_private_files_gate.py`; ignored local files are allowed.
# #
# Exclusions: .git, node_modules, __pycache__, .venv, dist, build, this # Exclusions: .git, node_modules, __pycache__, .venv, dist, build, this
# script itself. The HTML scan in (c) is scoped to wiki/alpha-library/** # script itself. The HTML scan in (c) is scoped to wiki/alpha-library/**
@@ -58,8 +61,8 @@ fi
# -------------------------------------------------------------- gate (b) # -------------------------------------------------------------- gate (b)
# docs/ is excluded: those are internal planning docs that discuss the # docs/ is excluded: those are internal planning docs that discuss the
# trademark policy itself (e.g. "the string 'WorldQuant' must not appear in # trademark policy itself (e.g. "the string 'WorldQuant' must not appear in
# user-facing artifacts"). docs/ is not shipped to PyPI / public consumers # user-facing artifacts"). Gate (g) prevents these local documents from
# (memory: feedback_no_push_docs). The gate's real target is source code, # being tracked. This gate's target is source code,
# READMEs, HTML/JSON manifests, and the wiki. # READMEs, HTML/JSON manifests, and the wiki.
echo "[gate b] no 'WorldQuant' trademark string in shipped artifacts ..." echo "[gate b] no 'WorldQuant' trademark string in shipped artifacts ..."
B_HITS=$(grep -rni --include='*.py' --include='*.md' --include='*.html' --include='*.json' \ B_HITS=$(grep -rni --include='*.py' --include='*.md' --include='*.html' --include='*.json' \
@@ -162,6 +165,12 @@ else
echo "${GREEN}ok${NC}" echo "${GREEN}ok${NC}"
fi fi
# -------------------------------------------------------------- gate (g)
echo "[gate g] no tracked internal documents ..."
if ! python tools/ci_private_files_gate.py; then
FAILED=1
fi
# --------------------------------------------------------------- result # --------------------------------------------------------------- result
if [ "$FAILED" -ne 0 ]; then if [ "$FAILED" -ne 0 ]; then
echo echo
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Reject internal documents in the Git index, including force-added files.
Run before committing with ``python tools/ci_private_files_gate.py``.
Only indexed paths are inspected; ignored local files and their contents are
never read. The same index check runs on CI's checked-out commit.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path, PurePosixPath
PRIVATE_DOCUMENT_NAMES = frozenset({
"agents.md",
"agents.override.md",
"claude.md",
"claude.local.md",
"memory.md",
"codex_memory.md",
"notes.md",
})
def main(repo_root: Path | None = None) -> int:
"""Check the Git index for internal document paths.
Args:
repo_root: Repository to check; defaults to this script's repository.
Returns:
Zero for a clean index, one for forbidden paths, or two if Git fails.
"""
root = repo_root if repo_root is not None else Path(__file__).resolve().parent.parent
try:
result = subprocess.run(
["git", "ls-files", "--cached", "--full-name", "-z"],
cwd=root,
capture_output=True,
check=True,
)
except (OSError, subprocess.CalledProcessError):
print("FAIL: cannot read the Git index; internal-document check did not run.")
return 2
forbidden = []
for raw_path in result.stdout.split(b"\0"):
if not raw_path:
continue
name = os.fsdecode(raw_path)
path = PurePosixPath(name)
if path.parts[0].casefold() == "docs" or path.name.casefold() in PRIVATE_DOCUMENT_NAMES:
forbidden.append(name)
if forbidden:
print("FAIL: internal documents are tracked in Git:")
for name in sorted(set(forbidden)):
# Escape control characters so a filename cannot inject CI output.
print(f" {ascii(name)}")
print("Use git rm --cached -- <path> to retain the local file without tracking it.")
print("Publish project documentation under wiki/docs/ or the relevant source directory.")
return 1
print("OK: no internal documents are tracked in Git.")
return 0
if __name__ == "__main__":
sys.exit(main())
+92
View File
@@ -0,0 +1,92 @@
"""Exercise the privacy gate against real temporary Git indexes."""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from ci_private_files_gate import main
def _git(root: Path, *args: str) -> None:
"""Run Git in an isolated test repository."""
subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True)
@pytest.fixture
def repo(tmp_path: Path) -> Path:
"""Create a real repository with an ignored internal-documents directory."""
_git(tmp_path, "init", "-q")
(tmp_path / ".gitignore").write_text("/docs/\nAGENTS.md\nCLAUDE.md\n", encoding="utf-8")
_git(tmp_path, "add", "--", ".gitignore")
return tmp_path
@pytest.mark.parametrize("name", [
"docs/sessions/private.md",
"docs/plans/design.json",
"docs",
"DOCS/private.md",
"AGENTS.md",
"agent/AGENTS.override.md",
"CLAUDE.md",
"frontend/CLAUDE.local.md",
"MEMORY.md",
"nested/CODEX_MEMORY.md",
"NOTES.md",
"docs/notes with spaces\nand newline.md",
])
def test_force_added_internal_documents_fail(repo: Path, name: str, capsys) -> None:
path = repo / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("private content must never appear in output", encoding="utf-8")
_git(repo, "add", "-f", "--", name)
assert main(repo) == 1
output = capsys.readouterr().out
assert ascii(name) in output
assert "private content" not in output
def test_public_docs_and_ignored_local_notes_pass(repo: Path) -> None:
for name in ["wiki/docs/guide.md", "agent/src/skills/example/SKILL.md", "README.md"]:
path = repo / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("public documentation", encoding="utf-8")
_git(repo, "add", "--", name)
(repo / "docs").mkdir()
(repo / "docs/private.md").write_text("local notes", encoding="utf-8")
(repo / "CLAUDE.md").write_text("local instructions", encoding="utf-8")
assert main(repo) == 0
def test_unstaged_deletion_does_not_hide_an_indexed_file(repo: Path) -> None:
path = repo / "docs/private.md"
path.parent.mkdir()
path.write_text("private", encoding="utf-8")
_git(repo, "add", "-f", "--", "docs/private.md")
path.unlink()
assert main(repo) == 1
def test_untracking_keeps_local_content_and_clears_failure(repo: Path) -> None:
path = repo / "docs/private.md"
path.parent.mkdir()
original = b"local notes\r\n\x00unchanged\n"
path.write_bytes(original)
_git(repo, "add", "-f", "--", "docs/private.md")
assert main(repo) == 1
_git(repo, "rm", "--cached", "--", "docs/private.md")
assert path.read_bytes() == original
assert main(repo) == 0
def test_missing_git_repository_fails_closed(tmp_path: Path, capsys) -> None:
assert main(tmp_path) == 2
assert "check did not run" in capsys.readouterr().out