ci: keep internal documents out of tracked files
This commit is contained in:
1 parent
b116a5e094
commit
4da7579065
7 files changed
+199
-273
No files matched your search
@@ -116,8 +116,8 @@ jobs:
|
||||
- name: Repository safety gates
|
||||
run: bash tools/ci_grep_gates.sh
|
||||
|
||||
- name: Test environment variable gate
|
||||
run: pytest tools/test_ci_env_var_gate.py -q
|
||||
- name: Test repository safety gates
|
||||
run: pytest tools/test_ci_env_var_gate.py tools/test_ci_private_files_gate.py -q
|
||||
|
||||
- name: Syntax check
|
||||
run: |
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
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
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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:
|
||||
#
|
||||
# bash tools/ci_grep_gates.sh
|
||||
@@ -20,6 +20,9 @@
|
||||
# (e) No raw `os.getenv` / `os.environ.get` / `os.environ["KEY"]` reads
|
||||
# outside the centralized config layer (`agent/src/config/`).
|
||||
# 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
|
||||
# script itself. The HTML scan in (c) is scoped to wiki/alpha-library/**
|
||||
@@ -58,8 +61,8 @@ fi
|
||||
# -------------------------------------------------------------- gate (b)
|
||||
# docs/ is excluded: those are internal planning docs that discuss the
|
||||
# trademark policy itself (e.g. "the string 'WorldQuant' must not appear in
|
||||
# user-facing artifacts"). docs/ is not shipped to PyPI / public consumers
|
||||
# (memory: feedback_no_push_docs). The gate's real target is source code,
|
||||
# user-facing artifacts"). Gate (g) prevents these local documents from
|
||||
# being tracked. This gate's target is source code,
|
||||
# READMEs, HTML/JSON manifests, and the wiki.
|
||||
echo "[gate b] no 'WorldQuant' trademark string in shipped artifacts ..."
|
||||
B_HITS=$(grep -rni --include='*.py' --include='*.md' --include='*.html' --include='*.json' \
|
||||
@@ -162,6 +165,12 @@ else
|
||||
echo "${GREEN}ok${NC}"
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------- gate (g)
|
||||
echo "[gate g] no tracked internal documents ..."
|
||||
if ! python tools/ci_private_files_gate.py; then
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------- result
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
Reference in new issue
Block a user