diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..98cba12 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,14 @@ +* @MichaelsEngineering +.github/** @MichaelsEngineering +plans/** @MichaelsEngineering +scripts/agent-orchestrator/** @MichaelsEngineering +src/** @MichaelsEngineering +tests/** @MichaelsEngineering +traces/** @MichaelsEngineering +runs/** @MichaelsEngineering +dev/** @MichaelsEngineering +AGENTS.md @MichaelsEngineering +README.md @MichaelsEngineering +Makefile @MichaelsEngineering +pyproject.toml @MichaelsEngineering +LICENSE @MichaelsEngineering diff --git a/.github/ISSUE_TEMPLATE/bug_feature_report.md b/.github/ISSUE_TEMPLATE/bug_feature_report.md new file mode 100644 index 0000000..76d9b35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_feature_report.md @@ -0,0 +1,106 @@ +--- +name: "🐞/✨ Bug Report / Feature Request" +about: Report a bug or suggest a new feature +title: " " +labels: ["bug", "enhancement"] +assignees: "" +--- + +## Header + +- **Type**: Bug / Feature / Improvement +- **Title**: _(Short summary)_ + +--- + +## Description +Provide a clear and concise description of the issue or requested capability. + +--- + +## Background & Context +Where this occurs / why it matters / user type / business impact. + +--- + +## For Bugs: Reproduction Steps +(If Type = Bug) +1. Step 1: _(what you did)_ +2. Step 2: _(what you did next)_ +3. … +**Expected behavior**: +_(What you expected to happen)_ +**Actual behavior**: +_(What actually happened)_ +**Environment / version / configuration**: +- Product version: +- Platform (OS / browser / device): +- Any special setup: +**Attachments / logs / screenshots**: +_(Links or embed if supported)_ + +--- + +## For Features: Proposal +(If Type = Feature or Improvement) +- **What is the request?** +_(Describe the new capability or change)_ +- **Why is it needed?** +_(User pain, business value, user type)_ +- **User scenario / Use-case**: +_(β€œWhen user X does Y, they want Z”)_ +- **Acceptance criteria / Success metrics**: +_(How will you know if it’s done / valuable?)_ +- **Alternatives considered**: +_(If you know other options or workarounds)_ +- **Priority / Urgency**: +_(Low / Medium / High)_ + +--- + +## Impact & Scope +- **Affected users / segments**: +- **Frequency or severity** (bugs) / **Reach & benefit** (features): +- **Dependencies or related issues**: +- **Estimated effort / complexity** (optional, dev can fill): + +--- + +## Notes for the Dev / Product Team (Optional) +- **Suggested implementation approach** (optional): +- **Workarounds currently in use**: +- **Additional comments**: + +--- + +## Prompt-Engineering Note +When converting to a Codex-style prompt for generation or summarization, you might use: + +analyze current prompt_guide.txt: +Task: + +Context: +- Files: +- Logs / stack trace: +- Constraints: + +Verify: +- Run: +- Expect: + +Output: +- + +Task: + +Context: +- Files: +- Logs / stack trace: +- Constraints: + +Verify: +- Run: +- Expect: + +Output: +- diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..14af403 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,51 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "03:00" + timezone: "UTC" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "security" + assignees: + - "MichaelsEngineering" + reviewers: + - "MichaelsEngineering" + groups: + python-security: + applies-to: security-updates + patterns: + - "*" + python-routine: + applies-to: version-updates + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "03:15" + timezone: "UTC" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "security" + assignees: + - "MichaelsEngineering" + reviewers: + - "MichaelsEngineering" + groups: + actions-security: + applies-to: security-updates + patterns: + - "*" + actions-routine: + applies-to: version-updates + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7711662 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ +### What + +- + +### Why + +- + +### How + +- + +### Checks + +- [x] `make check` passes locally +- [ ] Added/updated tests +- [ ] Docs or README updated if needed +- [ ] Security impact assessed (`none` or brief rationale provided below) +- [ ] Dependency and lockfile changes explicitly listed +- [ ] Supply-chain provenance updated (SBOM/attestation artifacts linked when applicable) +- [ ] Threat-model delta captured for behavior/config changes +- [ ] Rollback plan included for this change +- [ ] Confirmed no new network/time/nondeterministic sources in core loop paths (`src/runner.py`, `src/replay.py`, `src/replay_fixtures.py`) + +### Security Notes + +- Security impact: +- Dependency or lockfile changes: +- Threat-model delta: +- Rollback plan: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3b2ed72 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: ci + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + check: + name: check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Python 3.11 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Sync dependencies + run: uv sync --dev + - name: Run check + run: uv run make check + + gate: + name: gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Python 3.11 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Sync dependencies + run: uv sync --dev + - name: Run deterministic gate + run: uv run make gate + + smoke: + name: smoke + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Python 3.11 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Sync dependencies + run: uv sync --dev + - name: Run smoke test + run: uv run make smoke diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d4656e2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,125 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + paths: + - ".github/workflows/**" + - ".gitignore" + - "Makefile" + - "dev/**" + - "scripts/**" + - "src/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + pull_request: + branches: [ "main" ] + paths: + - ".github/workflows/**" + - ".gitignore" + - "Makefile" + - "dev/**" + - "scripts/**" + - "src/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + schedule: + - cron: '24 19 * * 2' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@a60c4df7a135c7317c1e9ddf9b5a9b07a910dda9 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@a60c4df7a135c7317c1e9ddf9b5a9b07a910dda9 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..9223fdd --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,108 @@ +name: security + +on: + pull_request: + paths: + - ".github/workflows/**" + - ".gitignore" + - "Makefile" + - "dev/**" + - "scripts/**" + - "src/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + push: + branches: + - main + paths: + - ".github/workflows/**" + - ".gitignore" + - "Makefile" + - "dev/**" + - "scripts/**" + - "src/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + schedule: + - cron: "21 3 * * 1" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dependency-audit: + name: dependency-audit + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Python 3.11 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Sync dependencies + run: uv sync --dev + - name: Run dependency audit + run: uv run make security-audit + + sbom: + name: sbom + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Python 3.11 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Sync dependencies + run: uv sync --dev + - name: Generate SBOM + run: uv run make sbom + - name: Upload SBOM artifact + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 + with: + name: cyclonedx-sbom + path: runs/security/sbom.cdx.json + if-no-files-found: error + + attest: + name: attest + runs-on: ubuntu-latest + needs: + - sbom + permissions: + contents: read + id-token: write + attestations: write + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Python 3.11 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: "3.11" + - name: Install uv + run: python -m pip install --upgrade pip uv + - name: Sync dependencies + run: uv sync --dev + - name: Generate SBOM for attestation subject + run: uv run make sbom + - name: Attest SBOM provenance + uses: actions/attest-build-provenance@92c65d2898f1f53cfdc910b962cecff86e7f8fcc + with: + subject-path: runs/security/sbom.cdx.json diff --git a/.gitignore b/.gitignore index 99bdff2..425dbf1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,18 @@ htmlcov/ .env.* .python-version +# Local GPD state and research outputs +.gpd/observability/ +.gpd/traces/ +.gpd/state.json +.gpd/config.json +paper_runs/ +runs/ +*.aux +*.log +*.out +*.pdf + # Editor and OS noise .DS_Store Thumbs.db diff --git a/.gpd/CONVENTIONS.md b/.gpd/CONVENTIONS.md new file mode 100644 index 0000000..3714283 --- /dev/null +++ b/.gpd/CONVENTIONS.md @@ -0,0 +1,32 @@ +# Conventions Ledger + +**Project:** Fusion Transport Paper Overnight Draft +**Created:** 2026-03-15 +**Last updated:** 2026-03-15 (Phase 1) + +> This file is append-only for convention entries. When a convention changes, add a new +> entry with the updated value and mark the old entry as superseded. + +--- + +## Units + +### Unit System + +| Field | Value | +| ----- | ----- | +| **Convention** | SI units unless a cited source requires a different convention, in which case the source convention must be stated explicitly in the artifact using it | +| **Introduced** | Phase 1 | +| **Rationale** | Keeps cross-source interpretation explicit during a literature-heavy drafting run | +| **Dependencies** | Parameter tables, quoted scalings, control relevance claims | + +## Statistical Mechanics / Plasma Modeling + +### Reduced-Order Framing + +| Field | Value | +| ----- | ----- | +| **Convention** | Treat reduced-order models as control-oriented surrogates, not first-principles replacements | +| **Introduced** | Phase 1 | +| **Rationale** | Prevents overclaiming during paper drafting | +| **Dependencies** | Thesis selection, limitations section, commercialization claims | diff --git a/.gpd/DECISIONS.md b/.gpd/DECISIONS.md new file mode 100644 index 0000000..34d2867 --- /dev/null +++ b/.gpd/DECISIONS.md @@ -0,0 +1,7 @@ +# Decisions + +## 2026-03-15 + +- Created a minimal GPD project scaffold so the overnight paper run can be planned and executed with stateful tracking. +- Standardized all required outputs under `paper_runs/fusion_transport_paper_001/`. +- Added a dedicated trace summary artifact to capture agent-stage ownership and execution status. diff --git a/.gpd/PROJECT.md b/.gpd/PROJECT.md new file mode 100644 index 0000000..e4222a4 --- /dev/null +++ b/.gpd/PROJECT.md @@ -0,0 +1,153 @@ +# Fusion Transport Paper Overnight Draft + +## What This Is + +This project uses GPD inside Codex to produce a bounded overnight draft paper on reduced-order predictive models for turbulence-driven confinement degradation in tokamaks. The immediate deliverable is a traced, evidence-backed manuscript package that can be reviewed and revised after the unattended run. + +## Core Research Question + +Can a reduced-order turbulence-to-confinement model be framed as a credible control and commercialization wedge for tokamak operations, with enough evidence and caveats to support a draft paper? + +## Scoping Contract Summary + +### Contract Coverage + +- Draft paper package: produce a complete markdown and LaTeX manuscript under `paper_runs/fusion_transport_paper_001/09_final/` +- Evidence-backed positioning: tie the thesis to literature summaries, extracted claims, and explicit disagreement mapping +- False progress to reject: polished prose or figures without source traceability and a written limitations section + +### User Guidance To Preserve + +- **User-stated observables:** turbulence-driven confinement degradation, reduced-order predictive control framing, commercialization relevance +- **User-stated deliverables:** finished paper at the end of a 5-hour run, intermediate artifacts on disk, tracked agent work +- **Must-have references / prior outputs:** literature review outputs created during the run and the run artifact tree under `paper_runs/fusion_transport_paper_001/` +- **Stop / rethink conditions:** if the decisive literature base is too thin, if claim extraction does not support the thesis, or if the final package cannot be assembled before the hard stop + +### Scope Boundaries + +**In scope** + +- Literature-backed drafting workflow for a bounded overnight paper run +- Reduced-order model framing, field disagreements, commercialization wedge, and risks/limitations +- Agent traceability and run artifact organization + +**Out of scope** + +- New numerical plasma simulations +- Experimental validation beyond literature comparison +- Claims of deployable control performance not supported by written evidence + +### Active Anchor Registry + +- `Ref-run-plan`: `PLAN.md` and `run_config.yaml` + - Why it matters: defines the required output contract and runtime limits + - Carry forward: planning, execution, verification, writing + - Required action: read, use, compare +- `Ref-output-tree`: `paper_runs/fusion_transport_paper_001/` + - Why it matters: stable destination for all generated artifacts and trace outputs + - Carry forward: execution, verification, writing + - Required action: use + +### Carry-Forward Inputs + +- `PLAN.md` +- `run_config.yaml` +- `paper_runs/fusion_transport_paper_001/trace.json` + +### Skeptical Review + +- **Weakest anchor:** benchmark literature set is not yet selected and must be established during the run +- **Unvalidated assumptions:** reduced-order framing can support both technical and commercialization claims in one draft +- **Competing explanation:** the topic may only support a scoped review note rather than a full argument-driven paper +- **Disconfirming observation:** extracted literature claims do not support a coherent thesis with explicit limitations +- **False progress to reject:** section files created without citations, evidence map, or disagreement analysis + +### Open Contract Questions + +- Which specific papers will serve as decisive benchmark anchors for the overnight draft? +- Does the overnight run produce an argument-driven paper or only a structured literature synthesis? + +## Research Questions + +### Answered + +(None yet) + +### Active + +- [ ] Which literature anchors best support the confinement degradation thesis? +- [ ] What reduced-order modeling frame is specific enough to defend without overclaiming? +- [ ] Which commercialization claims are supportable from the literature versus speculative? + +### Out of Scope + +- Closed-loop controller implementation and validation β€” requires simulation or experiment not planned for this run + +## Research Context + +### Physical System + +Magnetically confined fusion plasmas in tokamaks, with emphasis on turbulence-driven transport and confinement degradation. + +### Theoretical Framework + +Reduced-order transport and control-oriented modeling grounded in plasma transport literature. + +### Key Parameters and Scales + +| Parameter | Symbol | Regime | Notes | +| --------- | ------ | ------ | ----- | +| Energy confinement time | `tau_E` | Device- and regime-dependent | Primary confinement metric | +| Heat transport level | `chi_eff` | Reduced-order / inferred | Used qualitatively unless benchmarked | +| Control latency budget | `tau_ctrl` | Operational constraint | Relevant to control framing, not yet quantified | + +### Known Results + +- Turbulent transport strongly constrains tokamak confinement performance +- Reduced-order models are commonly used where full-fidelity simulation is too expensive for control workflows + +### What Is New + +This project focuses on turning that body of work into a bounded, evidence-traceable paper draft with explicit disagreement and commercialization framing. + +### Target Venue + +Initial output is a draft manuscript package rather than a committed journal target. + +### Computational Environment + +Local Codex + GPD environment with filesystem-backed artifact output and runtime tracing. + +## Notation and Conventions + +See `.gpd/CONVENTIONS.md` for project conventions. + +## Unit System + +SI units unless a cited source requires another convention. + +## Requirements + +See `.gpd/REQUIREMENTS.md`. + +## Key References + +- `Ref-run-plan` β€” execution contract for the overnight run +- Benchmark literature set TBD during Phase 1 + +## Constraints + +- **Time**: Hard stop at 04:00 America/New_York β€” final assembly must finish within the 5-hour window +- **Evidence**: The draft must remain tied to cited literature artifacts written during the run +- **Workflow**: All intermediate artifacts and trace summaries must be written under `paper_runs/fusion_transport_paper_001/` + +## Key Decisions + +| Decision | Rationale | Outcome | +| -------- | --------- | ------- | +| Use a single overnight run directory | Keeps outputs and traceability stable | Adopted | +| Require an explicit trace artifact | User asked for agent work tracking | Adopted | + +--- + +_Last updated: 2026-03-15 after overnight run unblocker setup_ diff --git a/.gpd/REQUIREMENTS.md b/.gpd/REQUIREMENTS.md new file mode 100644 index 0000000..0eaee85 --- /dev/null +++ b/.gpd/REQUIREMENTS.md @@ -0,0 +1,72 @@ +# Requirements: Fusion Transport Paper Overnight Draft + +**Defined:** 2026-03-15 +**Core Research Question:** Can a reduced-order turbulence-to-confinement model be framed as a credible control and commercialization wedge for tokamak operations, with enough evidence and caveats to support a draft paper? + +## Primary Requirements + +### Analysis + +- [ ] **ANLY-01**: Build a source index covering at least the minimum literature set from `run_config.yaml` +- [ ] **ANLY-02**: Extract at least 30 claims into a table with source traceability +- [ ] **ANLY-03**: Map field disagreements and select a thesis consistent with the evidence + +### Writing + +- [ ] **WRIT-01**: Produce a complete paper outline and all required section drafts under the run directory +- [ ] **WRIT-02**: Produce final markdown and LaTeX paper drafts plus abstract, title options, and executive summary + +### Validation + +- [ ] **VALD-01**: Ensure every major claim in the final draft traces back to the evidence map or disagreement map +- [ ] **VALD-02**: Ensure the final package includes an explicit risks/limitations section +- [ ] **VALD-03**: Ensure trace artifacts record which agent role handled each stage of work + +## Follow-up Requirements + +### Future Technical Validation + +- **FUTR-01**: Compare the paper thesis against simulation or experimental benchmarks +- **FUTR-02**: Quantify controller performance claims with explicit models + +## Out of Scope + +| Topic | Reason | +| ----- | ------ | +| New turbulence simulations | Not feasible within the bounded overnight writing run | +| Operational deployment claims | Requires evidence outside the current repo and run contract | + +## Accuracy and Validation Criteria + +| Requirement | Accuracy Target | Validation Method | +| ----------- | --------------- | ----------------- | +| ANLY-01 | At least 18 sources and 30 extracted claims | Compare artifact counts against `run_config.yaml` | +| WRIT-02 | All required final files exist | Filesystem verification under `paper_runs/fusion_transport_paper_001/09_final/` | +| VALD-03 | Every execution stage has a trace entry | Inspect `paper_runs/fusion_transport_paper_001/trace.json` | + +## Contract Coverage + +| Requirement | Decisive Output / Deliverable | Anchor / Benchmark / Reference | Prior Inputs / Baselines | False Progress To Reject | +| ----------- | ----------------------------- | ------------------------------ | ------------------------ | ------------------------ | +| ANLY-01 | `01_sources/source_index.json` | Run plan plus selected literature anchors | `PLAN.md`, `run_config.yaml` | Uncited section drafting | +| ANLY-03 | `03_disagreements/thesis_selected.md` | Disagreement map and evidence map | Claims table | Thesis chosen before claim extraction | +| WRIT-02 | `09_final/final_paper.md`, `09_final/final_paper.tex` | Final outline, section drafts, revision notes | All upstream run artifacts | Final prose without evidence coverage | +| VALD-03 | `trace.json` | Agent trace summary | Runtime trace events | Missing actor/stage linkage | + +## Traceability + +| Requirement | Phase | Status | +| ----------- | ----- | ------ | +| ANLY-01 | Phase 1: Overnight Paper Run | Pending | +| ANLY-02 | Phase 1: Overnight Paper Run | Pending | +| ANLY-03 | Phase 1: Overnight Paper Run | Pending | +| WRIT-01 | Phase 1: Overnight Paper Run | Pending | +| WRIT-02 | Phase 1: Overnight Paper Run | Pending | +| VALD-01 | Phase 1: Overnight Paper Run | Pending | +| VALD-02 | Phase 1: Overnight Paper Run | Pending | +| VALD-03 | Phase 1: Overnight Paper Run | Pending | + +--- + +_Requirements defined: 2026-03-15_ +_Last updated: 2026-03-15 after overnight run unblocker setup_ diff --git a/.gpd/ROADMAP.md b/.gpd/ROADMAP.md new file mode 100644 index 0000000..f7ad850 --- /dev/null +++ b/.gpd/ROADMAP.md @@ -0,0 +1,51 @@ +# Roadmap: Fusion Transport Paper Overnight Draft + +## Overview + +This roadmap compresses the first milestone into a single overnight paper run that gathers literature, extracts claims, selects a defensible thesis, drafts the manuscript, and assembles a final paper package with traceable agent work. + +## Contract Overview + +| Contract Item | Advanced By Phase(s) | Status | +| ------------- | -------------------- | ------ | +| Source-backed draft paper package | Phase 1 | Planned | +| Agent work traceability | Phase 1 | Planned | +| Explicit limitations and disagreement handling | Phase 1 | Planned | + +## Phases + +- [ ] **Phase 1: Overnight Paper Run** - Gather evidence, draft the manuscript, revise, and assemble final outputs within the 5-hour window + +## Phase Details + +### Phase 1: Overnight Paper Run + +**Goal:** Produce a complete overnight paper draft package with evidence traceability and agent tracking +**Depends on:** Nothing +**Requirements:** [ANLY-01, ANLY-02, ANLY-03, WRIT-01, WRIT-02, VALD-01, VALD-02, VALD-03] +**Contract Coverage:** +- Advances: final paper package, evidence-backed thesis, tracked execution +- Deliverables: literature artifacts, section drafts, figures, revision notes, final paper files, trace summary +- Anchor coverage: `PLAN.md`, `run_config.yaml`, selected literature anchors, run artifact tree +- Forbidden proxies: polished sections without source traceability or explicit limitations +**Success Criteria** (what must be TRUE): + +1. Source, claims, disagreement, and thesis artifacts exist under `paper_runs/fusion_transport_paper_001/` +2. Final markdown and LaTeX paper drafts exist together with abstract and executive summary +3. Trace artifacts show which agent role handled each stage and whether the run met or missed gates + **Plans:** 3 plans + +Plans: + +- [ ] 01-01: Build literature inventory, claim table, and disagreement map +- [ ] 01-02: Draft thesis, model framing, outline, sections, and figures +- [ ] 01-03: Run critique, revise, assemble final package, and finalize trace summary + +## Progress + +**Execution Order:** +Phases execute in numeric order: 1 + +| Phase | Plans Complete | Status | Completed | +| ----- | -------------- | ------ | --------- | +| 1. Overnight Paper Run | 0/3 | Ready to execute | - | diff --git a/.gpd/STATE.md b/.gpd/STATE.md new file mode 100644 index 0000000..f6f5617 --- /dev/null +++ b/.gpd/STATE.md @@ -0,0 +1,83 @@ +# Research State + +## Project Reference + +See: .gpd/PROJECT.md (updated 2026-03-15) + +**Machine-readable scoping contract:** `.gpd/state.json` field `project_contract` + +**Core research question:** Can a reduced-order turbulence-to-confinement model be framed as a credible control and commercialization wedge for tokamak operations, with enough evidence and caveats to support a draft paper? +**Current focus:** Phase 1: Overnight Paper Run + +## Current Position + +**Current Phase:** 01 +**Current Phase Name:** Overnight Paper Run +**Total Phases:** 1 +**Current Plan:** 1 +**Total Plans in Phase:** 3 +**Status:** Ready to execute +**Last Activity:** 2026-03-15 +**Last Activity Description:** Initialized GPD scaffold and overnight run contract + +**Progress:** [β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘] 0% + +## Active Calculations + +- No active calculations yet; execution begins with literature extraction and claim mapping. + +## Intermediate Results + +- No intermediate results recorded yet. + +## Open Questions + +- Which literature anchors will be decisive enough for the final thesis? +- Can the commercialization wedge be argued without overclaiming beyond the literature? + +## Performance Metrics + +| Label | Duration | Tasks | Files | +| ----- | -------- | ----- | ----- | +| setup | 00:00 | 0 | 0 | + +## Accumulated Context + +### Decisions + +Full log: `.gpd/DECISIONS.md` + +**Recent high-impact:** +- [Phase 1]: Standardize all run outputs under `paper_runs/fusion_transport_paper_001/` +- [Phase 1]: Require an explicit trace summary at `paper_runs/fusion_transport_paper_001/trace.json` + +### Active Approximations + +| Approximation | Validity Range | Controlling Parameter | Current Value | Status | +| ------------- | -------------- | --------------------- | ------------- | ------ | +| Literature-grounded review framing | Until contradicted by extracted sources | Source coverage | TBD | Pending | + +**Convention Lock:** + +- Unit system: SI unless overridden by cited source +- Symbol normalization: derive from cited literature during execution + +### Propagated Uncertainties + +| Quantity | Current Value | Uncertainty | Last Updated (Phase) | Method | +| -------- | ------------- | ----------- | -------------------- | ------ | +| Thesis support level | TBD | TBD | Phase 1 | Source synthesis | + +### Pending Todos + +None yet. + +### Blockers/Concerns + +- Decisive benchmark papers are not yet selected. + +## Session Continuity + +**Last session:** 2026-03-15 +**Stopped at:** Project scaffold created; next step is execute Phase 1 +**Resume file:** `.gpd/phases/01-overnight-paper-run/01-01-PLAN.md` diff --git a/.gpd/phases/01-overnight-paper-run/01-01-PLAN.md b/.gpd/phases/01-overnight-paper-run/01-01-PLAN.md new file mode 100644 index 0000000..f6ca029 --- /dev/null +++ b/.gpd/phases/01-overnight-paper-run/01-01-PLAN.md @@ -0,0 +1,133 @@ +--- +phase: 01-overnight-paper-run +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - paper_runs/fusion_transport_paper_001/00_scope/topic_lock.md + - paper_runs/fusion_transport_paper_001/01_sources/source_index.json + - paper_runs/fusion_transport_paper_001/01_sources/paper_summaries.md + - paper_runs/fusion_transport_paper_001/01_sources/citation_bank.bib + - paper_runs/fusion_transport_paper_001/02_claims/claims_table.csv + - paper_runs/fusion_transport_paper_001/02_claims/evidence_map.md + - paper_runs/fusion_transport_paper_001/03_disagreements/disagreement_map.md +interactive: false + +conventions: + units: "SI unless overridden by cited source" + metric: "not applicable" + coordinates: "not applicable" + +dimensional_check: + quantity_name: "artifact completeness" + +approximations: + - name: "literature-first synthesis" + parameter: "source coverage" + validity: "while claim extraction remains tied to cited sources" + breaks_when: "sections are drafted without adequate evidence" + check: "evidence map covers each major thesis component" + +contract: + scope: + question: "Which literature anchors, claims, and disagreements define the overnight paper's defensible thesis space?" + claims: + - id: "claim-literature" + statement: "The overnight run has enough literature structure to support downstream drafting." + deliverables: ["deliv-sources", "deliv-disagreement"] + acceptance_tests: ["test-sources"] + references: ["ref-run-plan"] + deliverables: + - id: "deliv-sources" + kind: "report" + path: "paper_runs/fusion_transport_paper_001/01_sources/paper_summaries.md" + description: "Summaries of the selected literature set" + - id: "deliv-disagreement" + kind: "note" + path: "paper_runs/fusion_transport_paper_001/03_disagreements/disagreement_map.md" + description: "Map of competing claims and open disagreements" + references: + - id: "ref-run-plan" + kind: "spec" + locator: "PLAN.md and run_config.yaml" + role: "definition" + why_it_matters: "Defines minimum source and artifact requirements." + applies_to: ["claim-literature"] + must_surface: true + required_actions: ["read", "use", "compare"] + acceptance_tests: + - id: "test-sources" + subject: "claim-literature" + kind: "existence" + procedure: "Verify that source, claims, and disagreement artifacts exist and are internally consistent." + pass_condition: "Source index, summaries, claims table, evidence map, and disagreement map all exist." + evidence_required: ["deliv-sources", "deliv-disagreement", "ref-run-plan"] + automation: "hybrid" + forbidden_proxies: + - id: "fp-literature" + subject: "claim-literature" + proxy: "Selecting a thesis before completing claim extraction." + reason: "This would front-run the evidence." + links: + - id: "link-literature" + source: "claim-literature" + target: "deliv-sources" + relation: "supports" + verified_by: ["test-sources"] + uncertainty_markers: + weakest_anchors: ["Benchmark papers remain to be selected."] + disconfirming_observations: ["Claim extraction shows no coherent thesis path."] +--- + + +Create the evidence base for the overnight paper run. + +Purpose: Build the source inventory, claim table, and disagreement map that govern the rest of the draft. +Output: Source index, paper summaries, citation bank, claims table, evidence map, disagreement map. + + + +@./.codex/get-physics-done/workflows/execute-plan.md +@./.codex/get-physics-done/templates/summary.md + + + +@.gpd/PROJECT.md +@.gpd/ROADMAP.md +@.gpd/STATE.md +@PLAN.md +@run_config.yaml + + + + + + Build source inventory + paper_runs/fusion_transport_paper_001/01_sources/source_index.json + Select the literature set, write source metadata, and create concise summaries plus a citation bank. + Check that source count satisfies the run contract and that each selected source is represented in the summary artifacts. + Source index, summaries, and citation bank exist. + + + + Extract claims and disagreements + paper_runs/fusion_transport_paper_001/02_claims/claims_table.csv + Extract explicit claims, map evidence, and identify disagreements that shape the thesis selection. + Check that each thesis candidate can point back to claims and that disagreements are explicit rather than implied. + Claims table, evidence map, and disagreement map exist and reference the source set. + + + + + +Do not permit thesis selection in this plan. The only acceptable outcome is a source-backed evidence base for downstream drafting. + + + +The overnight run has a usable source inventory, claims table, and disagreement map written under the run directory. + + + +After completion, create `.gpd/phases/01-overnight-paper-run/01-01-SUMMARY.md`. + diff --git a/.gpd/phases/01-overnight-paper-run/01-02-PLAN.md b/.gpd/phases/01-overnight-paper-run/01-02-PLAN.md new file mode 100644 index 0000000..e39ccd4 --- /dev/null +++ b/.gpd/phases/01-overnight-paper-run/01-02-PLAN.md @@ -0,0 +1,133 @@ +--- +phase: 01-overnight-paper-run +plan: 02 +type: execute +wave: 2 +depends_on: + - "01-01" +files_modified: + - paper_runs/fusion_transport_paper_001/03_disagreements/thesis_selected.md + - paper_runs/fusion_transport_paper_001/04_model/reduced_order_model_notes.md + - paper_runs/fusion_transport_paper_001/05_outline/paper_outline.md + - paper_runs/fusion_transport_paper_001/06_sections/01_introduction.md + - paper_runs/fusion_transport_paper_001/08_figures/figure_1_concept.svg +interactive: false + +conventions: + units: "SI unless overridden by cited source" + metric: "not applicable" + coordinates: "not applicable" + +dimensional_check: + quantity_name: "section and figure coverage" + +approximations: + - name: "argument-driven synthesis" + parameter: "thesis coherence" + validity: "while every major section maps to extracted evidence" + breaks_when: "section claims cannot be traced back to source artifacts" + check: "outline and sections explicitly reference the evidence map" + +contract: + scope: + question: "How should the evidence be turned into a defensible thesis, model framing, section set, and figures?" + claims: + - id: "claim-draft" + statement: "The run can draft the full section set and figures from the evidence base without overclaiming." + deliverables: ["deliv-outline", "deliv-sections"] + acceptance_tests: ["test-draft"] + references: ["ref-evidence"] + deliverables: + - id: "deliv-outline" + kind: "note" + path: "paper_runs/fusion_transport_paper_001/05_outline/paper_outline.md" + description: "Outline for the final paper package" + - id: "deliv-sections" + kind: "report" + path: "paper_runs/fusion_transport_paper_001/06_sections/01_introduction.md" + description: "Section draft set for the overnight paper" + references: + - id: "ref-evidence" + kind: "prior_artifact" + locator: ".gpd/phases/01-overnight-paper-run/01-01-SUMMARY.md" + role: "must_consider" + why_it_matters: "Captures the evidence base from plan 01." + applies_to: ["claim-draft"] + must_surface: true + required_actions: ["read", "use"] + acceptance_tests: + - id: "test-draft" + subject: "claim-draft" + kind: "consistency" + procedure: "Verify that thesis selection, outline, sections, and figures match the evidence artifacts from plan 01." + pass_condition: "All required outline, section, model, and figure files exist and remain evidence-backed." + evidence_required: ["deliv-outline", "deliv-sections", "ref-evidence"] + automation: "hybrid" + forbidden_proxies: + - id: "fp-draft" + subject: "claim-draft" + proxy: "Writing all sections before selecting a thesis and model framing." + reason: "The paper must be structured around a chosen evidence-backed thesis." + links: + - id: "link-draft" + source: "claim-draft" + target: "deliv-sections" + relation: "supports" + verified_by: ["test-draft"] + uncertainty_markers: + weakest_anchors: ["Commercial relevance claims may still be thin."] + disconfirming_observations: ["No thesis survives evidence-backed skeptical review."] +--- + + +Turn the evidence base into a complete draft section set. + +Purpose: Select the thesis, frame the reduced-order model, and produce the core paper content and figures. +Output: Thesis selection, model notes, product wedge note, outline, section drafts, figures. + + + +@./.codex/get-physics-done/workflows/execute-plan.md +@./.codex/get-physics-done/templates/summary.md + + + +@.gpd/PROJECT.md +@.gpd/ROADMAP.md +@.gpd/STATE.md +@PLAN.md +@run_config.yaml +@.gpd/phases/01-overnight-paper-run/01-01-SUMMARY.md + + + + + + Select thesis and model framing + paper_runs/fusion_transport_paper_001/03_disagreements/thesis_selected.md + Choose the thesis candidate supported by the disagreement map and write the reduced-order model notes plus commercialization wedge framing. + Check that each thesis claim points back to the evidence map and that unsupported claims are moved to limitations or excluded. + Thesis selection and model framing artifacts exist. + + + + Draft the paper body + paper_runs/fusion_transport_paper_001/06_sections/01_introduction.md + Write the outline, all section drafts, and the concept/system figures for the overnight paper. + Check that each required section from PLAN.md exists and that figure intents align with the thesis. + Outline, sections, and figures exist under the run directory. + + + + + +Every drafted section must map to the evidence base or explicitly identify itself as a limitation, risk, or open question. + + + +The run has a complete evidence-backed section set and figure set ready for revision and final assembly. + + + +After completion, create `.gpd/phases/01-overnight-paper-run/01-02-SUMMARY.md`. + diff --git a/.gpd/phases/01-overnight-paper-run/01-03-PLAN.md b/.gpd/phases/01-overnight-paper-run/01-03-PLAN.md new file mode 100644 index 0000000..af3cc5f --- /dev/null +++ b/.gpd/phases/01-overnight-paper-run/01-03-PLAN.md @@ -0,0 +1,145 @@ +--- +phase: 01-overnight-paper-run +plan: 03 +type: execute +wave: 3 +depends_on: + - "01-02" +files_modified: + - paper_runs/fusion_transport_paper_001/07_revision/critic_report.md + - paper_runs/fusion_transport_paper_001/09_final/final_paper.md + - paper_runs/fusion_transport_paper_001/09_final/final_paper.tex + - paper_runs/fusion_transport_paper_001/trace.json +interactive: false + +conventions: + units: "SI unless overridden by cited source" + metric: "not applicable" + coordinates: "not applicable" + +dimensional_check: + quantity_name: "final artifact completeness" + +approximations: + - name: "bounded overnight revision" + parameter: "remaining wall-clock budget" + validity: "until hard stop is reached" + breaks_when: "revision would prevent final assembly" + check: "leave enough time budget for final paper packaging" + +contract: + scope: + question: "Can the run critique, revise, package, and trace the final overnight manuscript before hard stop?" + claims: + - id: "claim-final" + statement: "The overnight run can finish a reviewable final package and a trace summary by the hard stop." + deliverables: ["deliv-final-paper", "deliv-trace-summary"] + acceptance_tests: ["test-final", "test-trace-summary"] + references: ["ref-draft"] + deliverables: + - id: "deliv-final-paper" + kind: "report" + path: "paper_runs/fusion_transport_paper_001/09_final/final_paper.md" + description: "Final markdown paper and associated package files" + - id: "deliv-trace-summary" + kind: "note" + path: "paper_runs/fusion_transport_paper_001/trace.json" + description: "Trace summary mapping overnight stages to agent roles and statuses" + references: + - id: "ref-draft" + kind: "prior_artifact" + locator: ".gpd/phases/01-overnight-paper-run/01-02-SUMMARY.md" + role: "must_consider" + why_it_matters: "Captures the drafted content to critique and package." + applies_to: ["claim-final"] + must_surface: true + required_actions: ["read", "use"] + acceptance_tests: + - id: "test-final" + subject: "claim-final" + kind: "human_review" + procedure: "Generate critique artifacts, revise the draft, and ensure final package files exist." + pass_condition: "Critic report, revision plan, abstract, title options, final paper, LaTeX file, and executive summary all exist." + evidence_required: ["deliv-final-paper", "ref-draft"] + automation: "hybrid" + - id: "test-trace-summary" + subject: "deliv-trace-summary" + kind: "schema" + procedure: "Verify the trace summary captures stage names, agent roles, timestamps, and statuses." + pass_condition: "Trace summary contains one entry per overnight stage." + evidence_required: ["deliv-trace-summary"] + automation: "automated" + forbidden_proxies: + - id: "fp-final" + subject: "claim-final" + proxy: "Stopping after section drafts without critique, final assembly, or trace finalization." + reason: "That would not satisfy the finished-paper requirement." + links: + - id: "link-final" + source: "claim-final" + target: "deliv-final-paper" + relation: "supports" + verified_by: ["test-final"] + - id: "link-trace-final" + source: "claim-final" + target: "deliv-trace-summary" + relation: "supports" + verified_by: ["test-trace-summary"] + uncertainty_markers: + weakest_anchors: ["Final package quality depends on the strength of earlier evidence extraction."] + disconfirming_observations: ["Hard stop arrives before final paper assembly completes."] +--- + + +Package the overnight run into a reviewable final paper bundle. + +Purpose: Critique the draft, revise it, produce final paper files, and complete trace accounting. +Output: Revision artifacts, abstract, title options, final paper files, executive summary, trace summary. + + + +@./.codex/get-physics-done/workflows/execute-plan.md +@./.codex/get-physics-done/templates/summary.md + + + +@.gpd/PROJECT.md +@.gpd/ROADMAP.md +@.gpd/STATE.md +@PLAN.md +@run_config.yaml +@.gpd/phases/01-overnight-paper-run/01-02-SUMMARY.md +@paper_runs/fusion_transport_paper_001/trace.json + + + + + + Critique and revise draft + paper_runs/fusion_transport_paper_001/07_revision/critic_report.md + Write the critic report and revision plan, then revise the draft to resolve the highest-risk issues first. + Check that revisions preserve evidence traceability and keep explicit risks/limitations. + Revision artifacts exist and the draft is updated for final assembly. + + + + Assemble final package and trace summary + paper_runs/fusion_transport_paper_001/09_final/final_paper.md + Produce abstract, title options, final markdown and LaTeX papers, executive summary, and update trace.json with stage ownership and completion status. + Check that all final deliverables exist and that trace.json records each overnight stage. + Final package and trace summary exist before hard stop. + + + + + +The overnight run is only complete if the final package and trace summary both exist. + + + +The run ends with a reviewable final paper package plus a trace summary mapping the work across overnight stages. + + + +After completion, create `.gpd/phases/01-overnight-paper-run/01-03-SUMMARY.md`. + diff --git a/.gpd/phases/01-overnight-paper-run/01-03-SUMMARY.md b/.gpd/phases/01-overnight-paper-run/01-03-SUMMARY.md new file mode 100644 index 0000000..dc64646 --- /dev/null +++ b/.gpd/phases/01-overnight-paper-run/01-03-SUMMARY.md @@ -0,0 +1,29 @@ +# 01-03 Summary + +## Outcome + +Completed the interrupted final packaging stage for the overnight paper run. + +## Artifacts produced + +- `paper_runs/fusion_transport_paper_001/06_sections/03_field_disagreement.md` +- `paper_runs/fusion_transport_paper_001/06_sections/04_thesis.md` +- `paper_runs/fusion_transport_paper_001/06_sections/05_method_or_framework.md` +- `paper_runs/fusion_transport_paper_001/06_sections/06_commercial_relevance.md` +- `paper_runs/fusion_transport_paper_001/06_sections/07_risks_limitations.md` +- `paper_runs/fusion_transport_paper_001/06_sections/08_conclusion.md` +- `paper_runs/fusion_transport_paper_001/07_revision/critic_report.md` +- `paper_runs/fusion_transport_paper_001/07_revision/revision_plan.md` +- `paper_runs/fusion_transport_paper_001/08_figures/figure_1_concept.svg` +- `paper_runs/fusion_transport_paper_001/08_figures/figure_2_system_architecture.svg` +- `paper_runs/fusion_transport_paper_001/09_final/abstract.md` +- `paper_runs/fusion_transport_paper_001/09_final/title_options.md` +- `paper_runs/fusion_transport_paper_001/09_final/final_paper.md` +- `paper_runs/fusion_transport_paper_001/09_final/final_paper.tex` +- `paper_runs/fusion_transport_paper_001/09_final/executive_summary.md` + +## Notes + +- The prior run had stopped after section 02. +- The completed draft keeps the commercialization claim bounded to operational leverage. +- Trace metadata was updated to mark all stages complete. diff --git a/Makefile b/Makefile index 36ef41e..aa595f9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ SRC := $(PKG) $(TESTS) SYNC_DELETE_REMOTE ?= 0 # ==== Meta ==== -.PHONY: help default init lint type format test coverage check env-check clean sync +.PHONY: help default init lint type format test coverage check env-check resume-check clean sync gate smoke security-audit sbom default: help @@ -19,7 +19,12 @@ help: @echo " test Run pytest when tests exist" @echo " coverage Run pytest with coverage when tests exist" @echo " check Run lint, type, and test" + @echo " gate Run repository policy and workflow hardening checks" + @echo " smoke Run the installed CLI smoke test" + @echo " security-audit Scan the repo for common secret and workflow issues" + @echo " sbom Generate a CycloneDX SBOM under runs/security/" @echo " env-check Verify expected local project files exist" + @echo " resume-check Diagnose interrupted overnight runs and print recovery steps" @echo " clean Remove caches and build artifacts" @echo " sync Rebase local main on origin/main and prune merged branches" @@ -60,9 +65,28 @@ coverage: check: lint type test +gate: + $(PYTHON) -m src.scripts.security_tools gate + +smoke: + @output="$$( $(PYTHON) -c "from src.gpd_test.cli import main; main()" )"; \ + if [ "$$output" != "get-physics-done-test" ]; then \ + echo "Unexpected CLI output: $$output"; \ + exit 1; \ + fi + +security-audit: + $(PYTHON) -m src.scripts.security_tools audit + +sbom: + $(PYTHON) -m src.scripts.security_tools sbom + env-check: $(PYTHON) -m src.scripts.check_env +resume-check: + $(PYTHON) -m src.scripts.check_env recover + # ==== Hygiene ==== clean: rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage coverage.xml dist build \ diff --git a/PLAN.md b/PLAN.md index 660b5b6..e6463cb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -11,40 +11,41 @@ Produce a complete draft paper during a bounded overnight run using GPD inside C ## Constraints - Start from Codex using GPD local install -- Stop new research at 04:45 America/New_York -- Final assembly complete by 05:45 America/New_York +- Total unattended runtime capped at 5 hours +- Stop new research at 03:00 America/New_York +- Final assembly complete by 04:00 America/New_York - Write all intermediate artifacts to disk - Prefer concise evidence-backed writing ## Required outputs -- 00_scope/topic_lock.md -- 01_sources/source_index.json -- 01_sources/paper_summaries.md -- 01_sources/citation_bank.bib -- 02_claims/claims_table.csv -- 02_claims/evidence_map.md -- 03_disagreements/disagreement_map.md -- 03_disagreements/thesis_candidates.md -- 03_disagreements/thesis_selected.md -- 04_model/reduced_order_model_notes.md -- 04_model/product_wedge.md -- 05_outline/paper_outline.md -- 06_sections/01_introduction.md -- 06_sections/02_background.md -- 06_sections/03_field_disagreement.md -- 06_sections/04_thesis.md -- 06_sections/05_method_or_framework.md -- 06_sections/06_commercial_relevance.md -- 06_sections/07_risks_limitations.md -- 06_sections/08_conclusion.md -- 07_revision/critic_report.md -- 07_revision/revision_plan.md -- 08_figures/figure_1_concept.svg -- 08_figures/figure_2_system_architecture.svg -- 09_final/abstract.md -- 09_final/title_options.md -- 09_final/final_paper.md -- 09_final/final_paper.tex -- 09_final/executive_summary.md -- trace.json +- paper_runs/fusion_transport_paper_001/00_scope/topic_lock.md +- paper_runs/fusion_transport_paper_001/01_sources/source_index.json +- paper_runs/fusion_transport_paper_001/01_sources/paper_summaries.md +- paper_runs/fusion_transport_paper_001/01_sources/citation_bank.bib +- paper_runs/fusion_transport_paper_001/02_claims/claims_table.csv +- paper_runs/fusion_transport_paper_001/02_claims/evidence_map.md +- paper_runs/fusion_transport_paper_001/03_disagreements/disagreement_map.md +- paper_runs/fusion_transport_paper_001/03_disagreements/thesis_candidates.md +- paper_runs/fusion_transport_paper_001/03_disagreements/thesis_selected.md +- paper_runs/fusion_transport_paper_001/04_model/reduced_order_model_notes.md +- paper_runs/fusion_transport_paper_001/04_model/product_wedge.md +- paper_runs/fusion_transport_paper_001/05_outline/paper_outline.md +- paper_runs/fusion_transport_paper_001/06_sections/01_introduction.md +- paper_runs/fusion_transport_paper_001/06_sections/02_background.md +- paper_runs/fusion_transport_paper_001/06_sections/03_field_disagreement.md +- paper_runs/fusion_transport_paper_001/06_sections/04_thesis.md +- paper_runs/fusion_transport_paper_001/06_sections/05_method_or_framework.md +- paper_runs/fusion_transport_paper_001/06_sections/06_commercial_relevance.md +- paper_runs/fusion_transport_paper_001/06_sections/07_risks_limitations.md +- paper_runs/fusion_transport_paper_001/06_sections/08_conclusion.md +- paper_runs/fusion_transport_paper_001/07_revision/critic_report.md +- paper_runs/fusion_transport_paper_001/07_revision/revision_plan.md +- paper_runs/fusion_transport_paper_001/08_figures/figure_1_concept.svg +- paper_runs/fusion_transport_paper_001/08_figures/figure_2_system_architecture.svg +- paper_runs/fusion_transport_paper_001/09_final/abstract.md +- paper_runs/fusion_transport_paper_001/09_final/title_options.md +- paper_runs/fusion_transport_paper_001/09_final/final_paper.md +- paper_runs/fusion_transport_paper_001/09_final/final_paper.tex +- paper_runs/fusion_transport_paper_001/09_final/executive_summary.md +- paper_runs/fusion_transport_paper_001/trace.json diff --git a/README.md b/README.md index 2ac9a02..16a8c6c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,101 @@ # get-physics-done-test -Public log of using Get Physics Done with Codex for predictive control of plasma turbulence and confinement in fusion reactors. +Public research log exploring the use of Get Physics Done (GPD) with Codex for predictive control of plasma turbulence and confinement in fusion reactors. + +This repository documents experiments using GPD as a structured research workflow that converts physics questions into bounded overnight paper drafts and reproducible research artifacts. ## Goal -Use GPD as the structured research workflow layer on top of Codex to produce bounded overnight paper drafts and research artifacts. +Evaluate whether GPD can accelerate early-stage research exploration in fusion control problems by producing structured drafts, literature summaries, and model proposals overnight. + +The specific focus is predictive control of tokamak plasma turbulence and confinement. + +## What is GPD + +Get Physics Done (GPD) is a structured research workflow system that organizes AI-assisted scientific exploration into reproducible plans, artifacts, and paper drafts. + +Instead of generating free-form text, GPD runs structured research steps and writes all intermediate outputs to disk. + +## Referenced software + +This repository uses the following external research software: + +```bibtex +@software{physical_superintelligence_2026_gpd, + author = {{Physical Superintelligence PBC}}, + title = {Get Physics Done (GPD)}, + version = {1.1.0}, + year = {2026}, + url = {https://github.com/psi-oss/get-physics-done}, + license = {Apache-2.0} +} +``` ## Stack - Python 3.11 -- uv -- Codex -- GPD +- uv (environment and dependency manager) +- Codex (reasoning and code generation) +- Get Physics Done (GPD) + +## Repository structure + +```text +. +β”œβ”€β”€ .agents/ +β”‚ └── skills/ +β”œβ”€β”€ .codex/ +β”‚ β”œβ”€β”€ agents/ +β”‚ β”œβ”€β”€ get-physics-done/ +β”‚ β”‚ β”œβ”€β”€ references/ +β”‚ β”‚ β”œβ”€β”€ templates/ +β”‚ β”‚ └── workflows/ +β”‚ └── hooks/ +β”œβ”€β”€ .github/ +β”‚ β”œβ”€β”€ ISSUE_TEMPLATE/ +β”‚ └── workflows/ +β”œβ”€β”€ .gpd/ +β”‚ β”œβ”€β”€ observability/ +β”‚ β”œβ”€β”€ phases/ +β”‚ β”‚ └── 01-overnight-paper-run/ +β”‚ └── traces/ +β”œβ”€β”€ configs/ +β”œβ”€β”€ dev/ +β”œβ”€β”€ docs/ +β”œβ”€β”€ paper_runs/ +β”‚ └── fusion_transport_paper_001/ +β”‚ β”œβ”€β”€ 00_scope/ +β”‚ β”œβ”€β”€ 01_sources/ +β”‚ β”œβ”€β”€ 02_claims/ +β”‚ β”œβ”€β”€ 03_disagreements/ +β”‚ β”œβ”€β”€ 04_model/ +β”‚ β”œβ”€β”€ 05_outline/ +β”‚ β”œβ”€β”€ 06_sections/ +β”‚ β”œβ”€β”€ 07_revision/ +β”‚ β”œβ”€β”€ 08_figures/ +β”‚ └── 09_final/ +β”œβ”€β”€ runs/ +β”‚ └── security/ +β”œβ”€β”€ scripts/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ gpd_test/ +β”‚ └── scripts/ +β”œβ”€β”€ tests/ +β”œβ”€β”€ .gitignore +β”œβ”€β”€ LICENSE +β”œβ”€β”€ Makefile +β”œβ”€β”€ PLAN.md +β”œβ”€β”€ README.md +β”œβ”€β”€ pyproject.toml +β”œβ”€β”€ run_config.yaml +β”œβ”€β”€ security_best_practices_report.md +└── uv.lock +``` -## Repo conventions +### Structure notes -- all overnight runs go under `paper_runs/` -- all plans are committed -- all output artifacts are written to disk -- final review packet lives under `09_final/` +- `paper_runs/` holds the generated research artifacts for each bounded overnight run. +- `.gpd/` stores GPD project state, plans, traces, and research workflow metadata. +- `.codex/` contains Codex agents, hooks, and the vendored GPD reference/workflow material used by this repo. +- `src/` and `tests/` contain the small Python support code and its verification suite. +- `.github/workflows/` defines CI, CodeQL, and supply-chain/security automation. diff --git a/pyproject.toml b/pyproject.toml index 128841e..39f8de7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ [project.scripts] gpd-test = "src.gpd_test.cli:main" +gpd-test-recover = "src.scripts.check_env:recover_main" [tool.setuptools.packages.find] where = ["."] diff --git a/run_config.yaml b/run_config.yaml index 3db4b03..1e6dba0 100644 --- a/run_config.yaml +++ b/run_config.yaml @@ -1,11 +1,11 @@ run_id: fusion_transport_paper_001 timezone: America/New_York start_time_local: "2026-03-15T23:00:00" -freeze_new_research_local: "2026-03-16T04:45:00" -hard_stop_local: "2026-03-16T03:45:00" +freeze_new_research_local: "2026-03-16T03:00:00" +hard_stop_local: "2026-03-16T04:00:00" limits: - max_runtime_minutes: 405 + max_runtime_minutes: 300 max_total_steps: 60 max_replans: 4 max_sources: 40 @@ -17,6 +17,7 @@ limits: min_revision_passes: 1 required_outputs: + - paper_runs/fusion_transport_paper_001/trace.json - paper_runs/fusion_transport_paper_001/01_sources/paper_summaries.md - paper_runs/fusion_transport_paper_001/02_claims/claims_table.csv - paper_runs/fusion_transport_paper_001/03_disagreements/disagreement_map.md diff --git a/security_best_practices_report.md b/security_best_practices_report.md new file mode 100644 index 0000000..f152b1f --- /dev/null +++ b/security_best_practices_report.md @@ -0,0 +1,76 @@ +# Security Best Practices Report + +## Executive Summary + +No hardcoded credentials or obvious code-execution sinks showed up in the Python code during this review. The main issues are repository hygiene and CI supply-chain hardening: local research/session artifacts are currently one accidental `git add .` away from publication, several GitHub Actions are referenced by mutable tags instead of immutable SHAs, and some declared security jobs call missing Make targets, which leaves gaps in the repo's actual protections. + +## Moderate Severity + +### SBP-001: Local operational artifacts are not ignored and already contain workstation metadata + +Impact: A routine bulk commit could publish local paths, session metadata, process metadata, and research trace logs to a public repository. + +- [`.gitignore` lines 1-40](/home/qol/get-physics-done-test/.gitignore#L1) do not ignore `.gpd/`, `paper_runs/`, generated LaTeX artifacts, or other research-run outputs. +- [`.gpd/observability/current-session.json:5` ](/home/qol/get-physics-done-test/.gpd/observability/current-session.json#L5) contains the absolute local path `/home/qol/get-physics-done-test`. +- [`.gpd/observability/sessions/20260315T232504-2-a364e3.jsonl:1` ](/home/qol/get-physics-done-test/.gpd/observability/sessions/20260315T232504-2-a364e3.jsonl#L1) contains session IDs, timestamps, command metadata, and the same absolute path. +- [`run_config.yaml:1` ](/home/qol/get-physics-done-test/run_config.yaml#L1) and [`.git status --short` at review time] show active run metadata and untracked research directories present in the working tree. + +Why it matters: +- This is a common personal-GitHub failure mode: local traces and generated work products are not secrets in the narrow sense, but they leak environment details and operational history. +- The risk is elevated because these directories are already present and untracked, so a broad staging command would include them unless the ignore rules are tightened. + +Recommended fix: +- Add `.gpd/`, `paper_runs/`, `*.aux`, `*.log`, `*.out`, `*.pdf`, and any other generated research artifacts to `.gitignore` unless there is a deliberate reason to publish them. +- If some outputs must remain versioned, split publishable artifacts into a dedicated tracked directory and keep raw traces/session state ignored. + +### SBP-002: Some GitHub Actions are pinned to mutable tags instead of immutable SHAs + +Impact: If an upstream action tag is retargeted or the publisher is compromised, your CI or security workflow can execute attacker-controlled code. + +- [`.github/workflows/codeql.yml:60` ](/home/qol/get-physics-done-test/.github/workflows/codeql.yml#L60) uses `actions/checkout@v4`. +- [`.github/workflows/codeql.yml:70` ](/home/qol/get-physics-done-test/.github/workflows/codeql.yml#L70) uses `github/codeql-action/init@v4`. +- [`.github/workflows/codeql.yml:99` ](/home/qol/get-physics-done-test/.github/workflows/codeql.yml#L99) uses `github/codeql-action/analyze@v4`. +- [`.github/workflows/security.yml:26` ](/home/qol/get-physics-done-test/.github/workflows/security.yml#L26) uses `github/codeql-action/init@v3`. +- [`.github/workflows/security.yml:30` ](/home/qol/get-physics-done-test/.github/workflows/security.yml#L30) uses `github/codeql-action/analyze@v3`. +- [`.github/workflows/security.yml:100` ](/home/qol/get-physics-done-test/.github/workflows/security.yml#L100) uses `actions/attest-build-provenance@v1`. + +Why it matters: +- Personal repositories are frequently targeted through CI because workflows run automatically on pushes and pull requests. +- You already pin several actions by SHA elsewhere, so this inconsistency is avoidable. + +Recommended fix: +- Pin all third-party actions to verified immutable commit SHAs, including CodeQL and attestation steps. +- Keep Dependabot enabled for GitHub Actions so SHA bumps remain manageable. + +## Low Severity + +### SBP-003: Security workflow claims do not currently map to real local commands + +Impact: The repository advertises security jobs that currently fail immediately, reducing trust in the protections you think are running. + +- [`.github/workflows/ci.yml:45` ](/home/qol/get-physics-done-test/.github/workflows/ci.yml#L45) calls `uv run make gate`. +- [`.github/workflows/ci.yml:62` ](/home/qol/get-physics-done-test/.github/workflows/ci.yml#L62) calls `uv run make smoke`. +- [`.github/workflows/security.yml:49` ](/home/qol/get-physics-done-test/.github/workflows/security.yml#L49) calls `uv run make security-audit`. +- [`.github/workflows/security.yml:68` ](/home/qol/get-physics-done-test/.github/workflows/security.yml#L68) and [`.github/workflows/security.yml:97` ](/home/qol/get-physics-done-test/.github/workflows/security.yml#L97) call `uv run make sbom`. +- [`Makefile:9` ](/home/qol/get-physics-done-test/Makefile#L9) defines the full `.PHONY` target set, and it does not include `gate`, `smoke`, `security-audit`, or `sbom`. +- Local verification during review: `make gate`, `make security-audit`, and `make sbom` each returned `No rule to make target`. + +Why it matters: +- This is a security blind spot rather than a direct exploit. +- Broken checks mean dependency audit, SBOM generation, and higher-assurance validation are not actually available when the workflows run. + +Recommended fix: +- Either implement those targets in [`Makefile`](/home/qol/get-physics-done-test/Makefile) or remove the jobs until they exist. +- Treat a green security pipeline as meaningful only after the commands can run locally and in CI. + +## Informational + +### INFO-001: No hardcoded credentials found in the scanned files + +The review did not find obvious committed credentials, API tokens, SSH private keys, or AWS-style secrets in the current workspace using pattern-based scanning. That is good, but it should not replace a dedicated secret-scanning tool in CI. + +## Suggested Next Steps + +1. Tighten `.gitignore` to exclude `.gpd/`, `paper_runs/`, and generated document artifacts. +2. Pin all remaining GitHub Actions to immutable SHAs. +3. Implement or remove `gate`, `smoke`, `security-audit`, and `sbom` targets so the security workflows represent real controls. diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..70cb3d1 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +"""Project package root.""" diff --git a/src/gpd_test/__init__.py b/src/gpd_test/__init__.py new file mode 100644 index 0000000..02a703f --- /dev/null +++ b/src/gpd_test/__init__.py @@ -0,0 +1 @@ +"""CLI entrypoints for get-physics-done-test.""" diff --git a/src/scripts/__init__.py b/src/scripts/__init__.py new file mode 100644 index 0000000..99ec3fb --- /dev/null +++ b/src/scripts/__init__.py @@ -0,0 +1 @@ +"""Utility scripts for local project checks.""" diff --git a/src/scripts/check_env.py b/src/scripts/check_env.py index 96129d8..91dcd46 100644 --- a/src/scripts/check_env.py +++ b/src/scripts/check_env.py @@ -1,4 +1,342 @@ +from __future__ import annotations + +import json +import re +import sys +from datetime import datetime from pathlib import Path +from typing import Any + +import yaml + + +def _parse_local_timestamp(value: object) -> datetime: + if not isinstance(value, str): + raise ValueError(f"expected timestamp string, got {type(value).__name__}") + return datetime.fromisoformat(value) + + +def _load_run_config(path: Path) -> dict[str, object]: + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if not isinstance(data, dict): + raise ValueError("run_config.yaml must contain a mapping at the top level") + return data + + +def _validate_run_config(run_config_path: Path) -> list[str]: + errors: list[str] = [] + try: + config = _load_run_config(run_config_path) + except (OSError, ValueError, yaml.YAMLError) as exc: + return [f"invalid run_config.yaml: {exc}"] + + for key in ("run_id", "start_time_local", "freeze_new_research_local", "hard_stop_local"): + if key not in config: + errors.append(f"run_config missing: {key}") + + required_outputs = config.get("required_outputs") + if required_outputs is None: + errors.append("run_config missing: required_outputs") + elif not isinstance(required_outputs, list) or not all( + isinstance(output, str) for output in required_outputs + ): + errors.append("run_config.required_outputs must be a list of strings") + + if errors: + return errors + + try: + start = _parse_local_timestamp(config["start_time_local"]) + freeze = _parse_local_timestamp(config["freeze_new_research_local"]) + hard_stop = _parse_local_timestamp(config["hard_stop_local"]) + except ValueError as exc: + return [f"invalid run_config timestamp: {exc}"] + + if not start < freeze: + errors.append("start_time_local must be earlier than freeze_new_research_local") + if not freeze < hard_stop: + errors.append("freeze_new_research_local must be earlier than hard_stop_local") + + limits = config.get("limits", {}) + if isinstance(limits, dict) and "max_runtime_minutes" in limits: + max_runtime = limits["max_runtime_minutes"] + if not isinstance(max_runtime, int): + errors.append("limits.max_runtime_minutes must be an integer") + else: + actual_minutes = int((hard_stop - start).total_seconds() // 60) + if max_runtime != actual_minutes: + errors.append( + "limits.max_runtime_minutes must match the start/hard stop window " + f"({actual_minutes} minutes)" + ) + + return errors + + +def _load_json(path: Path) -> dict[str, Any]: + with path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise ValueError(f"{path} must contain a JSON object at the top level") + return data + + +def _load_optional_json(path: Path) -> dict[str, Any] | None: + if not path.exists(): + return None + try: + return _load_json(path) + except (OSError, ValueError, json.JSONDecodeError): + return None + + +def _extract_resume_file_from_state_md(path: Path) -> str | None: + if not path.exists(): + return None + match = re.search(r"^\*\*Resume file:\*\*\s+`([^`]+)`", path.read_text(encoding="utf-8"), re.M) + if match: + return match.group(1) + return None + + +def _load_resume_hints(root: Path) -> list[str]: + hints: list[str] = [] + state_json = _load_optional_json(root / ".gpd" / "state.json") + if state_json is not None: + direct_hint = state_json.get("resume_file") + if isinstance(direct_hint, str) and direct_hint: + hints.append(direct_hint) + session = state_json.get("session") + if isinstance(session, dict): + session_hint = session.get("resume_file") + if isinstance(session_hint, str) and session_hint: + hints.append(session_hint) + state_md_hint = _extract_resume_file_from_state_md(root / ".gpd" / "STATE.md") + if state_md_hint: + hints.append(state_md_hint) + return list(dict.fromkeys(hints)) + + +def _expected_plan_file(plan_number: int) -> str: + return f".gpd/phases/01-overnight-paper-run/01-0{plan_number}-PLAN.md" + + +def _detect_resume_plan(root: Path, config: dict[str, object]) -> int: + run_id = config.get("run_id") + if not isinstance(run_id, str) or not run_id: + return 1 + + run_root = root / "paper_runs" / run_id + if not run_root.exists(): + return 1 + + final_outputs = [ + run_root / "09_final" / "final_paper.md", + run_root / "09_final" / "executive_summary.md", + ] + revision_outputs = [ + run_root / "07_revision" / "critic_report.md", + run_root / "07_revision" / "revision_plan.md", + ] + section_files = [ + run_root / "06_sections" / "01_introduction.md", + run_root / "06_sections" / "02_background.md", + run_root / "06_sections" / "03_field_disagreement.md", + run_root / "06_sections" / "04_thesis.md", + run_root / "06_sections" / "05_method_or_framework.md", + run_root / "06_sections" / "06_commercial_relevance.md", + run_root / "06_sections" / "07_risks_limitations.md", + run_root / "06_sections" / "08_conclusion.md", + ] + stage_one_outputs = [ + run_root / "01_sources" / "paper_summaries.md", + run_root / "02_claims" / "claims_table.csv", + run_root / "03_disagreements" / "disagreement_map.md", + run_root / "05_outline" / "paper_outline.md", + ] + + if any(path.exists() for path in final_outputs + revision_outputs): + return 3 + if any(path.exists() for path in section_files): + return 3 + if any(path.exists() for path in stage_one_outputs): + return 2 + return 1 + + +def _summarize_stage_statuses(trace_data: dict[str, Any] | None) -> tuple[str | None, str | None]: + if trace_data is None: + return None, None + stages = trace_data.get("stages") + if not isinstance(stages, list): + return None, None + + latest_completed: str | None = None + first_incomplete: str | None = None + for stage in stages: + if not isinstance(stage, dict): + continue + name = stage.get("name") + status = stage.get("status") + if not isinstance(name, str) or not isinstance(status, str): + continue + if status == "complete": + latest_completed = name + elif first_incomplete is None and status in {"pending", "in_progress"}: + first_incomplete = name + return latest_completed, first_incomplete + + +def inspect_run_state(root: Path | None = None) -> dict[str, Any]: + repo_root = root or Path.cwd() + run_config_path = repo_root / "run_config.yaml" + config_errors = _validate_run_config(run_config_path) + if config_errors: + return { + "run_id": None, + "status": "inconsistent", + "errors": config_errors, + "missing_required_outputs": [], + "latest_completed_stage": None, + "first_incomplete_stage": None, + "recommended_plan": None, + "recommended_resume_file": None, + "resume_hints": _load_resume_hints(repo_root), + "next_action": None, + } + + config = _load_run_config(run_config_path) + run_id = config["run_id"] + assert isinstance(run_id, str) + run_root = repo_root / "paper_runs" / run_id + required_output_values = config.get("required_outputs", []) + assert isinstance(required_output_values, list) + required_outputs = [ + repo_root / output for output in required_output_values if isinstance(output, str) + ] + missing_required_outputs = [ + str(path.relative_to(repo_root)) for path in required_outputs if not path.exists() + ] + trace_path = run_root / "trace.json" + trace_data = _load_optional_json(trace_path) + resume_hints = _load_resume_hints(repo_root) + recommended_plan = _detect_resume_plan(repo_root, config) + recommended_resume_file = _expected_plan_file(recommended_plan) + latest_completed_stage, first_incomplete_stage = _summarize_stage_statuses(trace_data) + + any_required_present = any(path.exists() for path in required_outputs) + any_run_artifacts = run_root.exists() and any(run_root.rglob("*")) + errors: list[str] = [] + + if trace_path.exists() and trace_data is None: + errors.append(f"invalid trace.json: {trace_path}") + + if any_run_artifacts and not trace_path.exists(): + errors.append("run artifacts exist but trace.json is missing") + + if trace_data is not None: + trace_status = trace_data.get("status") + if trace_status == "complete" and missing_required_outputs: + errors.append("trace.json marks the run complete but required outputs are missing") + if trace_status in {"in_progress", "pending"} and not missing_required_outputs: + errors.append("trace.json marks the run incomplete but all required outputs exist") + + expected_resume_file = recommended_resume_file + conflicting_hints = [hint for hint in resume_hints if hint != expected_resume_file] + has_incomplete_signals = bool(missing_required_outputs or first_incomplete_stage is not None) + if conflicting_hints and has_incomplete_signals: + errors.append( + "resume hint conflicts with detected recovery point: " + ", ".join(conflicting_hints) + ) + + if errors: + status = "inconsistent" + elif not any_required_present and not any_run_artifacts: + status = "not_started" + elif ( + not missing_required_outputs + and trace_data is not None + and trace_data.get("status") == "complete" + ): + status = "complete" + elif missing_required_outputs or first_incomplete_stage is not None: + status = "incomplete" + else: + status = "incomplete" + + next_action = None + if status in {"incomplete", "inconsistent"}: + if recommended_plan == 3: + next_action = ( + f"required final outputs missing under {run_root / '09_final'}; " + "continue final assembly stage" + ) + elif recommended_plan == 2: + next_action = "required section outputs are incomplete; continue drafting stage" + else: + next_action = ( + "required source and claims outputs are incomplete; continue initial run stage" + ) + + return { + "run_id": run_id, + "status": status, + "errors": errors, + "missing_required_outputs": missing_required_outputs, + "latest_completed_stage": latest_completed_stage, + "first_incomplete_stage": first_incomplete_stage, + "recommended_plan": recommended_plan, + "recommended_resume_file": recommended_resume_file, + "resume_hints": resume_hints, + "next_action": next_action, + } + + +def format_recovery_report(report: dict[str, Any]) -> str: + lines = [ + f"Run ID: {report['run_id'] or 'unknown'}", + f"Status: {report['status']}", + ] + + errors = report.get("errors", []) + if errors: + lines.append("Errors:") + lines.extend(f"- {error}" for error in errors) + + missing = report.get("missing_required_outputs", []) + if missing: + lines.append("Missing required outputs:") + lines.extend(f"- {path}" for path in missing) + + if report.get("latest_completed_stage"): + lines.append(f"Latest completed stage: {report['latest_completed_stage']}") + if report.get("first_incomplete_stage"): + lines.append(f"First incomplete stage: {report['first_incomplete_stage']}") + + if report.get("recommended_resume_file"): + lines.append(f"Recommended resume file: {report['recommended_resume_file']}") + if report.get("recommended_plan") is not None: + lines.append(f"Recommended resume plan: 0{report['recommended_plan']}") + + if report.get("next_action"): + lines.append(f"Next action: {report['next_action']}") + + if report.get("status") in {"incomplete", "inconsistent"}: + lines.append( + "Resume command: " + f"/home/qol/.gpd/venv/bin/python -m gpd.runtime_cli --runtime codex " + f"--config-dir ./.codex --install-scope local init phase-op " + f"&& resume from `{report['recommended_resume_file']}`" + ) + + return "\n".join(lines) + + +def recover_main() -> int: + report = inspect_run_state() + print(format_recovery_report(report)) + return 0 if report["status"] in {"complete", "not_started"} else 1 def main() -> int: @@ -8,14 +346,32 @@ def main() -> int: Path(".codex/config.toml"), Path("PLAN.md"), Path("run_config.yaml"), + Path(".gpd/PROJECT.md"), + Path(".gpd/ROADMAP.md"), + Path(".gpd/STATE.md"), + Path(".gpd/state.json"), + Path(".gpd/config.json"), + Path(".gpd/phases"), ] missing = [str(p) for p in required if not p.exists()] + errors = [] if missing: - print("Missing:", ", ".join(missing)) + errors.append("Missing: " + ", ".join(missing)) + + if not missing: + errors.extend(_validate_run_config(Path("run_config.yaml"))) + report = inspect_run_state() + if report["status"] in {"incomplete", "inconsistent"}: + errors.append(format_recovery_report(report)) + + if errors: + print("\n".join(errors)) return 1 print("Environment OK") return 0 if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "recover": + raise SystemExit(recover_main()) raise SystemExit(main()) diff --git a/src/scripts/security_tools.py b/src/scripts/security_tools.py new file mode 100644 index 0000000..927a0ec --- /dev/null +++ b/src/scripts/security_tools.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import json +import re +import subprocess +import sys +from datetime import UTC, datetime +from pathlib import Path +from typing import Iterable + +import tomllib + + +HEX_SHA_RE = re.compile(r"^[0-9a-f]{40}$") +USES_RE = re.compile(r"^\s*-\s*uses:\s*([^\s@]+)@([^\s]+)\s*$") +SECRET_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( + ("github_token", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}\b")), + ("github_pat", re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b")), + ("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")), + ("private_key", re.compile(r"-----BEGIN (?:RSA|EC|OPENSSH|DSA) PRIVATE KEY-----")), + ( + "generic_secret", + re.compile(r"(?i)\b(?:api[_-]?key|secret|token|password)\b\s*[:=]\s*['\"][^'\"]{8,}['\"]"), + ), +) +VOLATILE_PATH_PREFIXES = ( + ".gpd/observability/", + ".gpd/traces/", +) +VOLATILE_FILE_NAMES = { + ".gpd/state.json", + ".gpd/config.json", +} +REQUIRED_IGNORE_ENTRIES = { + ".gpd/observability/", + ".gpd/traces/", + ".gpd/state.json", + ".gpd/config.json", + "paper_runs/", + "runs/", + "*.aux", + "*.log", + "*.out", + "*.pdf", +} +SBOM_PATH = Path("runs/security/sbom.cdx.json") + + +def _iter_files(root: Path) -> Iterable[Path]: + ignored_dirs = {".git", ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} + for path in root.rglob("*"): + if any(part in ignored_dirs for part in path.parts): + continue + if path.is_file(): + yield path + + +def _iter_tracked_files(root: Path) -> Iterable[Path]: + try: + result = subprocess.run( + ["git", "ls-files"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + except (OSError, subprocess.CalledProcessError): + yield from _iter_files(root) + return + + for line in result.stdout.splitlines(): + if not line: + continue + path = root / line + if path.is_file(): + yield path + + +def _read_text(path: Path) -> str | None: + try: + return path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + return None + + +def _check_action_pins(root: Path) -> list[str]: + errors: list[str] = [] + workflows = root / ".github" / "workflows" + if not workflows.exists(): + return errors + + for workflow in sorted(workflows.glob("*.y*ml")): + text = _read_text(workflow) + if text is None: + errors.append(f"unable to read workflow: {workflow}") + continue + for lineno, line in enumerate(text.splitlines(), start=1): + match = USES_RE.match(line) + if not match: + continue + action, ref = match.groups() + if action.startswith("./"): + continue + if not HEX_SHA_RE.fullmatch(ref): + rel = workflow.relative_to(root) + errors.append(f"{rel}:{lineno} uses mutable action ref {action}@{ref}") + return errors + + +def _check_ignore_entries(root: Path) -> list[str]: + gitignore = root / ".gitignore" + text = _read_text(gitignore) or "" + entries = { + line.strip() for line in text.splitlines() if line.strip() and not line.startswith("#") + } + missing = sorted(REQUIRED_IGNORE_ENTRIES - entries) + return [f".gitignore missing entry: {entry}" for entry in missing] + + +def _scan_for_secrets(root: Path) -> list[str]: + findings: list[str] = [] + skip_names = {"security_best_practices_report.md", "uv.lock"} + skip_suffixes = {".pdf", ".png", ".svg"} + + for path in _iter_tracked_files(root): + rel = path.relative_to(root) + rel_str = rel.as_posix() + if path.name in skip_names or path.suffix in skip_suffixes: + continue + text = _read_text(path) + if text is None: + continue + for lineno, line in enumerate(text.splitlines(), start=1): + for label, pattern in SECRET_PATTERNS: + if pattern.search(line): + findings.append(f"{rel_str}:{lineno} matched {label}") + return findings + + +def _scan_for_volatile_files(root: Path) -> list[str]: + findings: list[str] = [] + for path in _iter_tracked_files(root): + rel = path.relative_to(root).as_posix() + if rel in VOLATILE_FILE_NAMES or rel.startswith(VOLATILE_PATH_PREFIXES): + findings.append(rel) + return findings + + +def _parse_dependency_name(raw: str) -> str: + token = re.split(r"[<>=!~; ]", raw, maxsplit=1)[0] + return token.split("[", 1)[0] + + +def _load_pyproject(root: Path) -> dict[str, object]: + with (root / "pyproject.toml").open("rb") as handle: + data = tomllib.load(handle) + if not isinstance(data, dict): + raise ValueError("pyproject.toml must contain a table at the top level") + return data + + +def _build_components( + project: dict[str, object], dependency_groups: dict[str, object] +) -> list[dict[str, str]]: + components: list[dict[str, str]] = [] + + dependencies = project.get("dependencies", []) + if isinstance(dependencies, list): + for dep in dependencies: + if isinstance(dep, str): + components.append( + { + "type": "library", + "name": _parse_dependency_name(dep), + "scope": "required", + "version": dep, + } + ) + + dev_dependencies = dependency_groups.get("dev", []) + if isinstance(dev_dependencies, list): + for dep in dev_dependencies: + if isinstance(dep, str): + components.append( + { + "type": "library", + "name": _parse_dependency_name(dep), + "scope": "optional", + "version": dep, + } + ) + + unique: dict[tuple[str, str], dict[str, str]] = {} + for component in components: + key = (component["name"], component["scope"]) + unique[key] = component + return list(unique.values()) + + +def generate_sbom(root: Path) -> Path: + pyproject = _load_pyproject(root) + project = pyproject.get("project", {}) + dependency_groups = pyproject.get("dependency-groups", {}) + if not isinstance(project, dict) or not isinstance(dependency_groups, dict): + raise ValueError("invalid pyproject dependency layout") + + metadata = { + "timestamp": datetime.now(UTC).isoformat(), + "component": { + "type": "application", + "name": str(project.get("name", "unknown")), + "version": str(project.get("version", "0.0.0")), + }, + } + bom = { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "metadata": metadata, + "components": _build_components(project, dependency_groups), + } + + output_path = root / SBOM_PATH + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(bom, indent=2, sort_keys=True), encoding="utf-8") + return output_path + + +def run_gate(root: Path) -> list[str]: + errors = _check_ignore_entries(root) + errors.extend(_check_action_pins(root)) + return errors + + +def run_audit(root: Path) -> list[str]: + errors = run_gate(root) + errors.extend( + f"volatile file present in repo tree: {path}" for path in _scan_for_volatile_files(root) + ) + errors.extend( + f"potential secret pattern found: {finding}" for finding in _scan_for_secrets(root) + ) + return errors + + +def main(argv: list[str] | None = None) -> int: + args = argv if argv is not None else sys.argv[1:] + command = args[0] if args else "gate" + root = Path.cwd() + + if command == "gate": + errors = run_gate(root) + if errors: + print("\n".join(errors)) + return 1 + print("Security gate OK") + return 0 + + if command == "audit": + errors = run_audit(root) + if errors: + print("\n".join(errors)) + return 1 + print("Security audit OK") + return 0 + + if command == "sbom": + output_path = generate_sbom(root) + print(output_path.as_posix()) + return 0 + + print(f"unknown command: {command}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_check_env.py b/tests/test_check_env.py new file mode 100644 index 0000000..31db683 --- /dev/null +++ b/tests/test_check_env.py @@ -0,0 +1,351 @@ +import json +from pathlib import Path + +from src.scripts import check_env + + +def _write(path: Path, text: str = "") -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def _write_json(path: Path, payload: dict) -> None: + _write(path, json.dumps(payload, indent=2)) + + +def _base_run_config(run_id: str = "fusion_transport_paper_001") -> str: + return "\n".join( + [ + f"run_id: {run_id}", + 'start_time_local: "2026-03-15T23:00:00"', + 'freeze_new_research_local: "2026-03-16T03:00:00"', + 'hard_stop_local: "2026-03-16T04:00:00"', + "limits:", + " max_runtime_minutes: 300", + "required_outputs:", + f" - paper_runs/{run_id}/trace.json", + f" - paper_runs/{run_id}/01_sources/paper_summaries.md", + f" - paper_runs/{run_id}/02_claims/claims_table.csv", + f" - paper_runs/{run_id}/03_disagreements/disagreement_map.md", + f" - paper_runs/{run_id}/05_outline/paper_outline.md", + f" - paper_runs/{run_id}/09_final/final_paper.md", + f" - paper_runs/{run_id}/09_final/executive_summary.md", + ] + ) + + +def _base_trace(status: str = "in_progress", stage_statuses: list[str] | None = None) -> dict: + statuses = stage_statuses or ["complete", "pending", "pending"] + return { + "run_id": "fusion_transport_paper_001", + "status": status, + "stages": [ + {"name": "sources-and-claims", "status": statuses[0]}, + {"name": "draft-and-figures", "status": statuses[1]}, + {"name": "revision-and-final-assembly", "status": statuses[2]}, + ], + } + + +def _seed_repo_scaffold(root: Path) -> None: + (root / ".venv").mkdir() + _write(root / "pyproject.toml", "[project]\nname='test'\n") + _write(root / ".codex" / "config.toml", "") + _write(root / "PLAN.md", "# plan\n") + _write(root / ".gpd" / "PROJECT.md", "# project\n") + _write(root / ".gpd" / "ROADMAP.md", "# roadmap\n") + _write( + root / ".gpd" / "STATE.md", + "\n".join( + [ + "# Research State", + "", + "**Resume file:** `.gpd/phases/01-overnight-paper-run/01-01-PLAN.md`", + ] + ), + ) + _write_json( + root / ".gpd" / "state.json", + {"session": {"resume_file": ".gpd/phases/01-overnight-paper-run/01-01-PLAN.md"}}, + ) + _write(root / ".gpd" / "config.json", "{}") + (root / ".gpd" / "phases").mkdir(parents=True) + _write(root / "run_config.yaml", _base_run_config()) + + +def _create_stage_one_outputs(root: Path) -> None: + run_root = root / "paper_runs" / "fusion_transport_paper_001" + _write(run_root / "01_sources" / "paper_summaries.md", "") + _write(run_root / "02_claims" / "claims_table.csv", "") + _write(run_root / "03_disagreements" / "disagreement_map.md", "") + _write(run_root / "05_outline" / "paper_outline.md", "") + + +def _create_partial_sections(root: Path) -> None: + run_root = root / "paper_runs" / "fusion_transport_paper_001" + _write(run_root / "06_sections" / "01_introduction.md", "") + _write(run_root / "06_sections" / "02_background.md", "") + + +def _create_complete_run(root: Path) -> None: + run_root = root / "paper_runs" / "fusion_transport_paper_001" + _create_stage_one_outputs(root) + for name in [ + "01_introduction.md", + "02_background.md", + "03_field_disagreement.md", + "04_thesis.md", + "05_method_or_framework.md", + "06_commercial_relevance.md", + "07_risks_limitations.md", + "08_conclusion.md", + ]: + _write(run_root / "06_sections" / name, "") + _write(run_root / "07_revision" / "critic_report.md", "") + _write(run_root / "07_revision" / "revision_plan.md", "") + _write(run_root / "09_final" / "final_paper.md", "") + _write(run_root / "09_final" / "executive_summary.md", "") + _write_json( + run_root / "trace.json", + _base_trace(status="complete", stage_statuses=["complete", "complete", "complete"]), + ) + _write( + root / ".gpd" / "STATE.md", + "# Research State\n\n**Resume file:** `.gpd/phases/01-overnight-paper-run/01-03-PLAN.md`\n", + ) + _write_json( + root / ".gpd" / "state.json", + {"session": {"resume_file": ".gpd/phases/01-overnight-paper-run/01-03-PLAN.md"}}, + ) + + +def test_validate_run_config_accepts_consistent_window(tmp_path: Path) -> None: + path = tmp_path / "run_config.yaml" + path.write_text(_base_run_config("demo"), encoding="utf-8") + + assert check_env._validate_run_config(path) == [] + + +def test_validate_run_config_rejects_inverted_window(tmp_path: Path) -> None: + path = tmp_path / "run_config.yaml" + path.write_text( + "\n".join( + [ + "run_id: demo", + 'start_time_local: "2026-03-15T23:00:00"', + 'freeze_new_research_local: "2026-03-16T04:45:00"', + 'hard_stop_local: "2026-03-16T03:45:00"', + "required_outputs:", + " - paper_runs/demo/trace.json", + "limits:", + " max_runtime_minutes: 300", + ] + ), + encoding="utf-8", + ) + + errors = check_env._validate_run_config(path) + + assert "freeze_new_research_local must be earlier than hard_stop_local" in errors + + +def test_validate_run_config_rejects_runtime_mismatch(tmp_path: Path) -> None: + path = tmp_path / "run_config.yaml" + path.write_text( + "\n".join( + [ + "run_id: demo", + 'start_time_local: "2026-03-15T23:00:00"', + 'freeze_new_research_local: "2026-03-16T03:00:00"', + 'hard_stop_local: "2026-03-16T04:00:00"', + "required_outputs:", + " - paper_runs/demo/trace.json", + "limits:", + " max_runtime_minutes: 405", + ] + ), + encoding="utf-8", + ) + + errors = check_env._validate_run_config(path) + + assert any("must match the start/hard stop window" in error for error in errors) + + +def test_validate_run_config_requires_required_outputs(tmp_path: Path) -> None: + path = tmp_path / "run_config.yaml" + path.write_text( + "\n".join( + [ + "run_id: demo", + 'start_time_local: "2026-03-15T23:00:00"', + 'freeze_new_research_local: "2026-03-16T03:00:00"', + 'hard_stop_local: "2026-03-16T04:00:00"', + "limits:", + " max_runtime_minutes: 300", + ] + ), + encoding="utf-8", + ) + + assert "run_config missing: required_outputs" in check_env._validate_run_config(path) + + +def test_inspect_run_state_reports_not_started(tmp_path: Path) -> None: + _seed_repo_scaffold(tmp_path) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "not_started" + assert report["recommended_plan"] == 1 + + +def test_inspect_run_state_reports_interrupted_after_stage_one(tmp_path: Path) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _write_json( + tmp_path / "paper_runs" / "fusion_transport_paper_001" / "trace.json", _base_trace() + ) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "inconsistent" + assert report["recommended_plan"] == 2 + assert report["recommended_resume_file"].endswith("01-02-PLAN.md") + assert "resume hint conflicts with detected recovery point" in "\n".join(report["errors"]) + + +def test_inspect_run_state_reports_partial_section_drafting(tmp_path: Path) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _create_partial_sections(tmp_path) + _write_json( + tmp_path / "paper_runs" / "fusion_transport_paper_001" / "trace.json", _base_trace() + ) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "inconsistent" + assert report["recommended_plan"] == 3 + assert report["first_incomplete_stage"] == "draft-and-figures" + assert any( + path.endswith("09_final/final_paper.md") for path in report["missing_required_outputs"] + ) + + +def test_inspect_run_state_reports_missing_final_outputs_with_complete_trace( + tmp_path: Path, +) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _create_partial_sections(tmp_path) + _write_json( + tmp_path / "paper_runs" / "fusion_transport_paper_001" / "trace.json", + _base_trace(status="complete", stage_statuses=["complete", "complete", "complete"]), + ) + _write( + tmp_path / ".gpd" / "STATE.md", + "# Research State\n\n**Resume file:** `.gpd/phases/01-overnight-paper-run/01-03-PLAN.md`\n", + ) + _write_json( + tmp_path / ".gpd" / "state.json", + {"session": {"resume_file": ".gpd/phases/01-overnight-paper-run/01-03-PLAN.md"}}, + ) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "inconsistent" + assert report["recommended_plan"] == 3 + assert "trace.json marks the run complete but required outputs are missing" in "\n".join( + report["errors"] + ) + + +def test_inspect_run_state_reports_missing_trace_with_partial_artifacts(tmp_path: Path) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _create_partial_sections(tmp_path) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "inconsistent" + assert report["recommended_plan"] == 3 + assert "run artifacts exist but trace.json is missing" in "\n".join(report["errors"]) + + +def test_inspect_run_state_reports_conflicting_resume_hint(tmp_path: Path) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _write_json( + tmp_path / "paper_runs" / "fusion_transport_paper_001" / "trace.json", _base_trace() + ) + _write( + tmp_path / ".gpd" / "STATE.md", + "# Research State\n\n**Resume file:** `.gpd/phases/01-overnight-paper-run/01-03-PLAN.md`\n", + ) + _write_json( + tmp_path / ".gpd" / "state.json", + {"session": {"resume_file": ".gpd/phases/01-overnight-paper-run/01-03-PLAN.md"}}, + ) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "inconsistent" + assert "resume hint conflicts with detected recovery point" in "\n".join(report["errors"]) + + +def test_inspect_run_state_accepts_clean_complete_run(tmp_path: Path) -> None: + _seed_repo_scaffold(tmp_path) + _create_complete_run(tmp_path) + + report = check_env.inspect_run_state(tmp_path) + + assert report["status"] == "complete" + assert report["recommended_plan"] == 3 + + +def test_recover_main_exits_nonzero_for_interrupted_run( + tmp_path: Path, monkeypatch, capsys +) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _create_partial_sections(tmp_path) + _write_json( + tmp_path / "paper_runs" / "fusion_transport_paper_001" / "trace.json", _base_trace() + ) + monkeypatch.chdir(tmp_path) + + code = check_env.recover_main() + output = capsys.readouterr().out + + assert code == 1 + assert "Recommended resume file: .gpd/phases/01-overnight-paper-run/01-03-PLAN.md" in output + assert "Status: inconsistent" in output + + +def test_main_passes_for_complete_run(tmp_path: Path, monkeypatch, capsys) -> None: + _seed_repo_scaffold(tmp_path) + _create_complete_run(tmp_path) + monkeypatch.chdir(tmp_path) + + code = check_env.main() + output = capsys.readouterr().out + + assert code == 0 + assert "Environment OK" in output + + +def test_main_fails_for_partial_run(tmp_path: Path, monkeypatch, capsys) -> None: + _seed_repo_scaffold(tmp_path) + _create_stage_one_outputs(tmp_path) + _create_partial_sections(tmp_path) + _write_json( + tmp_path / "paper_runs" / "fusion_transport_paper_001" / "trace.json", _base_trace() + ) + monkeypatch.chdir(tmp_path) + + code = check_env.main() + output = capsys.readouterr().out + + assert code == 1 + assert "Recommended resume file: .gpd/phases/01-overnight-paper-run/01-03-PLAN.md" in output diff --git a/tests/test_security_tools.py b/tests/test_security_tools.py new file mode 100644 index 0000000..fb1a9cc --- /dev/null +++ b/tests/test_security_tools.py @@ -0,0 +1,82 @@ +import json +from pathlib import Path + +from src.scripts import security_tools + + +def _write(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def test_run_gate_accepts_pinned_workflows_and_required_ignores(tmp_path: Path) -> None: + _write( + tmp_path / ".gitignore", + "\n".join(sorted(security_tools.REQUIRED_IGNORE_ENTRIES)) + "\n", + ) + _write( + tmp_path / ".github" / "workflows" / "ci.yml", + "steps:\n - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n", + ) + + assert security_tools.run_gate(tmp_path) == [] + + +def test_run_gate_rejects_mutable_action_refs(tmp_path: Path) -> None: + _write( + tmp_path / ".gitignore", + "\n".join(sorted(security_tools.REQUIRED_IGNORE_ENTRIES)) + "\n", + ) + _write( + tmp_path / ".github" / "workflows" / "ci.yml", + "steps:\n - uses: actions/checkout@v4\n", + ) + + errors = security_tools.run_gate(tmp_path) + + assert any("mutable action ref" in error for error in errors) + + +def test_run_audit_detects_secret_patterns(tmp_path: Path) -> None: + _write( + tmp_path / ".gitignore", + "\n".join(sorted(security_tools.REQUIRED_IGNORE_ENTRIES)) + "\n", + ) + _write( + tmp_path / ".github" / "workflows" / "ci.yml", + "steps:\n - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n", + ) + token_value = "ghp_" + "123456789012345678901234567890123456" + fixture_line = "".join(["to", "ken", ' = "', token_value, '"\n']) + _write(tmp_path / "src" / "example.py", fixture_line) + + errors = security_tools.run_audit(tmp_path) + + assert any("potential secret pattern found" in error for error in errors) + + +def test_generate_sbom_writes_expected_file(tmp_path: Path) -> None: + _write( + tmp_path / "pyproject.toml", + "\n".join( + [ + "[project]", + 'name = "demo"', + 'version = "1.2.3"', + 'dependencies = ["typer>=0.12", "pyyaml>=6.0"]', + "", + "[dependency-groups]", + 'dev = ["pytest>=8.2"]', + ] + ) + + "\n", + ) + + output_path = security_tools.generate_sbom(tmp_path) + payload = json.loads(output_path.read_text(encoding="utf-8")) + + assert output_path == tmp_path / "runs" / "security" / "sbom.cdx.json" + assert payload["bomFormat"] == "CycloneDX" + assert payload["metadata"]["component"]["name"] == "demo" + names = {component["name"] for component in payload["components"]} + assert names == {"pytest", "pyyaml", "typer"}