name: Editorial Governance on: pull_request: paths: - 'config/themes/**' - 'code/scripts/build_theme_catalog.py' - 'code/scripts/validate_description_mapping.py' - 'code/scripts/lint_theme_editorial.py' - 'code/scripts/ratchet_description_thresholds.py' - 'code/tests/test_theme_description_fallback_regression.py' workflow_dispatch: jobs: validate-editorial: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install deps run: | python -m pip install --upgrade pip python -m pip install -r requirements.txt if [ -f requirements-dev.txt ]; then python -m pip install -r requirements-dev.txt; fi - name: Build catalog (alt output, seed) run: | python code/scripts/build_theme_catalog.py --output config/themes/theme_list_ci.json --limit 0 env: EDITORIAL_INCLUDE_FALLBACK_SUMMARY: '1' EDITORIAL_SEED: '123' - name: Lint editorial YAML (enforced minimum examples) run: | python code/scripts/lint_theme_editorial.py --strict --min-examples 5 --enforce-min-examples env: EDITORIAL_REQUIRE_DESCRIPTION: '1' EDITORIAL_REQUIRE_POPULARITY: '1' EDITORIAL_MIN_EXAMPLES_ENFORCE: '1' - name: Validate description mapping run: | python code/scripts/validate_description_mapping.py - name: Run regression & unit tests (editorial subset + enforcement) run: | python -m pytest -q code/tests/test_theme_description_fallback_regression.py code/tests/test_synergy_pairs_and_provenance.py code/tests/test_editorial_governance_phase_d_closeout.py code/tests/test_theme_editorial_min_examples_enforced.py - name: Ratchet proposal (non-blocking) run: | python code/scripts/ratchet_description_thresholds.py > ratchet_proposal.json || true - name: Upload ratchet proposal artifact uses: actions/upload-artifact@v4 with: name: ratchet-proposal path: ratchet_proposal.json - name: Post ratchet proposal PR comment if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const markerStart = ''; const markerEnd = ''; let proposal = {}; try { proposal = JSON.parse(fs.readFileSync('ratchet_proposal.json','utf8')); } catch(e) { proposal = {error: 'Failed to read ratchet_proposal.json'}; } function buildBody(p) { if (p.error) { return `${markerStart}\n**Description Fallback Ratchet Proposal**\n\n:warning: Could not compute proposal: ${p.error}. Ensure history file exists and job built with EDITORIAL_INCLUDE_FALLBACK_SUMMARY=1.\n${markerEnd}`; } const curTotal = p.current_total_ceiling; const curPct = p.current_pct_ceiling; const propTotal = p.proposed_total_ceiling; const propPct = p.proposed_pct_ceiling; const changedTotal = propTotal !== curTotal; const changedPct = propPct !== curPct; const rationale = (p.rationale && p.rationale.length) ? p.rationale.map(r=>`- ${r}`).join('\n') : '- No ratchet conditions met (headroom not significant).'; const testFile = 'code/tests/test_theme_description_fallback_regression.py'; let updateSnippet = 'No changes recommended.'; if (changedTotal || changedPct) { updateSnippet = [ 'Update ceilings in regression test (lines asserting generic_total & generic_pct):', '```diff', `- assert summary.get('generic_total', 0) <= ${curTotal}, summary`, `+ assert summary.get('generic_total', 0) <= ${propTotal}, summary`, `- assert summary.get('generic_pct', 100.0) < ${curPct}, summary`, `+ assert summary.get('generic_pct', 100.0) < ${propPct}, summary`, '```' ].join('\n'); } return `${markerStart}\n**Description Fallback Ratchet Proposal**\n\nLatest snapshot generic_total: **${p.latest_total}** | median recent generic_pct: **${p.median_recent_pct}%** (window ${p.records_considered})\n\n| Ceiling | Current | Proposed |\n|---------|---------|----------|\n| generic_total | ${curTotal} | ${propTotal}${changedTotal ? ' ←' : ''} |\n| generic_pct | ${curPct}% | ${propPct}%${changedPct ? ' ←' : ''} |\n\n**Rationale**\n${rationale}\n\n${updateSnippet}\n\nHistory-based ratcheting keeps pressure on reducing generic fallback descriptions. If adopting the new ceilings, ensure editorial quality remains stable.\n\n_Analysis generated by ratchet bot._\n${markerEnd}`; } const body = buildBody(proposal); const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, per_page: 100 }); const existing = comments.find(c => c.body && c.body.includes(markerStart)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body }); core.info('Updated existing ratchet proposal comment.'); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body }); core.info('Created new ratchet proposal comment.'); }