Compare commits

...
32 Commits
Author SHA1 Message Date
Nubenetes Bot 01634bceb2 release: v2.6.2 — Fix .html URLs, add digest placeholders, clean URL mandate 2026-06-19 09:29:24 +02:00
Inaki d456128366 Merge pull request #353 from nubenetes/feat/fix-html-urls-and-digest-placeholders
fix: remove offline plugin, add digest placeholders, enforce clean URL mandate
2026-06-19 09:29:04 +02:00
Nubenetes BotandClaude Sonnet 4.6 13d51e8fa9 fix: remove offline plugin (.html suffix), add digest placeholder pages, enforce clean URL mandate
- Remove offline plugin from v2-mkdocs.yml — it forces .html suffixes
  on all URLs, breaking SEO and existing deep-links
- Add placeholder tech-digest.md and industry-digest.md pages to prevent
  404 errors before first Gemini digest generation run
- Add clean URL mandate to CLAUDE.md and GEMINI.md: offline plugin is
  permanently forbidden, use_directory_urls must always be true
- Update README with V2 URL Policy section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:28:50 +02:00
Nubenetes Bot 87809870c5 merge: back-merge master v2.6.1 into develop 2026-06-19 09:25:40 +02:00
Nubenetes Bot e1b49ab4b5 release: v2.6.1 — Add CLAUDE.md and gitflow mandate 2026-06-19 09:25:35 +02:00
Inaki 57f9c618f1 Merge pull request #352 from nubenetes/feat/gitflow-docs
docs: add CLAUDE.md and gitflow mandate to GEMINI.md
2026-06-19 09:25:21 +02:00
Nubenetes BotandClaude Sonnet 4.6 65ce209183 docs: add CLAUDE.md with gitflow instructions and update GEMINI.md with git workflow mandate
- Create CLAUDE.md documenting gitflow branching model, release process,
  repository structure, build commands, and coding conventions for Claude
  Code agents
- Add Gitflow section to GEMINI.md (Mandate) ensuring Gemini agents also
  follow the branching discipline: feat→develop→release→master+tag+release
- Both files ensure all AI agents follow gitflow without manual reminders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:24:56 +02:00
Nubenetes Bot fdd9eac809 merge: back-merge master v2.6.0 into develop 2026-06-19 09:23:03 +02:00
Nubenetes Bot 059b4de3e8 release: v2.6.0 — Intelligence Digest, Pipeline Hardening & Portal Overhaul 2026-06-19 09:22:12 +02:00
Inaki a11bfe4ced Merge pull request #351 from nubenetes/feat/v2-news-digest-and-improvements
feat: V2 Intelligence Digest, Pipeline Hardening & Portal Overhaul
2026-06-19 09:21:22 +02:00
Nubenetes BotandClaude Sonnet 4.6 9cb73cadcc docs: update README with Intelligence Digest, enrichment pipeline, dedup engine, and MkDocs enhancements
- Add V2 Intelligence Digest section documenting 26-category temporal
  digest system with 3/6/12 month panels
- Add V2 Data Quality and Pipeline Hardening section documenting CNCF
  integration, GitHub activity enrichment, license detection, dedup,
  exception observability, expanded discovery, and stale health re-check
- Add V2 MkDocs Material Enhancements section documenting instant nav,
  breadcrumbs, announcement bar, tags/RSS/PWA plugins, and stub merges
- Update Agentic Stack table with News Digest, Enrichment, Dedup, and
  PWA capabilities
- Update Repository Inventory with new source code modules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:19:47 +02:00
Nubenetes BotandClaude Sonnet 4.6 05b66d006f feat: add company/geo_region extraction to Gemini prompts, add enrichment/dedup to cron, fix nav sync for digest pages
- Update curator Gemini prompt with Phase 5: Company & Geo Classification
  to extract company name and geo_region (americas/europe/spain/asia_pacific)
  for industry digest categories
- Store company and geo_region from AI response in eval_data
- Add Intelligence Digest nav entries to _sync_enterprise_navigation so
  digest pages persist across nav rebuilds
- Add dedup scan and enrichment pipeline steps to monthly cron workflow
  (01.1.agentic_cron.yml) in addition to publish workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:04:28 +02:00
Nubenetes BotandClaude Sonnet 4.6 3dc52af391 feat: add CNCF landscape integration, GitHub activity enrichment, license change detection, dedup engine, PWA support, and expanded video hub
- Add src/enrichment.py (395 lines): CNCF landscape API integration for
  project graduation status, GitHub issue/PR velocity tracking with
  community health scoring, and license change detection with history
- Add src/dedup.py (194 lines): URL normalization dedup, content-hash
  dedup, title-similarity dedup (85% threshold via SequenceMatcher),
  and resolution strategy keeping highest-quality entry
- Enable PWA/offline plugin in v2-mkdocs.yml for cached offline reading
- Expand video hub categorization with MLOps, security, IaC, GitOps,
  observability, FinOps, cloud providers, and language-specific routing
- Add dedup scan and enrichment pipeline steps to CI publish workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:00:53 +02:00
Nubenetes BotandClaude Sonnet 4.6 8b04851097 fix: exclude digest pages from v2_filter.js injection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:46:47 +02:00
Nubenetes BotandClaude Sonnet 4.6 782dc1bd06 feat: add AI re-evaluation staleness detection, cross-dimension See Also, and last_ai_eval tracking
- Add last_ai_eval timestamp to eval_data in both fast-track and grounded-track
  evaluation paths, plus SQL schema and curator
- Entries enriched >6 months ago are automatically flagged for re-evaluation
  instead of being skipped (stale content detection)
- Enhance "See Also" links with cross-dimension references based on shared
  tags between pages, in addition to same-dimension related links
- Add _collect_tags_from_tree() helper for recursive tag extraction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:45:57 +02:00
Nubenetes BotandClaude Sonnet 4.6 1149a9c8f7 feat: add announcement bar, tags/RSS/minify plugins, markdown extensions, and fix comparison table threshold
- Add announcement bar with digest promo via template override
- Enable tags plugin (native clickable tag navigation)
- Add RSS plugin for digest feed subscription
- Enable minify plugin for production HTML optimization
- Add markdown extensions: highlight, inlinehilite, smartsymbols, caret, tilde, tables, footnotes, abbr, def_list
- Add mkdocs-rss-plugin to requirements.txt
- Fix comparison table threshold mismatch (config: 8, code: 5 → aligned to 8)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:42:17 +02:00
Nubenetes BotandClaude Sonnet 4.6 4e87c248fd feat: implement AI-powered news digest engine, MkDocs UX overhaul, pipeline hardening, and stub page merges
- Add 26-category news digest engine (src/news_digest.py) with Gemini AI ranking
  for 3/6/12 month temporal panels across tech, cloud, and geo categories
- Add discovered_at, company, geo_region fields to inventory schema with backfill
  script populating 18K+ existing entries
- Fix critical v2-mkdocs.yml bug: plugins were nested under theme (silently disabled)
- Add MkDocs Material features: instant nav, breadcrumbs, footer, announce bar
- Add trending cards CSS grid and replace Agentic Pulse with dynamic Trending Now
- Generate tech-digest.md and industry-digest.md with tabbed 3/6/12 month views
- Merge 12 stub pages (<40 lines each) into parent categories with redirects
- Replace 50 bare except:pass patterns with contextual logging across all pipeline files
- Expand autonomous discovery from 6 to 14 GitHub search queries
- Add stale health re-check for online entries older than 30 days
- Track addition_method by source type (rss, twitter, github_trending)
- Add digest generation step to CI publish workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:38:56 +02:00
Nubenetes Bot 51e8ec8cfa docs: automated README metric synchronization [skip ci] 2026-06-18 15:42:37 +00:00
Nubenetes Bot 138b63c69f fix(ci): fix README.md TOC and safety check logic, and guard against NoneType titles in v2_optimizer 2026-06-18 17:42:04 +02:00
Nubenetes Bot 95c7078e6a fix(ci): stage both inventory.yaml and inventory.sql in workflows to prevent unstaged rebase failures 2026-06-18 17:35:40 +02:00
Nubenetes Bot cba67c689c merge branch 'develop' 2026-06-18 17:33:04 +02:00
Nubenetes Bot e527be5189 feat: implement SQLite dual-save engine, pre-commit schema linting, debate consensus caching, and reputation registry 2026-06-18 17:32:48 +02:00
Nubenetes Bot 1cd243ee58 feat: implement SQLite dual-save engine, pre-commit schema linting, debate consensus caching, and reputation registry 2026-06-18 17:32:08 +02:00
Nubenetes Bot 0c3ae9661c docs: automated README metric synchronization [skip ci] 2026-06-18 15:04:18 +00:00
Nubenetes Bot 0f53474aa9 Merge branch 'develop' 2026-06-18 17:03:36 +02:00
Nubenetes Bot 12402e089d docs: update CHANGELOG for release v2.5.8 2026-06-18 17:03:26 +02:00
Nubenetes Bot 871bd30e75 docs: document Fast-Pass, Twitter toggle, failure-aware domain health checks, and tags cap in README 2026-06-18 17:02:30 +02:00
Nubenetes Bot 78f5600e75 Merge branch 'develop' 2026-06-18 17:01:57 +02:00
Nubenetes Bot d7c9162cb8 docs: document Fast-Pass, Twitter toggle, failure-aware domain health checks, and tags cap in README 2026-06-18 17:01:47 +02:00
Nubenetes Bot 24b5bbc6a2 docs: automated README metric synchronization [skip ci] 2026-06-18 14:54:52 +00:00
Nubenetes Bot 42442a1a6c docs: update CHANGELOG for release v2.5.7 2026-06-18 16:53:44 +02:00
Nubenetes Bot 5fe5431c53 docs: automated README metric synchronization [skip ci] 2026-06-18 14:53:09 +00:00
47 changed files with 304309 additions and 169676 deletions
+15
View File
@@ -178,6 +178,21 @@ jobs:
gh workflow run 01.1.agentic_cron.yml -f historical_mode=true -f historical_chunked=true -f historical_until_date=$NEXT_DATE
fi
- name: Run Deduplication Scan
if: success()
env:
PYTHONPATH: .
run: |
python -u -c "import asyncio; from src.dedup import run_dedup; asyncio.run(run_dedup(dry_run=False))" || echo "Dedup scan skipped"
- name: Run Enrichment Pipeline
if: success()
env:
PYTHONPATH: .
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python -u -m src.enrichment || echo "Enrichment pipeline skipped"
- name: Upload Visual Dashboard Artifact
if: always()
uses: actions/upload-artifact@v7
@@ -61,9 +61,9 @@ jobs:
run: |
git config --global user.name "Nubenetes Bot"
git config --global user.email "bot@nubenetes.com"
git add data/inventory.yaml
git add data/inventory.yaml data/inventory.sql
if git diff --staged --quiet; then
echo "No changes in inventory.yaml to commit."
echo "No changes in inventory to commit."
else
git commit -m "chore: update inventory stars and licenses [skip ci]"
git pull origin develop --rebase
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
run: |
git config --global user.name "Nubenetes Bot"
git config --global user.email "bot@nubenetes.com"
git add data/inventory.yaml
git add data/inventory.yaml data/inventory.sql
if git diff --staged --quiet; then
echo "No changes in AI analysis to commit."
else
+1 -1
View File
@@ -85,7 +85,7 @@ jobs:
run: |
git config --global user.name "Nubenetes Bot"
git config --global user.email "bot@nubenetes.com"
git add data/inventory.yaml v2-docs/videos/
git add data/inventory.yaml data/inventory.sql v2-docs/videos/
if git diff --staged --quiet; then
echo "No automated changes to commit."
else
@@ -73,6 +73,27 @@ jobs:
run: |
python src/reorganize_mosaic.py
- name: Run Deduplication Scan
env:
PYTHONPATH: ${{ github.workspace }}
run: |
python -u -c "import asyncio; from src.dedup import run_dedup; asyncio.run(run_dedup(dry_run=False))" || echo "Dedup scan skipped"
- name: Run Enrichment Pipeline
env:
PYTHONPATH: ${{ github.workspace }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python -u -m src.enrichment || echo "Enrichment pipeline skipped (no token or error)"
- name: Generate News Digest
env:
PYTHONPATH: ${{ github.workspace }}
GEMINI_API_KEY_1: ${{ secrets.GEMINI_API_KEY_1 }}
GEMINI_API_KEY_2: ${{ secrets.GEMINI_API_KEY_2 }}
run: |
python -u -m src.news_digest || echo "News digest generation skipped (no API key or error)"
- name: Run V2 Publisher (Render-Only)
env:
PYTHONPATH: ${{ github.workspace }}
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
run: |
git config --global user.name "nubenetes-bot"
git config --global user.email "bot@nubenetes.com"
git add docs/ v2-docs/ README.md data/inventory.yaml src/memory/
git add docs/ v2-docs/ README.md data/inventory.yaml data/inventory.sql src/memory/
if ! git diff --cached --quiet; then
git commit -m "style(cleaner): auto-correcting formatting & URL normalization"
git push origin HEAD:${{ github.event.pull_request.head.ref }} || echo "⚠️ Push failed (likely fork permission limit)."
+6
View File
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[2.5.8]](https://github.com/nubenetes/awesome-kubernetes/releases/tag/v2.5.8) - 2026-06-18
### Added
- **Debate & Ingestion Documentation**: Documented the Fast-Pass screening evaluator, Twikit toggle, failure-aware domain timeouts, mobile identity rotation, and tag pagination limits in `README.md`.
- **Consensus Flow Visualization**: Redesigned the multi-agent consensus protocol Mermaid diagram in `README.md` to map the Fast-Pass screening logic.
## [[2.5.7]](https://github.com/nubenetes/awesome-kubernetes/releases/tag/v2.5.7) - 2026-06-18
### Added
+93
View File
@@ -0,0 +1,93 @@
# CLAUDE.md — Nubenetes Project Instructions for Claude Code
## Git Workflow: Gitflow
This repository uses **Gitflow**. All agents MUST follow this branching model:
### Branches
- **`master`** — Production. Only receives merges from release branches. Every merge to master gets a **tag** and a **GitHub Release**.
- **`develop`** — Integration branch. All feature branches merge here first.
- **`feat/*`** — Feature branches. Created from `develop`, merged back to `develop` via PR.
- **`release/vX.Y.Z`** — Release branches. Created from `develop` when ready to release, merged to both `master` AND back to `develop`.
- **`gh-pages`** — Deployment. Never touch directly.
### Release Process (mandatory for all releases)
1. Create feature branch from `develop`: `git checkout -b feat/description develop`
2. Implement changes, commit, push feature branch
3. Create PR: `gh pr create --base develop --head feat/description`
4. Merge PR to develop: `gh pr merge N --merge`
5. Create release branch: `git checkout -b release/vX.Y.Z develop`
6. Merge release to master: `git checkout master && git merge release/vX.Y.Z --no-ff`
7. Tag: `git tag -a vX.Y.Z -m "description"`
8. Push master + tag: `git push origin master && git push origin vX.Y.Z`
9. Back-merge master to develop: `git checkout develop && git merge master --no-ff && git push origin develop`
10. Create GitHub Release: `gh release create vX.Y.Z --title "..." --notes "..."`
### Versioning
- Current: `v2.6.0`
- Format: `v{major}.{minor}.{patch}`
- Major: breaking changes or architectural shifts
- Minor: new features (like the digest engine, new modules)
- Patch: bug fixes, config tweaks
### Protected Branches
`master`, `develop`, and `gh-pages` are NEVER deleted. Branch cleanup runs bi-monthly for merged feature branches.
## Repository Structure
### Key Directories
- `docs/` — V1 source (exhaustive archive, source of truth)
- `v2-docs/` — V2 source (AI-curated elite portal, derived from V1)
- `src/` — Python pipeline source code
- `data/` — Inventory (YAML + SQL), config files, digest JSON
- `scripts/` — Utility scripts (backfill, etc.)
- `.github/workflows/` — CI/CD (15 workflows)
### Key Config Files
- `v2-mkdocs.yml` — V2 MkDocs Material configuration
- `mkdocs.yml` — V1 MkDocs configuration
- `data/inventory.yaml` / `data/inventory.sql` — Unified inventory (18K+ entries)
- `data/curation_sources.yaml` — RSS feeds and X/Twitter accounts
- `data/link_rules.yaml` — Curation policies
- `GEMINI.md` — AI mandates and learning roadmap (read by Gemini agents)
### Pipeline Modules
- `src/v2_optimizer.py` — Main rendering engine (V2VisionEngine class)
- `src/news_digest.py` — 26-category temporal digest with Gemini ranking
- `src/enrichment.py` — CNCF Landscape + GitHub activity + license detection
- `src/dedup.py` — URL/hash/title deduplication engine
- `src/agentic_curator.py` — Ingestion pipeline with AI evaluation
- `src/autonomous_discovery.py` — GitHub trending discovery (14 queries)
- `src/gemini_utils.py` — Gemini API wrapper with key rotation
- `src/inventory_manager.py` — Dual YAML+SQL inventory management
## Build and Run
### Local testing (no API keys needed)
```bash
pip install -r requirements.txt
mkdocs serve -f v2-mkdocs.yml # Preview V2 portal
mkdocs serve -f mkdocs.yml # Preview V1 portal
```
### Pipeline commands
```bash
python3 -m scripts.backfill_discovered_at # Backfill discovered_at field
python3 -m src.news_digest # Generate digest (needs Gemini API key)
python3 -m src.enrichment # CNCF + GitHub enrichment (needs GH_TOKEN)
python3 -m src.dedup # Dedup scan (dry-run by default)
python3 -m src.v2_optimizer --render-only # Render V2 portal (no AI calls)
```
## Coding Conventions
- All Python exceptions must use `except Exception as e: log_event(f"[WARN] context: {str(e)[:100]}")` — never bare `except: pass`
- Use `from src.logger import log_event` for logging
- Use `from src.config import MADRID_TZ` for timezone-aware timestamps
- Inventory fields: `discovered_at` (ISO), `last_ai_eval` (ISO), `company`, `geo_region` must be preserved during merges
- The `update_inventory_entry()` function preserves `discovered_at` — never overwrite it with new data
## URL Policy: Clean URLs, No .html Suffix
Both V1 and V2 MUST use `use_directory_urls: true` in their mkdocs.yml. This produces clean URLs like `/kubernetes/` instead of `/kubernetes.html`. **NEVER** enable the `offline` plugin — it forces `.html` suffixes on all URLs, breaking SEO and existing deep-links. This is a hard rule.
## RSS/Twitter Sources
RSS feeds are limited to those that actually work (many block bots). Don't add new RSS feeds without testing. Current working feeds are defined in `data/curation_sources.yaml`.
+28 -1
View File
@@ -376,4 +376,31 @@ The bot must rotate between profiles to avoid detection:
- **V2 Index Metrics Protocol**: The "Knowledge Architecture and AI Coverage Status" report in the V2 index MUST include a direct comparison between V1 and V2 inventory. This report MUST display: 1. **V1 Base Inventory** (Total resources in the master archive), 2. **V2 Elite Selection** (Count of candidates and the resulting density ratio), 3. **AI Enrichment Coverage**, and 4. **GitHub Metadata Coverage**. This ensures transparency in the knowledge distillation process.
- **Redundancy-Free Branding**: To ensure professional UI density, the V2 Portal header MUST NOT repeat the "Nubenetes" brand. The title MUST follow the pattern: "Nubenetes Elite Portal (V2) | Awesome Kubernetes and Cloud".
- **Decoupled Workflow Architecture**: The Agentic V2 ecosystem MUST utilize a decoupled micro-workflow structure (Health Monitor, Metadata Engine, AI Curator, and Publisher) to optimize compute quotas and minimize Gemini token consumption. Any update to the V2 rendering logic MUST use the `--render-only` flag in the Publisher pipeline to maintain execution speed.
to maintain execution speed.
## Git Workflow: Gitflow (Mandatory)
This repository uses **Gitflow**. All agents and automated processes MUST follow this branching model:
- **`master`**: Production branch. Only receives merges from `release/*` branches. Every merge to master gets a **semantic version tag** (`vX.Y.Z`) and a **GitHub Release** with detailed release notes.
- **`develop`**: Integration branch. All feature branches merge here via PR. Back-merged from master after each release.
- **`feat/*`**: Feature branches. Created from `develop`, merged back to `develop` via PR.
- **`release/vX.Y.Z`**: Release branches. Created from `develop`, merged to `master` with `--no-ff`, then back-merged to `develop`.
- **`gh-pages`**: Deployment. NEVER modified directly.
- **Protected branches**: `master`, `develop`, `gh-pages` — NEVER deleted.
### Release Sequence
1. Feature branch → PR to `develop` → merge
2. Create `release/vX.Y.Z` from `develop`
3. Merge release to `master` (`--no-ff`)
4. Create annotated tag: `git tag -a vX.Y.Z -m "..."`
5. Push master + tag
6. Back-merge master to develop (`--no-ff`)
7. Create GitHub Release with `gh release create`
### Versioning: `v{major}.{minor}.{patch}`
- **Major**: breaking changes or architectural shifts
- **Minor**: new features (modules, digest categories, new pipelines)
- **Patch**: bug fixes, config tweaks, content updates
## URL Policy: Clean URLs (Mandatory)
Both V1 (`mkdocs.yml`) and V2 (`v2-mkdocs.yml`) MUST use `use_directory_urls: true` to produce clean URLs like `/kubernetes/` instead of `/kubernetes.html`. **NEVER** enable the MkDocs `offline` plugin — it forces `.html` suffixes on all URLs, breaking SEO authority and thousands of existing deep-links. This is a hard, non-negotiable rule.
+99 -34
View File
@@ -40,7 +40,7 @@
* [5.4. The Incremental Elite Engine](#54-the-incremental-elite-engine)
* [5.5. Decoupled Knowledge Lifecycle (V2 Architecture)](#55-decoupled-knowledge-lifecycle-v2-architecture)
* [5.6. Multi-Language Support Policy](#56-multi-language-support-policy)
6. [6. The Unified Agentic Database (Knowledge Graph)](#6-the-unified-agentic-database-knowledge-graph)
6. [6. The Unified Agentic Database (Coexistence Knowledge Graph)](#6-the-unified-agentic-database-coexistence-knowledge-graph)
* [6.1. Database Components](#61-database-components)
* [6.2. The 'Database-First' Reasoning Protocol (Zero-Redundancy)](#62-the-database-first-reasoning-protocol-zero-redundancy)
* [6.3. Database Lifecycle and Hygiene](#63-database-lifecycle-and-hygiene)
@@ -142,7 +142,7 @@ Additionally, as of May 2026, Nubenetes has reached the **Platinum Operational T
| :--- | :--- |
| **Total Technical Resources (Links)** | **18647+** |
| **Specialized MD Pages** | **162** |
| **Total Commits** | **5990+** |
| **Total Commits** | **6013+** |
| **Primary AI Engine** | **Google Gemini (Agentic)** |
<!-- HEART_STATS_END -->
@@ -180,7 +180,7 @@ The growth of Nubenetes reflects the acceleration of the Cloud Native ecosystem.
| 6 | 2023 | 30 | 123 | Maintenance & Refinement |
| 7 | 2024 | 53 | 218 | Curation Strategy Pivot |
| 8 | 2025 | 5 | 20 | Stability & Research Phase |
| 9 | 2026 | 2431 | 10,040 | **Agentic AI Surge** (May 2026 Inception) |
| 9 | 2026 | 2454 | 10,135 | **Agentic AI Surge** (May 2026 Inception) |
<!-- ANNUAL_GROWTH_END -->
<!-- ANNUAL_CHART_START -->
@@ -196,8 +196,8 @@ xychart-beta
title "Nubenetes Annual Growth Metrics (20182026)"
x-axis ["2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025", "2026"]
y-axis "Volume (Commits / Estimated New Refs)" 0 --> 11000
bar [1445, 586, 8449, 2193, 1660, 123, 218, 20, 10040]
bar [350, 142, 2046, 531, 402, 30, 53, 5, 2431]
bar [1445, 586, 8449, 2193, 1660, 123, 218, 20, 10135]
bar [350, 142, 2046, 531, 402, 30, 53, 5, 2454]
```
<!-- ANNUAL_CHART_END -->
@@ -207,7 +207,7 @@ xychart-beta
| :--- | :---: | :---: | :--- |
| 2026-04 | 25 | 103 | Active Curation |
| 2026-05 | 2101 | 8,677 | **Agentic Inception (Gemini Era)** |
| 2026-06 | 305 | 1,259 | Active Curation |
| 2026-06 | 328 | 1,354 | Active Curation |
<!-- MONTHLY_SURGE_END -->
### 2.4. Content Distribution and Semantic Clustering
@@ -268,6 +268,10 @@ The autonomy of Nubenetes is powered by a modern, resilient tech stack that ensu
| **Discovery** | Twikit and Playwright | Autonomous scraping and account rotation. |
| **Resilience** | Identity Rotation | Evasion of anti-bot blocks using multiple profiles. |
| **Deployment** | MkDocs Material & Native GH Pages | High-performance static site generation via native artifact deployment. |
| **Intelligence** | News Digest Engine | AI-powered temporal digest across 26 categories (3/6/12 months). |
| **Enrichment** | CNCF + GitHub Activity | Landscape graduation status, issue/PR velocity, license change detection. |
| **Dedup** | Similarity Engine | URL, content-hash, and title-similarity deduplication (85% threshold). |
| **Offline** | PWA Support | Service Worker caching for offline reading of the portal. |
---
@@ -394,11 +398,51 @@ Nubenetes operates with two distinct editions to serve different engineering nee
- **No stars**: Standard reference documentation and technical resources.
- **Multi-Dimensional Tagging (1:N):** Every resource is classified with multiple semantic tags (e.g., `[DE FACTO STANDARD]`, `[GUIDE]`, `[CASE STUDY]`, `[EMERGING]`) providing deep technical context and maturity status.
- **Minimalist Inline Summaries**: Resources feature a **"Deep-Dive"** inline tag (using native HTML5 `<details>`) that expands into a rich technical summary without consuming space when collapsed. These summaries use the **Double-Evidence Synthesis** protocol to provide verified architectural insights.
- **Semantic Cross-Linking:** The portal autonomously identifies and links related categories within the same strategic dimension (e.g., suggesting `Flux` when reading about `Argo`), creating a cohesive **Industrial Knowledge Graph**.
- **Semantic Cross-Linking:** The portal autonomously identifies and links related categories within the same strategic dimension (e.g., suggesting `Flux` when reading about `Argo`), creating a cohesive **Industrial Knowledge Graph**. Additionally, **cross-dimension "See Also" links** connect pages that share technical tags across different dimensions.
- **Executive Context**: Every strategic dimension features an AI-generated **State-of-the-Art Introduction** providing high-level architectural context and industry direction before the link listings.
- **Source of Truth:** The `v2-docs/` directory (Derived from V1).
- **Deployment:** [nubenetes.com/v2/](https://nubenetes.com/v2)
#### V2 Intelligence Digest (June 2026)
The V2 portal includes an **AI-powered Intelligence Digest** system that surfaces the most relevant resources from the last 3, 6, and 12 months across **26 curated categories**:
| Category Group | Categories |
| :--- | :--- |
| **Tech Core (9)** | Kubernetes & Orchestration, Containers & Runtime, Networking & Service Mesh, Architecture & Microservices, Data/Messaging/Storage, AI & Agents, MLOps & Data Science, Python/Java/Dev Ecosystem, Linux & System Foundations |
| **Platform & Ops (8)** | Security & Compliance, Infrastructure as Code, CI/CD & GitOps, Observability/SRE/Testing, DevOps & Culture, Platform Engineering & DevEx, FinOps & Cloud Cost, Certification & Training |
| **Cloud & Enterprise (5)** | AWS, Azure, GCP/OCI/Others, OpenShift/Red Hat, Virtualization & Private Cloud (VMware/Broadcom, Proxmox, Nutanix, KubeVirt) |
| **Industry / Geo (4)** | Americas, Europe, Spain, Asia-Pacific |
**Key features:**
- **Trending Now** cards on the index page with the top cross-category items ranked by Gemini AI
- **Dedicated digest pages** (`tech-digest.md`, `industry-digest.md`) with tabbed 3/6/12 month views
- **Temporal tracking** via `discovered_at` field on all 18,000+ inventory entries
- **Company & geo-region classification** extracted by Gemini during ingestion for industry digest
- **Automatic staleness detection**: entries enriched >6 months ago are re-evaluated by AI (`last_ai_eval`)
#### V2 Data Quality and Pipeline Hardening (June 2026)
- **CNCF Landscape Integration** (`src/enrichment.py`): Auto-fetches graduation status (Sandbox/Incubating/Graduated/Archived) for CNCF projects to power maturity tags.
- **GitHub Activity Enrichment**: Fetches issue/PR velocity and assigns community health scores (active/healthy/low/dormant).
- **License Change Detection**: Compares stored licenses with current GitHub data, flagging high-impact changes (e.g., BSL, SSPL switches).
- **Deduplication Engine** (`src/dedup.py`): URL normalization, content-hash matching, and title-similarity detection (85% threshold) to eliminate duplicate entries.
- **Exception Observability**: All 50+ bare `except: pass` patterns across the pipeline replaced with contextual logging.
- **Expanded Discovery**: Autonomous GitHub trending discovery expanded from 6 to 14 search queries covering DevOps, observability, security, IaC, databases, CI/CD, service mesh, and platform engineering.
- **Stale Health Re-check**: Online entries older than 30 days are automatically re-validated instead of being skipped.
#### V2 MkDocs Material Enhancements (June 2026)
- **Instant Navigation** with prefetch for SPA-like experience across 140+ pages
- **Breadcrumbs** (`navigation.path`) for orientation in deep category hierarchies
- **Announcement Bar** promoting the Intelligence Digest
- **Tags Plugin** for native clickable cross-page tag navigation
- **RSS Feed** for digest page subscription
- **PWA/Offline Support** for cached offline reading
- **Minify Plugin** for production HTML optimization
- **12 Stub Pages Merged** into parent categories with automatic redirects (e.g., `react.md``javascript.md`, `chef.md``ansible.md`, `oauth.md``securityascode.md`)
#### V2 URL Policy (June 2026)
- **Clean URLs enforced**: Both V1 and V2 use `use_directory_urls: true` producing SEO-friendly URLs (e.g., `/kubernetes/` not `/kubernetes.html`).
- **Offline plugin permanently removed**: The MkDocs `offline` plugin forces `.html` suffixes on all URLs, breaking thousands of existing deep-links and SEO authority. It is explicitly forbidden in both `CLAUDE.md` and `GEMINI.md` mandates.
### 5.3. Architecture Comparison Matrix: V1 vs. V2
To better understand the dual-nature of the project, the following matrix details the technical and philosophical differences between the two editions:
@@ -490,24 +534,31 @@ To embrace the diverse global Cloud Native community while maintaining internati
---
## 6. The Unified Agentic Database (Knowledge Graph)
## 6. The Unified Agentic Database (Coexistence Knowledge Graph)
Nubenetes now utilizes a **Unified Metadata Architecture** to maintain consistency across V1 and V2 while optimizing AI performance. All links are indexed in a local YAML database that serves as the **Persistent Memory** for our autonomous agents.
Nubenetes now utilizes a **Unified SQL & YAML Database Architecture** to maintain consistency across V1 and V2 while optimizing agentic operations and repository efficiency. All curated links and metadata are managed via a coexisting local database engine.
### 6.1. Database Components
1. **Central Inventory ([`data/inventory.yaml`](data/inventory.yaml))**: The universal single source of truth for technical metadata and resource lifecycle.
* **Core Data**: `title`, `year`, `stars` (0-5), `description` (V1 Native), `ai_summary` (V2 English), `category`.
* **Structural Intelligence**: `hierarchy` (Recursive list up to 10 levels), `v1_locations`, `v2_locations`.
* **Platinum Lifecycle**: `content_hash` (SHA256), `health_score` (0-100), `source_provenance`, `social_preview_url`, `mentions_count`.
### 6.1. Database Components & SQLite Engine (Option 3 Coexistence)
To guarantee backward compatibility and Git efficiency, the system operates on a dual-save database coexistence model:
1. **SQLite Database & SQL Text ([`data/inventory.sql`](data/inventory.sql))**: The Git source-of-truth. During execution, the SQL script compiles into a temporary in-memory SQLite database, enabling full relational schema access and SQL query optimization. On save, SQLite's native `iterdump()` decompiles it back into a flat SQL text database file where each resource insert occupies a single line for perfect git diff readability.
2. **Central Backup Inventory ([`data/inventory.yaml`](data/inventory.yaml))**: Automatically synchronized during database saves. Serves as a backward-compatible interface for legacy markdown parsing scripts.
3. **High-Speed Parsing (C-Loader Integration)**: Direct YAML parsing utilizes high-speed native C-extensions (`yaml.CSafeLoader` and `yaml.CSafeDumper`) across all Python scripts (e.g. `v2_optimizer.py`, `reorganize_mosaic.py`, `safety_guard.py`) for a 10x-20x speedup in parsing operations.
#### 6.1.2. Platinum Lifecycle Schema
* **Core Data**: `url` (Primary Key), `title`, `year`, `stars` (0-5), `description` (V1 Native), `ai_summary` (V2 English), `category`.
* **Structural Intelligence**: `hierarchy` (Recursive JSON list), `tags` (JSON list), `v1_locations`, `v2_locations`, `youtube_mosaic` (JSON dict).
* **Platinum Lifecycle**: `content_hash` (SHA256 fingerprint), `health_score` (0-100), `source_provenance`, `social_preview_url`, `mentions_count`, `addition_method`.
### 6.2. The 'Database-First' Reasoning Protocol (Zero-Redundancy)
To maximize economic efficiency and maintain the **30-minute execution standard**, all AI agents follow a **Database-First** and **Zero-Redundancy** protocol:
1. **Local Lookup**: Before initiating any Gemini call, the agent checks if the URL is already indexed in [`data/inventory.yaml`](data/inventory.yaml).
2. **Zero-Redundancy Pipeline**: The V2 Optimizer leverages health and metadata (`gh_stars`, `gh_license`) already validated by the `IntelligentLinkCleaner`. If a resource is marked as `status: online` and has recent metadata, V2 bypasses redundant network checks.
3. **Smart Grounding (Search Retrieval)**: AI agents only activate grounding-heavy calls (Google Search) for resources that are new, missing metadata, or flagged for `needs_ai_refresh`. This reduces latencia by >80% for 15k+ link archives.
4. **Insight Reuse**: If the resource exists with valid metadata, the agent **reuses existing insights**, reducing API traffic to zero.
5. **Memory Efficiency Tracking**: The system tracks **Cache Hit Ratios** and **Estimated Token Savings** in every Intelligence Report.
6. **Mandatory Persistence**: Modified YAML files are automatically injected into Pull Requests, ensuring that "System Memory" is version-controlled and shared across all workflows.
1. **Local Lookup**: Before initiating any Gemini call, the agent queries the compiled SQLite/SQL database to see if the URL is already indexed.
2. **Domain Reputation Registry**: In `main.py`, scraping/health-check success rates are recorded under `domain_reputation` inside `health_learning.json` for adaptive timeout and scraping rotation.
3. **Stateful Debate Caching**: In `v2_debate.py`, consensus evaluations for borderline resources are cached based on the SHA256 hash of their combined metadata (`title`, `description`, `tags`). On cache hits, the agent skips redundant LLM calls and retrieves the score directly.
4. **Pre-Commit Markdown Lint Hook**: In `src/pre_commit_schema_check.py`, a local Git pre-commit hook automatically runs on developer changes to enforce heading rules (no emojis/ampersands in titles), protocol integrity, link bracket spacing, and duplicate checks in docs markdown.
5. **Insight Reuse**: If the resource exists with valid metadata, the agent **uses existing insights**, reducing API traffic to zero.
6. **Memory Efficiency Tracking**: The system tracks **Cache Hit Ratios** and **Estimated Token Savings** in every Intelligence Report.
7. **Mandatory Persistence**: Modified databases are automatically injected into Pull Requests, ensuring that "System Memory" is version-controlled and shared across all workflows.
### 6.3. Database Lifecycle and Hygiene
To maintain a high-performance "Single Source of Truth", Nubenetes implements automated hygiene protocols:
@@ -774,6 +825,7 @@ The following matrix defines our strategic model tiering across all workflows:
| **PR Guardian** | PR Presubmit | **Gemini Flash/Lite** | Tier 1 | Rapid syntax and mandate format linting. | **Medium** |
| **Curator (X/RSS)** | Agentic Curator | **Gemini Pro** | Tier 2 | Deep reasoning for human/social context. | **Low (Burst)** |
| **Auditor** | V2 Elite Builder | **Gemini Pro** | Tier 2 | High-fidelity verification of [ELITE] resources. | **Medium** |
| **Fast-Pass Evaluator** | Curation / V2 Builder | **Gemini Flash/Lite** | Tier 1 | Rapid single-call screening for obvious consensus/non-consensus. | **High** |
| **Debater Personas** | Curation / V2 Builder | **Gemini Flash/Lite** | Tier 1 | Independent multi-perspective evaluations and rebuttals. | **High** |
| **Debate Synthesis** | Curation / V2 Builder | **Gemini Pro** | Tier 2 | High-fidelity final consensus and summary synthesis. | **Medium** |
@@ -793,17 +845,22 @@ The heart of the new Nubenetes is a suite of AI Agents that operate on our `deve
- **Elite Selection:** Scans the massive V1 archive to select the "Elite" top-tier resources.
- **2026 Taxonomy:** Reorganizes content into high-density dimensions using **relevance-first sorting**.
- **MVQ Hardening:** Automatically identifies stale repositories to exclude them from the Elite portal.
- **Tags Page Cap (Recommendation #5)**: Caps the technical tag listings in `tags.md` to 100 entries per tag block (sorted by stars/year) to prevent DOM bloat, providing fallback links to the V1 Historical Archive.
3. **IntelligentHealthChecker ([`src/intelligent_health_checker.py`](src/intelligent_health_checker.py))**:
- **Resilience:** asynchronous health checks with 3x retry and identity rotation.
- **V1 Integrity:** Focuses on link validity (removing 404s) to ensure the exhaustive V1 archive remains accessible.
- **Domain failure audits (Recommendation #2)**: Automatically logs consecutive connection failures per domain in `health_learning.json` and drops check timeouts from 12s to 3s when consecutive failures $\ge 3$ to avoid hanging.
- **Transparency:** Provides detailed, real-time unbuffered logging of all cleaning operations.
4. **DebatePanelEngine ([`src/v2_debate.py`](src/v2_debate.py))**:
- **Persona-based Evaluation**: Coordinates specialized opinions across Security Architect, SRE, and AI Engineer personas.
- **Fast-Pass screening (Recommendation #3)**: Runs a single Flash model call at start; clear-cut cases bypass the full panel debate immediately.
- **Persona-based Evaluation**: Coordinates specialized expert opinions (Security Architect, SRE, and AI Engineer personas) for borderline cases (initial scores in `[60, 75]`).
- **Consensus Resolution**: Resolves high score-divergences (>= 15 points) using a round-robin debate structure.
- **Auto-Corrective Memory**: Appends resolution logs to `src/memory/health_learning.json` for persistent, few-shot alignment.
5. **Resilient Architecture Core**:
- **Exponential Backoff**: Intelligent `tenacity`-based retry logic in `gemini_utils.py` gracefully handles 429 Rate Limits before triggering the Circuit Breaker.
- **Flash-First Architecture**: Prioritizes Gemini Flash/Lite models for high-density Analyst tasks, enabling processing of 10,000+ resources within the 6-hour GitHub Actions limit through 100-item batching and 2-second safety delays.
- **Curation Ingestion Toggle**: Supports `ENABLE_TWITTER_CURATION` environment flag to dynamically toggle Playwright-based Twikit extraction when remote scraping blocks.
- **Adaptive Timeout & UA Rotation**: Adapts request headers and reduces health check timeouts dynamically under network throttle/block conditions.
- **Programmatic Smart Injection (Option B)**: The system extracts document headers and has Gemini Flash choose the target header, performing the actual line insertion using Python. This bypasses the need for Gemini Pro to rewrite entire documents, slashing API usage and preventing 429 errors.
- **Incremental Persistence (Mandate 22)**: Implements a dual-phase auto-save mechanism that flushes the `inventory.yaml` database to disk periodically **without waiting for the workflow to finish**:
* **Metadata Phase**: Saves every **500 GitHub repositories** processed.
@@ -819,17 +876,20 @@ To eliminate individual LLM rating bias, resolve borderline cases, and prevent a
```mermaid
graph TD
A["New Resource Found"] --> B["Persona 1: Security Architect"]
A --> C["Persona 2: Cloud Native SRE"]
A --> D["Persona 3: AI Platform Engineer"]
B --> E["Independent Evaluations"]
A["New Resource Found"] --> FP["Fast-Pass Evaluator (Flash)"]
FP -->|Confident Score Outside 60-75| G["Accept / Reject Directly"]
FP -->|Borderline Score 60-75| B["Persona 1: Security Architect"]
FP -->|Borderline Score 60-75| C["Persona 2: Cloud Native SRE"]
FP -->|Borderline Score 60-75| D["Persona 3: AI Platform Engineer"]
B --> E["Independent expert Evaluations"]
C --> E
D --> E
E -->|Scores Diverge >= 15 points| F["Trigger Debate Round"]
E -->|Scores Converge| G["Accept / Reject Directly"]
E -->|Expert Scores Diverge >= 15 points| F["Trigger Debate Round"]
E -->|Expert Scores Converge| G
F --> H["Round-Robin Discussion: Argue Pros and Cons"]
H --> I["Consensus Reached and Final Score Assigned"]
I --> J["Save Decision to Persistent Memory JSON"]
G --> J
```
#### 8.3.1. Panel of Expert Personas
@@ -839,14 +899,15 @@ The panel consists of three distinct virtual expert roles, each prompting Gemini
* **AI Platform Engineer**: Judges developer productivity, ease of integration with the modern AI stack (e.g., Model Context Protocol (MCP) tools), and overall 2026 Cloud Native architectural relevance.
#### 8.3.2. Protocol Execution Flow
The debate protocol executes asynchronously in three distinct phases:
1. **Phase 1: Independent Evaluation**: Each expert persona independently evaluates the resource (using Google Search Grounding to check the live state of the project). They assign an architectural impact score (0100) and write a 12 sentence justification.
2. **Phase 2: Divergence Assessment and Rebuttal**: If the difference between the highest and lowest assigned scores is **$\ge 15$ points**, a debate round is triggered. Each expert receives the scores and justifications of their peers and is asked to defend or revise their score in a rebuttal round.
3. **Phase 3: Consensus and Synthesis**: The final consensus score is the average of the revised scores of the three personas. A fourth agent (Curation Synthesis Agent) compiles the justifications and rebuttals, generating a refined, high-density technical summary (25 sentences) and selecting precise ecosystem tags (e.g., `[DE FACTO STANDARD]`, `[ENTERPRISE-STABLE]`, `[EMERGING]`).
The debate protocol executes in the following phases:
1. **Fast-Pass Screening**: A single-call evaluator rates the resource. If the rating is highly confident (score $\le 59$ or $\ge 76$), it bypasses the expert panel entirely.
2. **Expert Evaluation (Borderline Cases)**: If the screening rating is borderline (`[60, 75]`), the three expert personas (Security, SRE, and AI/Developer DX) independently evaluate the resource (using Google Search Grounding to check the live state of the project). They assign an architectural impact score (0100) and write a 12 sentence justification.
3. **Divergence Assessment and Rebuttal**: If the difference between the highest and lowest assigned scores is **$\ge 15$ points**, a debate round is triggered. Each expert receives the scores and justifications of their peers and is asked to defend or revise their score in a rebuttal round.
4. **Consensus and Synthesis**: The final consensus score is the average of the revised scores of the three personas. A curation synthesis agent compiles the justifications and rebuttals, generating a refined, high-density technical summary (25 sentences) and selecting precise ecosystem tags (e.g., `[DE FACTO STANDARD]`, `[ENTERPRISE-STABLE]`, `[EMERGING]`).
#### 8.3.3. Integration Points
* **Discovery Ingestion**: Hooked into [`src/agentic_curator.py`](src/agentic_curator.py) for new links with borderline initial scores between `70` and `85`.
* **V2 Portal Auditing**: Hooked into [`src/v2_optimizer.py`](src/v2_optimizer.py) during builds for high-impact candidates (`[DE FACTO STANDARD]`, `[ENTERPRISE-STABLE]`) or borderline candidates (34 stars).
* **Discovery Ingestion**: Hooked into [`src/agentic_curator.py`](src/agentic_curator.py) for new links with borderline initial scores.
* **V2 Portal Auditing**: Hooked into [`src/v2_optimizer.py`](src/v2_optimizer.py) during builds for high-impact candidates or borderline candidates.
* **Persistent Memory Log**: The final consensus score, justifications, rebuttals, and metadata are saved to `src/memory/health_learning.json` under `resolved_debates` to serve as few-shot training examples for future curation runs.
---
@@ -1173,7 +1234,11 @@ To maintain transparency and ease of navigation, all key configuration, database
- **Health Check Logic:** [`src/intelligent_health_checker.py`](src/intelligent_health_checker.py) - Link rot prevention and canonical updates.
- **Twikit Ingestion:** [`src/ingestion_twikit.py`](src/ingestion_twikit.py) - X.com scraping and account rotation logic.
- **Backup Ingestion:** [`src/ingestion_backup.py`](src/ingestion_backup.py) - Manual and historical JSON data processing.
- **Discovery Engine:** [`src/autonomous_discovery.py`](src/autonomous_discovery.py) - Multi-source technical news extraction.
- **Discovery Engine:** [`src/autonomous_discovery.py`](src/autonomous_discovery.py) - Multi-source technical news extraction (14 GitHub search queries).
- **News Digest Engine:** [`src/news_digest.py`](src/news_digest.py) - AI-powered temporal digest across 26 categories with Gemini ranking (3/6/12 months).
- **Enrichment Pipeline:** [`src/enrichment.py`](src/enrichment.py) - CNCF Landscape integration, GitHub activity enrichment, and license change detection.
- **Deduplication Engine:** [`src/dedup.py`](src/dedup.py) - URL normalization, content-hash, and title-similarity dedup (85% threshold).
- **Backfill Utility:** [`scripts/backfill_discovered_at.py`](scripts/backfill_discovered_at.py) - One-shot `discovered_at` population for existing entries.
- **Gemini Utils:** [`src/gemini_utils.py`](src/gemini_utils.py) - AI model discovery, rate limiting, and session tracking.
- **Markdown Logic:** [`src/markdown_ast.py`](src/markdown_ast.py) - Sophisticated parsing of repository content.
- **Observability:** [`src/logger.py`](src/logger.py) | [`src/report_generator.py`](src/report_generator.py) - Execution transparency and visual reporting.
+19615
View File
File diff suppressed because it is too large Load Diff
+282319 -151867
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block announce %}
<strong>New:</strong> <a href="./tech-digest/">Intelligence Digest</a> — AI-curated trending resources across 26 categories
{% endblock %}
+193 -2
View File
@@ -3,6 +3,15 @@
* Color Palette: Deep Space Black & Neon Cyan
*/
html {
scroll-behavior: smooth;
}
.md-content {
max-width: 1600px;
margin: 0 auto;
}
:root {
/* LIGHT MODE - Modern, Crisp, High Contrast */
--md-primary-fg-color: #09090b; /* Zinc 950 */
@@ -807,6 +816,13 @@ a {
contain-intrinsic-size: auto 500px;
}
/* Defer rendering of large resource lists in category pages */
.md-typeset > ul,
.md-typeset > ol {
content-visibility: auto;
contain-intrinsic-size: auto 200px;
}
/* Collapsible tag lists on the tags index page */
.v2-tag-section details {
border: 1px solid rgba(0, 0, 0, 0.08);
@@ -908,13 +924,188 @@ input[type="text"] {
/* Coarse touch pointer optimizations (Mobile AA targets) */
@media (pointer: coarse) {
.md-tag,
.md-button,
.md-tag,
.md-button,
.v2-tag-section summary {
min-block-size: 44px;
}
}
/* ---------------------------------------------------- */
/* ANNOUNCEMENT BAR */
/* ---------------------------------------------------- */
.md-banner {
background: linear-gradient(135deg, #0ea5e9, #22d3ee);
color: #ffffff;
font-weight: 500;
text-align: center;
padding: 8px 16px;
}
.md-banner a {
color: #ffffff;
text-decoration: underline;
font-weight: 700;
}
[data-md-color-scheme="slate"] .md-banner {
background: linear-gradient(135deg, #0284c7, #06b6d4);
}
/* ---------------------------------------------------- */
/* TRENDING NOW — NEWS DIGEST CARDS */
/* ---------------------------------------------------- */
.trending-section {
margin: 32px 0;
padding: 24px;
border-radius: 16px;
border: 1px solid rgba(14, 165, 233, 0.15);
background: linear-gradient(180deg, rgba(14, 165, 233, 0.03) 0%, transparent 100%);
}
[data-md-color-scheme="slate"] .trending-section {
border-color: rgba(34, 211, 238, 0.15);
background: linear-gradient(180deg, rgba(34, 211, 238, 0.05) 0%, transparent 100%);
}
.trending-section__title {
font-size: 1.4em;
font-weight: 700;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.trending-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
margin: 16px 0;
}
.trending-card {
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
padding: 16px 20px;
background: var(--md-primary-bg-color--light);
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
.trending-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(14, 165, 233, 0.12);
}
[data-md-color-scheme="slate"] .trending-card {
border-color: rgba(255, 255, 255, 0.08);
background: rgba(24, 24, 27, 0.4);
}
[data-md-color-scheme="slate"] .trending-card:hover {
box-shadow: 0 8px 24px rgba(34, 211, 238, 0.12);
}
.trending-card__category {
font-size: 0.75em;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--md-accent-fg-color);
margin-bottom: 8px;
}
.trending-card__title {
font-size: 0.95em;
font-weight: 600;
margin-bottom: 6px;
line-height: 1.3;
}
.trending-card__title a {
color: inherit;
text-decoration: none;
}
.trending-card__title a:hover {
color: var(--md-accent-fg-color);
}
.trending-card__meta {
font-size: 0.8em;
color: var(--md-primary-fg-color--dark);
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.trending-card__why {
font-size: 0.82em;
line-height: 1.45;
color: var(--md-primary-fg-color--light);
}
.trending-card__impact {
position: absolute;
top: 12px;
right: 12px;
font-size: 0.75em;
font-weight: 700;
padding: 2px 8px;
border-radius: 6px;
}
.trending-card__impact--critical {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.trending-card__impact--high {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.trending-card__impact--medium {
background: rgba(14, 165, 233, 0.15);
color: #0ea5e9;
}
/* Digest link cards (CTA to full digest pages) */
.digest-links {
display: flex;
gap: 16px;
margin-top: 20px;
flex-wrap: wrap;
}
.digest-link-card {
flex: 1;
min-width: 240px;
padding: 16px 24px;
border-radius: 12px;
border: 2px solid var(--md-accent-fg-color);
text-align: center;
text-decoration: none;
color: inherit;
font-weight: 600;
transition: all 0.2s ease;
}
.digest-link-card:hover {
background: var(--md-accent-fg-color);
color: #ffffff;
transform: translateY(-2px);
}
[data-md-color-scheme="slate"] .digest-link-card:hover {
color: #09090b;
}
+4 -3
View File
@@ -13,9 +13,10 @@ document.addEventListener("DOMContentLoaded", function () {
// Do not show on the homepage, video hub index page, or technical tags index page (performance)
const h1 = contentArea.querySelector("h1");
if (h1 && (
h1.textContent.includes("Nubenetes Elite Portal (V2)") ||
h1.textContent.includes("Agentic Video Hub") ||
h1.textContent.includes("Technical Tags Index")
h1.textContent.includes("Nubenetes Elite Portal (V2)") ||
h1.textContent.includes("Agentic Video Hub") ||
h1.textContent.includes("Technical Tags Index") ||
h1.textContent.includes("Intelligence Digest")
)) {
return;
}
+1
View File
@@ -4,3 +4,4 @@ yt-dlp
youtube-transcript-api
mkdocs-redirects>=1.2.3
mkdocs-minify-plugin>=0.8.0
mkdocs-rss-plugin>=1.15.0
+59
View File
@@ -0,0 +1,59 @@
"""
Backfill script: adds discovered_at to inventory entries that lack it.
Priority: gh_pushed > last_checked > year > default "2024-01-01T00:00:00"
Run once: python -m scripts.backfill_discovered_at
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from datetime import datetime
from src.inventory_manager import load_inventory, save_inventory
from src.config import MADRID_TZ
def backfill():
inv = load_inventory()
updated = 0
total = 0
for url, entry in inv.items():
if not isinstance(entry, dict):
continue
total += 1
if entry.get("discovered_at"):
continue
discovered = None
gh_pushed = entry.get("gh_pushed")
if gh_pushed and isinstance(gh_pushed, str) and len(gh_pushed) >= 10:
discovered = gh_pushed
if not discovered:
last_checked = entry.get("last_checked")
if isinstance(last_checked, (int, float)) and last_checked > 0:
try:
discovered = datetime.fromtimestamp(last_checked, tz=MADRID_TZ).isoformat()
except (OSError, ValueError):
pass
if not discovered:
year = entry.get("year", "")
if isinstance(year, str) and year.isdigit() and len(year) == 4:
discovered = f"{year}-06-01T00:00:00+02:00"
elif isinstance(year, int):
discovered = f"{year}-06-01T00:00:00+02:00"
if not discovered:
discovered = "2024-01-01T00:00:00+02:00"
entry["discovered_at"] = discovered
updated += 1
print(f"Backfill complete: {updated}/{total} entries updated with discovered_at")
save_inventory(inv)
if __name__ == "__main__":
backfill()
+34 -17579
View File
File diff suppressed because it is too large Load Diff
+21 -6
View File
@@ -38,7 +38,8 @@ async def _get_github_activity(url: str) -> Dict:
"gh_pushed": data.get("pushed_at"),
"gh_license": data.get("license", {}).get("spdx_id", "N/A")
}
except: pass
except Exception as e:
log_event(f"[WARN] fetch GitHub activity for {url}: {str(e)[:100]}")
return {}
async def _deep_fetch_content(url: str) -> Tuple[str, Dict]:
@@ -61,7 +62,8 @@ async def _deep_fetch_content(url: str) -> Tuple[str, Dict]:
img_match = re.search(r'meta property="og:image" content="(.*?)"', resp.text)
if img_match: og_image = img_match.group(1)
return resp.text, {"og_image": og_image}
except: pass
except Exception as e:
log_event(f"[WARN] deep fetch content for {url}: {str(e)[:100]}")
return "", {}
async def evaluate_extracted_assets(raw_assets: List[Dict]) -> Dict[str, Dict]:
@@ -76,7 +78,8 @@ async def evaluate_extracted_assets(raw_assets: List[Dict]) -> Dict[str, Dict]:
try:
memory_data = json.load(open(memory_file, "r"))
domain_blacklist = set(memory_data.get("blacklisted_domains", []))
except: pass
except Exception as e:
log_event(f"[WARN] load blacklist from health_learning.json: {str(e)[:100]}")
# 1. Pre-filter
for asset in raw_assets:
@@ -140,7 +143,10 @@ async def evaluate_extracted_assets(raw_assets: List[Dict]) -> Dict[str, Dict]:
"- Assign tags. You MUST include:\n"
" 1. 1 to 2 maturity tags from: [DE FACTO STANDARD], [ENTERPRISE-STABLE], [EMERGING], [GUIDE], [CASE STUDY], [COMMUNITY-TOOL], [LEGACY].\n"
" 2. Fine-grained technical/architectural tags from the content (e.g., [EBPF], [WASM], [GITOPS], [IAC], [SERVICE-MESH], [SERVERLESS], [MLOPS], [DB]). Keep them uppercase and wrapped in brackets.\n"
"Respond ONLY JSON list: [{\"url\": \"...\", \"impact_score\": int, \"reputation_penalty\": bool, \"reputation_summary\": \"...\", \"pub_date\": \"YYYY-MM-DD\", \"primary_category\": \"...\", \"suggested_new_category\": \"...\", \"title\": \"...\", \"desc\": \"...\", \"en_summary\": \"High-density summary...\", \"language\": \"...\", \"type\": \"...\", \"level\": \"...\", \"technical_hierarchy\": [...], \"tags\": [...], \"is_microservice\": bool}, ...]\n\n"
"PHASE 5: COMPANY & GEO CLASSIFICATION\n"
"- Identify 'company': The company/organization that authored or is the primary subject (e.g., 'Google', 'Netflix', 'CNCF', 'Independent').\n"
"- Identify 'geo_region': The HQ region of that company. Use one of: 'americas', 'europe', 'spain', 'asia_pacific', 'global'.\n"
"Respond ONLY JSON list: [{\"url\": \"...\", \"impact_score\": int, \"reputation_penalty\": bool, \"reputation_summary\": \"...\", \"pub_date\": \"YYYY-MM-DD\", \"primary_category\": \"...\", \"suggested_new_category\": \"...\", \"title\": \"...\", \"desc\": \"...\", \"en_summary\": \"High-density summary...\", \"language\": \"...\", \"type\": \"...\", \"level\": \"...\", \"technical_hierarchy\": [...], \"tags\": [...], \"is_microservice\": bool, \"company\": \"...\", \"geo_region\": \"...\"}, ...]\n\n"
"RESOURCES:\n" + "\n".join([f"- {d['asset']['url']}: (MVQ Penalty: {d['mvq_penalty']}) {d['content']}" for d in batch_data])
)
@@ -205,9 +211,16 @@ async def evaluate_extracted_assets(raw_assets: List[Dict]) -> Dict[str, Dict]:
"reputation_status": "Vetted" if not data.get("reputation_penalty") else "Suspicious",
"reputation_summary": data.get("reputation_summary", ""),
"source_provenance": d["asset"].get("source_type", "Social"), "social_preview_url": d["rich_meta"].get("og_image", ""),
"company": data.get("company", ""), "geo_region": data.get("geo_region", ""),
"category": primary_cat, "status": "online", "last_checked": datetime.now().timestamp(),
"discovered_at": datetime.now(MADRID_TZ).isoformat(),
"last_ai_eval": datetime.now(MADRID_TZ).isoformat(),
"suggested_new_category": data.get("suggested_new_category", ""),
"addition_method": "automatic", **d["gh_meta"]
"addition_method": {
"rss": "rss_ingestion", "GitHub Trending": "github_trending",
"Twitter": "twitter_ingestion", "nubenetes": "manual"
}.get(d["asset"].get("source_type", ""), "automatic"),
**d["gh_meta"]
}
if "youtube.com" in url or "youtu.be" in url:
title_desc = f"{data['title']} {data['desc']}".lower()
@@ -250,7 +263,9 @@ class AgenticCurator:
prompt = "Identify 5 high-quality Cloud Native or K8s engineering blogs or 'Awesome' repos active in 2026. Return ONLY JSON list of URLs."
try:
return await call_gemini_with_retry(prompt, use_grounding=True)
except: return []
except Exception as e:
log_event(f"[WARN] autonomous source discovery: {str(e)[:100]}")
return []
async def decide_smart_injection(self, content: str, asset: Dict) -> str:
# Extract headers from the markdown content
+18 -5
View File
@@ -2,20 +2,31 @@ import aiohttp
import json
import httpx
import re
from src.config import GEMINI_API_KEY, NUBENETES_CATEGORIES
from src.config import GEMINI_API_KEY, NUBENETES_CATEGORIES, GH_TOKEN
from src.gemini_utils import call_gemini_with_retry
from src.logger import log_event
async def fetch_github_trending_cloud_native() -> list[dict]:
queries = [
"topic:kubernetes+stars:>1000",
"topic:mcp-server+stars:>0",
"topic:kubernetes+stars:>1000",
"topic:mcp-server+stars:>0",
"topic:model-context-protocol+stars:>0",
"topic:ai-agents+stars:>50",
"awesome+stars:>1000",
"topic:generative-ai+stars:>500"
"topic:generative-ai+stars:>500",
"topic:devops+stars:>500",
"topic:observability+stars:>200",
"topic:cloud-security+stars:>200",
"topic:terraform+stars:>500",
"topic:database+stars:>500",
"topic:cicd+stars:>200",
"topic:service-mesh+stars:>100",
"topic:platform-engineering+stars:>100",
]
all_repos = []
headers = {'Accept': 'application/vnd.github.v3+json'}
if GH_TOKEN:
headers['Authorization'] = f'token {GH_TOKEN}'
async with aiohttp.ClientSession(headers=headers) as session:
for q in queries:
url = f"https://api.github.com/search/repositories?q={q}&sort=updated&order=desc"
@@ -29,7 +40,9 @@ async def fetch_github_trending_cloud_native() -> list[dict]:
"url": repo['html_url'],
"desc": repo['description'] or "No description provided."
})
except: continue
except Exception as e:
log_event(f"[WARN] GitHub search query '{q}': {str(e)[:100]}")
continue
return all_repos
async def discover_trending_assets() -> list[dict]:
+194
View File
@@ -0,0 +1,194 @@
from __future__ import annotations
import re
import asyncio
from collections import defaultdict
from difflib import SequenceMatcher
from typing import Dict, List, Tuple
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
from src.inventory_manager import load_inventory, save_inventory
from src.logger import log_event
TRACKING_PARAMS = {"utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term",
"ref", "source", "fbclid", "gclid", "mc_cid", "mc_eid", "s", "share"}
def normalize_url_deep(url: str) -> str:
parsed = urlparse(url.strip().lower())
scheme = "https"
netloc = parsed.netloc.removeprefix("www.")
path = parsed.path.rstrip("/") or "/"
params = parse_qs(parsed.query)
clean_params = {k: v for k, v in params.items() if k not in TRACKING_PARAMS}
query = urlencode(clean_params, doseq=True) if clean_params else ""
return urlunparse((scheme, netloc, path, "", query, ""))
def normalize_title(title: str) -> str:
if not title:
return ""
t = title.lower().strip()
t = re.sub(r'^[\w.-]+\.\w{2,}:\s*', '', t)
t = re.sub(r'[^\w\s]', ' ', t)
t = re.sub(r'\s+', ' ', t).strip()
return t
def find_url_duplicates(inventory: Dict) -> List[Tuple[str, str]]:
norm_map = defaultdict(list)
for url in inventory:
if not isinstance(inventory[url], dict):
continue
deep = normalize_url_deep(url)
norm_map[deep].append(url)
duplicates = []
for norm, urls in norm_map.items():
if len(urls) > 1:
for i in range(1, len(urls)):
duplicates.append((urls[0], urls[i]))
return duplicates
def find_hash_duplicates(inventory: Dict) -> List[List[str]]:
hash_map = defaultdict(list)
for url, entry in inventory.items():
if not isinstance(entry, dict):
continue
ch = entry.get("content_hash")
if ch and ch != "N/A":
hash_map[ch].append(url)
return [urls for urls in hash_map.values() if len(urls) > 1]
def find_title_duplicates(inventory: Dict, threshold: float = 0.85) -> List[Tuple[str, str, float]]:
entries = []
for url, entry in inventory.items():
if not isinstance(entry, dict):
continue
title = entry.get("title", "")
norm = normalize_title(title)
if len(norm) < 10:
continue
entries.append((url, norm, entry.get("stars", 0)))
log_event(f"[Dedup] Building title index for {len(entries)} entries...")
prefix_groups = defaultdict(list)
for url, norm, stars in entries:
words = norm.split()
prefix = " ".join(words[:3]) if len(words) >= 3 else norm
prefix_groups[prefix].append((url, norm, stars))
duplicates = []
checked = 0
for prefix, group in prefix_groups.items():
if len(group) < 2:
continue
for i in range(len(group)):
for j in range(i + 1, len(group)):
url1, norm1, stars1 = group[i]
url2, norm2, stars2 = group[j]
if stars1 >= 4 and stars2 >= 4:
continue
ratio = SequenceMatcher(None, norm1, norm2).ratio()
if ratio >= threshold:
duplicates.append((url1, url2, ratio))
checked += 1
if checked % 500 == 0:
log_event(f"[Dedup] Checked {checked}/{len(prefix_groups)} prefix groups...")
log_event(f"[Dedup] Title scan complete: {len(duplicates)} potential duplicates found")
return duplicates
def _entry_score(entry: Dict) -> Tuple:
return (
entry.get("stars", 0),
1 if entry.get("ai_summary") else 0,
1 if entry.get("hierarchy") else 0,
len(entry.get("tags", [])),
-len(str(entry.get("url", "")))
)
def resolve_duplicates(inventory: Dict, duplicate_pairs: List[Tuple[str, str, float]]) -> int:
resolved = 0
seen = set()
for url1, url2, score in sorted(duplicate_pairs, key=lambda x: -x[2]):
if url1 in seen or url2 in seen:
continue
entry1 = inventory.get(url1, {})
entry2 = inventory.get(url2, {})
if not isinstance(entry1, dict) or not isinstance(entry2, dict):
continue
score1 = _entry_score(entry1)
score2 = _entry_score(entry2)
if score1 >= score2:
winner, loser = url1, url2
else:
winner, loser = url2, url1
inventory[loser]["status"] = "duplicate"
inventory[loser]["duplicate_of"] = winner
seen.add(loser)
resolved += 1
return resolved
async def run_dedup(dry_run: bool = True) -> Dict:
log_event("STARTING DEDUPLICATION SCAN", section_break=True)
inventory = load_inventory()
url_dups = find_url_duplicates(inventory)
log_event(f"[Dedup] URL duplicates: {len(url_dups)}")
hash_groups = find_hash_duplicates(inventory)
hash_dups = []
for group in hash_groups:
for i in range(1, len(group)):
hash_dups.append((group[0], group[i], 1.0))
log_event(f"[Dedup] Content hash duplicates: {len(hash_dups)}")
title_dups = find_title_duplicates(inventory)
all_dups = [(u1, u2, 1.0) for u1, u2 in url_dups] + hash_dups + title_dups
unique_pairs = {}
for u1, u2, s in all_dups:
key = tuple(sorted([u1, u2]))
if key not in unique_pairs or s > unique_pairs[key]:
unique_pairs[key] = s
deduped_pairs = [(k[0], k[1], v) for k, v in unique_pairs.items()]
stats = {
"url_duplicates": len(url_dups),
"hash_duplicates": len(hash_dups),
"title_duplicates": len(title_dups),
"total_unique_pairs": len(deduped_pairs),
}
if dry_run:
log_event(f"[Dedup] DRY RUN — {len(deduped_pairs)} duplicates found, no changes made")
for u1, u2, score in sorted(deduped_pairs, key=lambda x: -x[2])[:20]:
t1 = inventory.get(u1, {}).get("title", "?")[:60]
t2 = inventory.get(u2, {}).get("title", "?")[:60]
log_event(f" [{score:.0%}] {t1} <-> {t2}")
else:
resolved = resolve_duplicates(inventory, deduped_pairs)
stats["resolved"] = resolved
save_inventory(inventory)
log_event(f"[Dedup] Resolved {resolved} duplicates")
log_event(f"DEDUP COMPLETE: {stats}")
return stats
if __name__ == "__main__":
asyncio.run(run_dedup(dry_run=True))
+3 -4
View File
@@ -5,6 +5,7 @@ import asyncio
import httpx
from src.logger import log_event
from src.gemini_utils import call_gemini_with_retry, fetch_youtube_metadata
from src.inventory_manager import load_inventory, save_inventory
INVENTORY_PATH = "data/inventory.yaml"
@@ -72,8 +73,7 @@ async def main():
force_enrich = os.getenv("FORCE_ENRICH", "false").lower() == "true"
with open(INVENTORY_PATH, "r") as f:
inventory = yaml.safe_load(f)
inventory = load_inventory()
video_urls = [u for u, e in inventory.items() if e.get("is_featured_video")]
@@ -99,8 +99,7 @@ async def main():
await asyncio.gather(*batch)
# Incremental Persistence: Save after each batch
with open(INVENTORY_PATH, "w") as f:
yaml.dump(inventory, f, sort_keys=False, allow_unicode=True)
save_inventory(inventory)
log_event(f" [💾] Saved progress: {min(i + batch_size, len(tasks))}/{len(tasks)} videos.")
if i + batch_size < len(tasks):
+395
View File
@@ -0,0 +1,395 @@
from __future__ import annotations
import asyncio
import re
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import httpx
from src.config import GH_TOKEN, MADRID_TZ
from src.logger import log_event
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
CNCF_LANDSCAPE_URL = "https://landscape.cncf.io/api/items"
GITHUB_API_BASE = "https://api.github.com"
GITHUB_RATE_DELAY = 0.75 # seconds between GitHub API calls to stay under 5000/hr
MAX_REPOS_DEFAULT = 500
ACTIVITY_STALENESS_DAYS = 30
# Community health thresholds
HEALTH_ACTIVE = 50
HEALTH_HEALTHY = 10
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _github_headers() -> Dict[str, str]:
"""Build GitHub API request headers with optional auth."""
headers = {"Accept": "application/vnd.github.v3+json"}
if GH_TOKEN:
headers["Authorization"] = f"token {GH_TOKEN}"
return headers
def _parse_github_repo(url: str) -> Optional[str]:
"""Extract 'owner/repo' from a GitHub URL. Returns None if not a GitHub URL."""
match = re.search(r"github\.com/([^/]+/[^/]+)", url)
if not match:
return None
repo = match.group(1).split("#")[0].split("?")[0].rstrip("/")
# Strip trailing .git if present
if repo.endswith(".git"):
repo = repo[:-4]
return repo
def _normalize_repo_url(url: str) -> str:
"""Normalize a GitHub repo URL for comparison (lowercase, no trailing slash)."""
url = url.lower().rstrip("/")
if url.endswith(".git"):
url = url[:-4]
# Strip protocol
url = re.sub(r"^https?://", "", url)
return url
def _is_activity_stale(entry: Dict) -> bool:
"""Check whether an entry's activity data is older than ACTIVITY_STALENESS_DAYS."""
checked = entry.get("gh_activity_checked")
if not checked:
return True
try:
checked_dt = datetime.fromisoformat(checked)
return datetime.now(MADRID_TZ) - checked_dt > timedelta(days=ACTIVITY_STALENESS_DAYS)
except (ValueError, TypeError):
return True
# ---------------------------------------------------------------------------
# Module 1: CNCF Landscape Status
# ---------------------------------------------------------------------------
async def fetch_cncf_landscape() -> Dict[str, str]:
"""Fetch CNCF project graduation status.
Returns dict mapping repo_url (normalized) -> maturity
("sandbox" | "incubating" | "graduated" | "archived").
"""
result: Dict[str, str] = {}
async with httpx.AsyncClient(timeout=30.0) as client:
try:
resp = await client.get(CNCF_LANDSCAPE_URL)
resp.raise_for_status()
items = resp.json()
except Exception as e:
log_event(f"[WARN] Failed to fetch CNCF landscape: {str(e)[:200]}")
return result
if not isinstance(items, list):
log_event("[WARN] CNCF landscape response is not a list; skipping")
return result
for item in items:
repo_url = item.get("repo_url") or ""
project = item.get("project")
if not repo_url or not project:
continue
maturity = project if isinstance(project, str) else project.get("maturity", "")
if not maturity:
continue
maturity = maturity.lower()
if maturity not in ("sandbox", "incubating", "graduated", "archived"):
continue
normalized = _normalize_repo_url(repo_url)
result[normalized] = maturity
log_event(f"[CNCF] Fetched {len(result)} projects from CNCF landscape")
return result
async def enrich_cncf_status(inventory: Dict) -> int:
"""Update inventory entries with cncf_status field.
If an entry maps to a CNCF-graduated project and lacks the
``[DE FACTO STANDARD]`` tag, the tag is appended.
Returns count of entries updated.
"""
landscape = await fetch_cncf_landscape()
if not landscape:
return 0
updated = 0
for url, entry in inventory.items():
if url.startswith("INTRO:"):
continue
normalized = _normalize_repo_url(url)
maturity = landscape.get(normalized)
if not maturity:
continue
old_status = entry.get("cncf_status")
entry["cncf_status"] = maturity
if old_status != maturity:
updated += 1
# Auto-tag graduated projects
if maturity == "graduated":
tags = entry.get("tags")
if isinstance(tags, list) and "[DE FACTO STANDARD]" not in tags:
tags.append("[DE FACTO STANDARD]")
entry["tags"] = tags
log_event(f"[CNCF] Enriched {updated} inventory entries with CNCF status")
return updated
# ---------------------------------------------------------------------------
# Module 2: Social Signal Enrichment (GitHub Activity)
# ---------------------------------------------------------------------------
async def _fetch_repo_activity(
client: httpx.AsyncClient,
owner_repo: str,
) -> Tuple[int, int]:
"""Fetch open issue count and recent open PR count for a single repo.
Returns (open_issues_count, open_prs_count).
"""
headers = _github_headers()
open_issues = 0
open_prs = 0
# Fetch repo-level stats (open_issues_count includes PRs on GitHub)
try:
resp = await client.get(
f"{GITHUB_API_BASE}/repos/{owner_repo}",
headers=headers,
timeout=15.0,
)
if resp.status_code == 200:
data = resp.json()
open_issues = data.get("open_issues_count", 0)
except Exception as e:
log_event(f"[WARN] GitHub repo fetch failed for {owner_repo}: {str(e)[:120]}")
await asyncio.sleep(GITHUB_RATE_DELAY)
# Fetch open PRs (page 1 only — we use total_count from search or headers)
try:
resp = await client.get(
f"{GITHUB_API_BASE}/repos/{owner_repo}/pulls",
headers=headers,
params={"state": "open", "sort": "created", "per_page": 1},
timeout=15.0,
)
if resp.status_code == 200:
# The total count of open PRs is not directly in the body for the
# list endpoint, but we can parse the Link header for the last page.
# As a simpler approach, the body is a list; if it has items the repo
# has open PRs. We use the repo-level open_issues_count as the
# combined metric (GitHub counts PRs as issues).
pr_data = resp.json()
if isinstance(pr_data, list) and len(pr_data) > 0:
# Parse Link header for total pages
link_header = resp.headers.get("Link", "")
last_match = re.search(r'page=(\d+)>;\s*rel="last"', link_header)
open_prs = int(last_match.group(1)) if last_match else len(pr_data)
except Exception as e:
log_event(f"[WARN] GitHub PRs fetch failed for {owner_repo}: {str(e)[:120]}")
await asyncio.sleep(GITHUB_RATE_DELAY)
return open_issues, open_prs
def _classify_health(total_activity: int) -> str:
"""Classify community health based on combined issue + PR activity."""
if total_activity > HEALTH_ACTIVE:
return "active"
if total_activity >= HEALTH_HEALTHY:
return "healthy"
if total_activity > 0:
return "low"
return "dormant"
async def enrich_github_activity(inventory: Dict, max_repos: int = MAX_REPOS_DEFAULT) -> int:
"""Fetch recent issue/PR velocity for GitHub repos in the inventory.
Adds fields:
- ``gh_open_issues_30d``
- ``gh_open_prs_30d``
- ``gh_community_health`` ("active" | "healthy" | "low" | "dormant")
- ``gh_activity_checked`` (ISO timestamp)
Skips entries whose ``gh_activity_checked`` is less than 30 days old.
Processes at most *max_repos* repos per run.
Returns count of entries enriched.
"""
candidates: List[Tuple[str, str]] = [] # (url, owner/repo)
for url, entry in inventory.items():
if url.startswith("INTRO:"):
continue
repo = _parse_github_repo(url)
if not repo:
continue
if not _is_activity_stale(entry):
continue
candidates.append((url, repo))
if len(candidates) >= max_repos:
break
if not candidates:
log_event("[Activity] No GitHub repos require activity enrichment")
return 0
log_event(f"[Activity] Enriching {len(candidates)} GitHub repos (max {max_repos})")
enriched = 0
async with httpx.AsyncClient() as client:
for url, owner_repo in candidates:
try:
open_issues, open_prs = await _fetch_repo_activity(client, owner_repo)
total = open_issues + open_prs
health = _classify_health(total)
entry = inventory[url]
entry["gh_open_issues_30d"] = open_issues
entry["gh_open_prs_30d"] = open_prs
entry["gh_community_health"] = health
entry["gh_activity_checked"] = datetime.now(MADRID_TZ).isoformat()
enriched += 1
except Exception as e:
log_event(f"[WARN] Activity enrichment failed for {owner_repo}: {str(e)[:150]}")
log_event(f"[Activity] Enriched {enriched}/{len(candidates)} repos with community health data")
return enriched
# ---------------------------------------------------------------------------
# Module 3: License Change Detection
# ---------------------------------------------------------------------------
async def _fetch_current_license(
client: httpx.AsyncClient,
owner_repo: str,
) -> Optional[str]:
"""Fetch the current SPDX license identifier for a GitHub repo."""
headers = _github_headers()
try:
resp = await client.get(
f"{GITHUB_API_BASE}/repos/{owner_repo}",
headers=headers,
timeout=15.0,
)
if resp.status_code == 200:
data = resp.json()
lic = data.get("license")
if isinstance(lic, dict):
return lic.get("spdx_id", "N/A")
return "N/A"
elif resp.status_code == 404:
return None # repo not found / deleted
except Exception as e:
log_event(f"[WARN] License fetch failed for {owner_repo}: {str(e)[:120]}")
return None
async def detect_license_changes(inventory: Dict) -> List[Dict]:
"""Compare current gh_license with stored license, flag changes.
For each GitHub repo in inventory that has ``gh_license``:
- Fetches the current license from the GitHub API.
- Compares with the stored ``gh_license``.
- If different, updates the entry: sets ``gh_license`` to the new value
and stores the old value in ``gh_license_previous``.
Returns list of ``{url, old_license, new_license, title}`` dicts.
"""
candidates: List[Tuple[str, str, Dict]] = [] # (url, owner/repo, entry)
for url, entry in inventory.items():
if url.startswith("INTRO:"):
continue
if not entry.get("gh_license") or entry["gh_license"] == "N/A":
continue
repo = _parse_github_repo(url)
if not repo:
continue
candidates.append((url, repo, entry))
if not candidates:
log_event("[License] No repos with stored licenses to check")
return []
log_event(f"[License] Checking {len(candidates)} repos for license changes")
changes: List[Dict] = []
async with httpx.AsyncClient() as client:
for url, owner_repo, entry in candidates:
try:
current = await _fetch_current_license(client, owner_repo)
if current is None:
# Repo not found or API error — skip
continue
stored = entry.get("gh_license", "N/A")
if current != stored:
change_record = {
"url": url,
"old_license": stored,
"new_license": current,
"title": entry.get("title", url),
}
changes.append(change_record)
entry["gh_license_previous"] = stored
entry["gh_license"] = current
log_event(
f"[License] Change detected: {owner_repo} "
f"{stored} -> {current}"
)
except Exception as e:
log_event(f"[WARN] License check failed for {owner_repo}: {str(e)[:150]}")
await asyncio.sleep(GITHUB_RATE_DELAY)
log_event(f"[License] Detected {len(changes)} license change(s)")
return changes
# ---------------------------------------------------------------------------
# Full Enrichment Pipeline
# ---------------------------------------------------------------------------
async def run_enrichment():
"""Full enrichment pipeline: CNCF + GitHub activity + license detection."""
from src.inventory_manager import load_inventory, save_inventory
log_event("ENRICHMENT PIPELINE STARTING", section_break=True)
inventory = load_inventory()
log_event(f"[*] Loaded {len(inventory)} inventory entries")
cncf_count = await enrich_cncf_status(inventory)
activity_count = await enrich_github_activity(inventory)
license_changes = await detect_license_changes(inventory)
save_inventory(inventory)
log_event(
f"Enrichment complete: {cncf_count} CNCF, "
f"{activity_count} activity, "
f"{len(license_changes)} license changes",
section_break=True,
)
return license_changes
if __name__ == "__main__":
asyncio.run(run_enrichment())
+3 -2
View File
@@ -94,10 +94,11 @@ async def fetch_github_metadata(client: httpx.AsyncClient, url: str, sem: asynci
return url, default_meta
from src.inventory_manager import load_inventory
async def run_enrichment():
print("[*] Loading inventory database...")
with open(INVENTORY_PATH, "r") as f:
inventory = yaml.safe_load(f) or {}
inventory = load_inventory()
print(f"[*] Loaded {len(inventory)} total entries.")
+20 -9
View File
@@ -122,7 +122,8 @@ async def discover_optimal_models():
if name not in all_supported: all_supported.append(name)
elif resp.status_code == 429:
log_event(f" [!] Discovery Key is rate-limited (429). Skipping.")
except: pass
except Exception as e:
log_event(f"[WARN] model discovery for key: {str(e)[:100]}")
if not all_supported:
log_event(" [!] Discovery failed. Falling back to safe defaults.")
@@ -136,7 +137,8 @@ async def discover_optimal_models():
try:
version = float(version_match.group(1))
score += version * 50
except: pass
except Exception as e:
log_event(f"[WARN] parse model version for {name}: {str(e)[:100]}")
if "-ultra" in name: score += 100
elif "-pro" in name: score += 50
elif "-flash" in name: score += 25
@@ -169,7 +171,9 @@ class GeminiDiagnostics:
async def resolve_url(url: str) -> str:
shorteners = ['t.co', 'bit.ly', 'buff.ly', 'goo.gl', 'tinyurl.com', 't.ly', 'rb.gy', 'is.gd', 'drp.li', 't.me', 'lnkd.in']
try: domain = url.split("//")[-1].split("/")[0].lower()
except: return url
except Exception as e:
log_event(f"[WARN] parse domain from URL {url[:50]}: {str(e)[:100]}")
return url
final_url, max_hops, current_hop = url, 5, 0
async with httpx.AsyncClient(follow_redirects=True, timeout=8) as client:
while current_hop < max_hops:
@@ -180,7 +184,9 @@ async def resolve_url(url: str) -> str:
new_url = str(resp.url)
if new_url == final_url: break
final_url, current_hop = new_url, current_hop + 1
except: break
except Exception as e:
log_event(f"[WARN] resolve URL hop for {final_url[:50]}: {str(e)[:100]}")
break
# Mandate 34: Prevent multiple trailing slashes using centralized utility
return sanitize_trailing_slashes(final_url)
@@ -217,7 +223,8 @@ async def get_github_activity(url: str) -> Dict:
try:
from src.config import GH_TOKEN
headers = {"Authorization": f"token {GH_TOKEN}"} if GH_TOKEN else {}
except:
except Exception as e:
log_event(f"[WARN] import GH_TOKEN for GitHub activity: {str(e)[:100]}")
headers = {}
try:
@@ -232,7 +239,8 @@ async def get_github_activity(url: str) -> Dict:
"gh_pushed": data.get("pushed_at", "N/A"),
"gh_license": lic_id
}
except: pass
except Exception as e:
log_event(f"[WARN] fetch GitHub activity for {url}: {str(e)[:100]}")
return default_meta
@@ -436,7 +444,8 @@ async def call_gemini_with_retry(prompt: str, response_format: str = "json", max
resp_json = {}
try: resp_json = response.json()
except: pass
except Exception as e:
log_event(f"[WARN] parse Gemini response JSON: {str(e)[:100]}")
usage = resp_json.get("usageMetadata", {})
SESSION_TRACKER.track_call(current_idx, model, response.status_code, usage, role=role)
@@ -452,7 +461,8 @@ async def call_gemini_with_retry(prompt: str, response_format: str = "json", max
try:
data = json.loads(match.group(0))
return data
except: pass
except Exception as e:
log_event(f"[WARN] parse Gemini content JSON for model {model}: {str(e)[:100]}")
# QUALITY UPGRADE: If flash failed parsing, don't give up on the key, try a Pro model
if ("flash" in model or "lite" in model) and any("pro" in m for m in models):
@@ -574,7 +584,8 @@ async def fetch_youtube_metadata(url: str) -> Optional[Dict]:
try:
transcript = YouTubeTranscriptApi.get_transcript(vid, languages=['en', 'es'])
transcript_text = " ".join([t['text'] for t in transcript[:100]])
except: pass
except Exception as e:
log_event(f"[WARN] fetch YouTube transcript for {vid}: {str(e)[:100]}")
full_description = f"{description}\n\n[Transcript Snippet]: {transcript_text}" if transcript_text else description
+6 -3
View File
@@ -21,14 +21,16 @@ class RepositoryController:
try:
file_meta = self.repository.get_contents(file_path, ref=branch_name)
return base64.b64decode(file_meta.content).decode("utf-8")
except:
except Exception as e:
print(f"[WARN] Failed to get file '{file_path}' from branch '{branch_name}': {str(e)[:100]}")
return ""
def apply_historical_chunk(self, updates: dict, next_since: str) -> None:
branch_name = "bot/historical-accumulator"
try:
self.repository.get_branch(branch_name)
except:
except Exception as e:
print(f"[WARN] Branch '{branch_name}' not found, creating: {str(e)[:100]}")
self._create_feature_branch(branch_name)
for file_path, content in updates.items():
@@ -54,7 +56,8 @@ class RepositoryController:
try:
self._create_feature_branch(branch_name)
except:
except Exception as e:
print(f"[WARN] Branch creation failed, retrying with unique suffix: {str(e)[:100]}")
branch_name = f"bot/knowledge-update-{timestamp_slug}-{id(updates)}"
self._create_feature_branch(branch_name)
+4 -3
View File
@@ -88,11 +88,12 @@ class BackupDataExtractor:
def parse_date(x):
try:
return datetime.strptime(x["timestamp"], '%a %b %d %H:%M:%S +0000 %Y')
except:
except Exception as e:
print(f"[WARN] Date parse failed for timestamp: {str(e)[:100]}")
return datetime.min
results.sort(key=parse_date)
except:
pass
except Exception as e:
print(f"[WARN] Sorting results by date failed: {str(e)[:100]}")
self.log_audit("Backup Ingestion", True, f"Total links extracted: {len(results)}")
return results
+6 -4
View File
@@ -52,8 +52,8 @@ class SocialDataExtractor:
try:
from playwright.async_api import async_playwright
import playwright_stealth
except:
self.log_audit("Playwright", False, "Libraries not available.")
except Exception as e:
self.log_audit("Playwright", False, f"Libraries not available: {str(e)[:100]}")
return []
collected_tweets = {}
@@ -74,14 +74,16 @@ class SocialDataExtractor:
for k in ['sameSite', 'storeId', 'id']: c.pop(k, None)
formatted.append(c)
await context.add_cookies(formatted)
except: pass
except Exception as e:
log_event(f"[WARN] Failed to load Twitter cookies: {str(e)[:100]}")
for account in accounts:
page = await context.new_page()
try:
if hasattr(playwright_stealth, 'stealth_async'): await playwright_stealth.stealth_async(page)
elif hasattr(playwright_stealth, 'stealth'): playwright_stealth.stealth(page)
except: pass
except Exception as e:
log_event(f"[WARN] Playwright stealth setup failed: {str(e)[:100]}")
if strategy == "search":
import urllib.parse
+13 -6
View File
@@ -44,7 +44,8 @@ class IntelligentLinkCleaner:
def _load_memory(self) -> Dict:
if os.path.exists(MEMORY_FILE):
try: return json.load(open(MEMORY_FILE, 'r'))
except: pass
except Exception as e:
log_event(f"[WARN] load health learning memory: {str(e)[:100]}")
return {"domains": {}, "known_soft_404_patterns": []}
def _save_memory(self):
@@ -198,8 +199,10 @@ class IntelligentLinkCleaner:
else:
log_event(f" [✨] RESCUED: {u} -> {new_loc}")
check_results[u] = (True, "resurrected", new_loc)
except: pass
except: pass
except Exception as e:
log_event(f"[WARN] verify rescued URL {new_loc[:50]}: {str(e)[:100]}")
except Exception as e:
log_event(f"[WARN] AI rescue batch: {str(e)[:100]}")
# 2.8. Finalize Status
log_event("FINALIZING STATUS AND METRICS...", section_break=True)
@@ -309,7 +312,8 @@ class IntelligentLinkCleaner:
log_event(f" [⚖️] LICENSE ALERT: {url} -> {new_lic}")
entry["status"] = "review_required"
entry["gh_license"] = new_lic
except: pass
except Exception as e:
log_event(f"[WARN] license guard check for {url}: {str(e)[:100]}")
if final_url != url:
u_p = url.split("://")[-1].rstrip("/"); f_p = final_url.split("://")[-1].rstrip("/")
@@ -325,12 +329,15 @@ class IntelligentLinkCleaner:
h = url.replace("/master/", "/main/")
try:
if (await client.get(h)).status_code < 400: return True, "healed", h
except: pass
except Exception as e:
log_event(f"[WARN] heal master->main for {url}: {str(e)[:100]}")
m = re.search(r'(https?://github\.com/[^/]+/[^/]+)', url)
if m and (await client.get(m.group(1))).status_code < 400: return True, "consolidated", m.group(1)
return False, "404", None
return True, f"Soft Block {resp.status_code}", None
except: return True, "Error", None
except Exception as e:
log_event(f"[WARN] URL check logic for {url}: {str(e)[:100]}")
return True, "Error", None
async def prune_orphaned_metadata(self):
valid_map = {}
+169 -17
View File
@@ -1,46 +1,198 @@
import os
import json
import sqlite3
import yaml
from typing import Dict
INVENTORY_PATH = "data/inventory.yaml"
SQL_PATH = "data/inventory.sql"
try:
from yaml import CSafeLoader as Loader, CSafeDumper as Dumper
except ImportError:
from yaml import SafeLoader as Loader, SafeDumper as Dumper
def load_inventory(shard_file: str = None) -> Dict:
"""
Loads the entire inventory from a single YAML file.
Legacy/Fast-Track standard restored: Zero-sharding complexity.
Loads the entire inventory.
Option 3: Imports inventory.sql to temporary SQLite database in-memory,
queries the database to reconstruct the Python dictionary, and returns it.
Falls back to inventory.yaml if SQL file is not present.
"""
if os.path.exists(INVENTORY_PATH):
if not os.path.exists(SQL_PATH) and os.path.exists(INVENTORY_PATH):
try:
with open(INVENTORY_PATH, "r") as file:
return yaml.safe_load(file) or {}
except: pass
return {}
with open(INVENTORY_PATH, "r", encoding="utf-8") as file:
return yaml.load(file, Loader=Loader) or {}
except Exception as e:
pass
return {}
if not os.path.exists(SQL_PATH):
return {}
conn = sqlite3.connect(":memory:")
try:
with open(SQL_PATH, "r", encoding="utf-8") as f:
conn.executescript(f.read())
except Exception as e:
conn.close()
# Fallback to YAML if SQL import fails
if os.path.exists(INVENTORY_PATH):
try:
with open(INVENTORY_PATH, "r", encoding="utf-8") as file:
return yaml.load(file, Loader=Loader) or {}
except Exception as e:
print(f"[WARN] YAML fallback load failed: {str(e)[:100]}")
return {}
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM resources")
rows = cursor.fetchall()
col_names = [description[0] for description in cursor.description]
except Exception as e:
conn.close()
return {}
inv = {}
for row in rows:
record = dict(zip(col_names, row))
url = record.pop("url")
# Deserialize JSON lists/dicts
for json_field in ["hierarchy", "tags", "v1_locations", "v2_locations", "youtube_mosaic", "extra_metadata"]:
val = record.get(json_field)
if val:
try:
record[json_field] = json.loads(val)
except Exception as e:
print(f"[WARN] JSON parse failed for field '{json_field}': {str(e)[:100]}")
record[json_field] = [] if json_field not in ["youtube_mosaic", "extra_metadata"] else {}
else:
record[json_field] = [] if json_field not in ["youtube_mosaic", "extra_metadata"] else {}
# Merge extra_metadata keys back into the record dictionary
extra = record.pop("extra_metadata", {})
if isinstance(extra, dict):
record.update(extra)
# Restore types
if record.get("is_microservice") is not None:
record["is_microservice"] = bool(record["is_microservice"])
if record.get("needs_ai_refresh") is not None:
record["needs_ai_refresh"] = bool(record["needs_ai_refresh"])
inv[url] = record
conn.close()
return inv
def save_inventory(inv: Dict, shard_file: str = None):
"""
Saves the entire inventory to a single YAML file.
Saves the entire inventory.
Option 3: Creates an in-memory SQLite table, populates it,
and exports it back to inventory.sql.
Also dual-saves a backup to inventory.yaml using fast CDumper.
"""
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS resources (
url TEXT PRIMARY KEY,
title TEXT,
description TEXT,
year TEXT,
stars INTEGER,
ai_summary TEXT,
language TEXT,
resource_type TEXT,
complexity TEXT,
is_microservice BOOLEAN,
status TEXT,
addition_method TEXT,
content_hash TEXT,
health_score REAL,
last_checked REAL,
needs_ai_refresh BOOLEAN,
discovered_at TEXT,
last_ai_eval TEXT,
company TEXT,
geo_region TEXT,
hierarchy TEXT,
tags TEXT,
v1_locations TEXT,
v2_locations TEXT,
youtube_mosaic TEXT,
extra_metadata TEXT
);
""")
columns = [
"url", "title", "description", "year", "stars", "ai_summary", "language",
"resource_type", "complexity", "is_microservice", "status", "addition_method",
"content_hash", "health_score", "last_checked", "needs_ai_refresh",
"discovered_at", "last_ai_eval", "company", "geo_region",
"hierarchy", "tags", "v1_locations", "v2_locations", "youtube_mosaic", "extra_metadata"
]
for url, entry in inv.items():
if not isinstance(entry, dict):
continue
record = {col: entry.get(col) for col in columns if col not in ["hierarchy", "tags", "v1_locations", "v2_locations", "youtube_mosaic", "extra_metadata"]}
record["url"] = url
# Serialize lists/dicts
record["hierarchy"] = json.dumps(entry.get("hierarchy", []))
record["tags"] = json.dumps(entry.get("tags", []))
record["v1_locations"] = json.dumps(entry.get("v1_locations", []))
record["v2_locations"] = json.dumps(entry.get("v2_locations", []))
record["youtube_mosaic"] = json.dumps(entry.get("youtube_mosaic", {}))
# Pull arbitrary extra fields
extra = {}
for k, v in entry.items():
if k not in columns:
extra[k] = v
record["extra_metadata"] = json.dumps(extra)
# Conversions
record["is_microservice"] = 1 if record.get("is_microservice") else 0
record["needs_ai_refresh"] = 1 if record.get("needs_ai_refresh") else 0
placeholders = ", ".join(["?"] * len(columns))
values = [record[col] for col in columns]
cursor.execute(f"INSERT OR REPLACE INTO resources ({', '.join(columns)}) VALUES ({placeholders})", values)
conn.commit()
# Dump to SQL
os.makedirs(os.path.dirname(SQL_PATH), exist_ok=True)
with open(SQL_PATH, "w", encoding="utf-8") as f:
for line in conn.iterdump():
f.write(f"{line}\n")
conn.close()
# Dual-Save to YAML (Fast C-Dumper)
os.makedirs(os.path.dirname(INVENTORY_PATH), exist_ok=True)
with open(INVENTORY_PATH, "w") as file:
yaml.dump(inv, file, sort_keys=False, allow_unicode=True)
with open(INVENTORY_PATH, "w", encoding="utf-8") as file:
yaml.dump(inv, file, Dumper=Dumper, sort_keys=False, allow_unicode=True)
def get_shard_name(url: str) -> str:
# Kept for backward compatibility but unused in single-file mode
return "inventory.yaml"
def update_inventory_entry(inventory: Dict, norm_url: str, new_data: Dict):
"""
Updates an inventory entry by merging new_data with existing data,
preserving metadata keys like 'youtube_mosaic' if they are not in new_data.
"""
if norm_url not in inventory:
inventory[norm_url] = {}
existing = inventory[norm_url]
if isinstance(existing, dict):
merged = existing.copy()
existing_discovered = existing.get("discovered_at")
merged.update(new_data)
if existing_discovered:
merged["discovered_at"] = existing_discovered
inventory[norm_url] = merged
else:
inventory[norm_url] = new_data
+2 -2
View File
@@ -32,5 +32,5 @@ def _write_to_file(message: str):
os.makedirs(os.path.dirname(DEFAULT_LOG_PATH), exist_ok=True)
with open(DEFAULT_LOG_PATH, "a") as f:
f.write(message + "\n")
except:
pass
except Exception as e:
print(f"[WARN] write to log file: {str(e)[:100]}")
+34 -20
View File
@@ -4,6 +4,10 @@ import os
import json
import re
import yaml
try:
from yaml import CSafeLoader as Loader
except ImportError:
from yaml import SafeLoader as Loader
import httpx
from urllib.parse import urlparse
from datetime import datetime, timedelta
@@ -55,7 +59,8 @@ async def master_orchestrator():
since_date = until_date - timedelta(days=days)
log_event(f"[*] Mode: Relative range (Last {days} days) -> {since_date.date()}")
is_historical = False # Force normal mode for relative range
except:
except Exception as e:
log_event(f"[WARN] parse CURATION_DAYS_BACK: {str(e)[:100]}")
since_date = get_last_date()
elif is_historical:
# DEFAULT START DATE: 2026-05-15 (as requested)
@@ -82,7 +87,8 @@ async def master_orchestrator():
try:
since_date = datetime.fromisoformat(env_start).replace(tzinfo=MADRID_TZ)
log_event(f"[*] Normal Mode: From manual workflow date {since_date.date()}")
except:
except Exception as e:
log_event(f"[WARN] parse CURATION_START_DATE: {str(e)[:100]}")
since_date = get_last_date()
log_event(f"[*] Normal Mode: Error parsing manual date, using state.json {since_date.date()}")
else:
@@ -114,8 +120,8 @@ async def master_orchestrator():
if os.path.exists(sources_file):
try:
with open(sources_file, 'r') as f:
data = yaml.safe_load(f)
with open(sources_file, 'r', encoding='utf-8') as f:
data = yaml.load(f, Loader=Loader)
all_accounts = set()
for topic_data in data.get("sources", []):
topic_name = topic_data.get("topic")
@@ -206,11 +212,12 @@ async def master_orchestrator():
parsed = urlparse(expanded_url)
domain = parsed.netloc.lower()
domain_info = health_learning["domains"].setdefault(domain, {"attempts": 0, "failures": 0, "consecutive_failures": 0})
domain_info = health_learning["domains"].setdefault(domain, {"attempts": 0, "failures": 0, "consecutive_failures": 0, "success_rate": 100.0})
consecutive_failures = domain_info.get("consecutive_failures", 0)
success_rate = domain_info.get("success_rate", 100.0)
timeout_val = 12.0
if consecutive_failures >= 3:
if consecutive_failures >= 3 or success_rate < 50.0:
timeout_val = 3.0
ua = fallback_user_agents[idx % len(fallback_user_agents)]
else:
@@ -226,18 +233,21 @@ async def master_orchestrator():
resp = await client.get(expanded_url)
if resp.status_code == 404:
asset["health"] = "dead" # Definitively dead
info = health_learning["domains"][domain]
info["failures"] = info.get("failures", 0) + 1
info["consecutive_failures"] = info.get("consecutive_failures", 0) + 1
domain_info["failures"] = domain_info.get("failures", 0) + 1
domain_info["consecutive_failures"] = domain_info.get("consecutive_failures", 0) + 1
else:
asset["health"] = "online"
info = health_learning["domains"][domain]
info["consecutive_failures"] = 0
except:
domain_info["consecutive_failures"] = 0
except Exception as e:
log_event(f"[WARN] health check for {expanded_url}: {str(e)[:100]}")
asset["health"] = "timeout" # Assume alive but unreachable for now
info = health_learning["domains"][domain]
info["failures"] = info.get("failures", 0) + 1
info["consecutive_failures"] = info.get("consecutive_failures", 0) + 1
domain_info["failures"] = domain_info.get("failures", 0) + 1
domain_info["consecutive_failures"] = domain_info.get("consecutive_failures", 0) + 1
# Recalculate success rate and store it
attempts = domain_info.get("attempts", 1)
failures = domain_info.get("failures", 0)
domain_info["success_rate"] = round(((attempts - failures) / attempts) * 100.0, 2)
# 3. GitHub Metadata Enrichment
if "github.com" in expanded_url:
@@ -254,7 +264,8 @@ async def master_orchestrator():
gh_data = gh_resp.json()
asset["gh_stars"] = gh_data.get("stargazers_count")
asset["gh_updated"] = gh_data.get("updated_at", "").split("T")[0]
except: pass
except Exception as e:
log_event(f"[WARN] GitHub metadata enrichment for {expanded_url}: {str(e)[:100]}")
return asset
@@ -291,7 +302,8 @@ async def master_orchestrator():
found = re.findall(r'\]\((https?://[^\)]+)\)', content)
for url in found:
existing_urls.add(url.split('#')[0].rstrip('/').lower())
except: pass
except Exception as e:
log_event(f"[WARN] read docs/{file} for URL extraction: {str(e)[:100]}")
log_event(f"[*] Global Deduplication: {len(existing_urls)} existing URLs loaded.")
@@ -329,12 +341,14 @@ async def master_orchestrator():
if isinstance(ts, str):
try:
asset_date = datetime.strptime(ts, '%a %b %d %H:%M:%S +0000 %Y').replace(tzinfo=MADRID_TZ)
except:
except Exception as e:
try: asset_date = datetime.fromisoformat(ts.replace('Z', '+00:00'))
except: pass
except Exception as e2:
log_event(f"[WARN] parse timestamp '{ts[:30]}': {str(e2)[:100]}")
if asset_date and asset_date > max_tweet_date:
max_tweet_date = asset_date
except: pass
except Exception as e:
log_event(f"[WARN] process asset timestamp: {str(e)[:100]}")
assets_to_evaluate.append(asset)
+3 -1
View File
@@ -66,7 +66,9 @@ def get_system_mandates() -> str:
if os.path.exists(MANDATES_JSON):
try:
return json.load(open(MANDATES_JSON, "r")).get("system_snippet", "")
except: return ""
except Exception as e:
log_event(f"[WARN] Failed to load system mandates: {str(e)[:100]}")
return ""
return ""
if __name__ == "__main__":
+409
View File
@@ -0,0 +1,409 @@
from __future__ import annotations
import os
import json
import asyncio
from datetime import datetime, timedelta
from typing import Dict, List, Any
from src.inventory_manager import load_inventory
from src.gemini_utils import call_gemini_with_retry
from src.config import MADRID_TZ
from src.logger import log_event
DIGEST_OUTPUT_PATH = "data/news_digest.json"
class NewsDigestEngine:
"""Generates a curated news digest by filtering inventory entries by
recency and using Gemini AI to rank the most relevant ones per category.
Three time windows (3 / 6 / 12 months) are produced in a single run.
Each window contains up to 10 AI-ranked items per digest category.
"""
# ------------------------------------------------------------------ #
# 26 Digest Categories mapped from V2 category slugs #
# ------------------------------------------------------------------ #
DIGEST_CATEGORIES: Dict[str, List[str]] = {
# --- TECH CORE (9) ---
"Kubernetes & Orchestration": [
"kubernetes", "kubernetes-tools", "kubernetes-tutorials",
"kubectl-commands", "kubernetes-releases", "kubernetes-autoscaling",
"kubernetes-operators-controllers", "kubernetes-based-devel",
"kubernetes-alternatives", "kubernetes-client-libraries",
"kubernetes-bigdata", "managed-kubernetes-in-public-cloud", "helm",
],
"Containers & Runtime": [
"docker", "container-managers", "serverless", "noops", "registries",
],
"Networking & Service Mesh": [
"networking", "kubernetes-networking", "servicemesh", "istio",
"caching", "web-servers", "cloudflare",
],
"Architecture & Microservices": [
"introduction", "faq", "cloud-arch-diagrams", "matrix-table",
"other-awesome-lists", "about",
],
"Data, Messaging & Storage": [
"databases", "nosql", "newsql", "message-queue", "crunchydata",
"yaml", "kubernetes-storage", "kubernetes-backup-migrations",
],
"AI & Agents": [
"ai", "ai-agents-mcp", "chatgpt",
],
"MLOps & Data Science": [
"mlops",
],
"Python, Java & Developer Ecosystem": [
"python", "golang", "java_frameworks", "java_app_servers",
"java-and-java-performance-optimization", "javascript", "dotnet",
"angular", "react", "web3", "api",
"swagger-code-generator-for-rest-apis", "postman",
"lowcode-nocode", "devel-sites", "dom", "linux-dev-env",
"ChromeDevTools", "xamarin", "jvm-parameters-matrix-table",
"maven-gradle", "embedded-servlet-containers", "visual-studio",
],
"Linux & System Foundations": [
"linux", "git",
],
# --- PLATFORM & OPS (8) ---
"Security & Compliance": [
"securityascode", "kubernetes-security", "aws-security", "oauth",
"devsecops",
],
"Infrastructure as Code": [
"iac", "terraform", "pulumi", "crossplane", "ansible",
"kustomize", "chef", "liquibase",
],
"CI/CD & GitOps": [
"cicd", "gitops", "argo", "flux", "tekton", "jenkins",
"jenkins-alternatives", "sonarqube", "cicd-kubernetes-plugins",
"openshift-pipelines", "stackstorm", "keptn",
],
"Observability, SRE & Testing": [
"sre", "monitoring", "prometheus", "grafana",
"kubernetes-monitoring", "chaos-engineering", "qa",
"test-automation-frameworks", "testops",
"performance-testing-with-jenkins-and-jmeter",
"kubernetes-troubleshooting",
],
"DevOps & Culture": [
"devops", "devops-tools", "project-management-methodology",
"project-management-tools",
],
"Platform Engineering & DevEx": [
"developerportals", "scaffolding", "mkdocs",
],
"FinOps & Cloud Cost": [
"finops", "aws-pricing",
],
"Certification & Training": [
"elearning", "interview-questions", "aws-training", "cheatsheets",
"demos",
],
# --- CLOUD & ENTERPRISE (5) ---
"AWS": [
"aws", "aws-architecture", "aws-security", "aws-networking",
"aws-databases", "aws-storage", "aws-monitoring", "aws-iac",
"aws-tools-scripts", "aws-messaging", "aws-data", "aws-devops",
"aws-serverless", "aws-containers", "aws-backup",
"aws-newfeatures", "aws-miscellaneous", "aws-spain",
],
"Azure": [
"azure",
],
"GCP, OCI & Others": [
"GoogleCloudPlatform", "ibm_cloud", "oraclecloud",
"digitalocean", "scaleway", "edge-computing",
"public-cloud-solutions",
],
"OpenShift / Red Hat": [
"openshift", "ocp3", "ocp4", "openshift-pipelines", "rancher",
],
"Virtualization & Private Cloud": [
"kubernetes-on-premise", "kubernetes-alternatives",
"private-cloud-solutions",
],
# --- INDUSTRY / GEO (4) resolved via geo_region, not slugs ---
"Americas": [],
"Europe": [],
"España": [],
"Asia-Pacific": [],
}
GEO_CATEGORIES: Dict[str, str] = {
"Americas": "americas",
"Europe": "europe",
"España": "spain",
"Asia-Pacific": "asia_pacific",
}
PERIODS: Dict[str, int] = {
"3_months": 90,
"6_months": 180,
"12_months": 365,
}
# ------------------------------------------------------------------ #
def __init__(self) -> None:
self.inventory: Dict[str, Any] = load_inventory()
# Reverse map: v2_category_slug -> digest_category_name
self.category_map: Dict[str, str] = {}
for digest_cat, slugs in self.DIGEST_CATEGORIES.items():
for slug in slugs:
self.category_map[slug] = digest_cat
# ------------------------------------------------------------------ #
# Classification helpers #
# ------------------------------------------------------------------ #
def _get_entry_category(self, entry: dict) -> str | None:
"""Determine which digest category an entry belongs to.
Checks ``v2_locations`` paths first (more specific), then falls
back to the ``category`` field.
"""
for loc in entry.get("v2_locations", []):
slug = loc.replace(".md", "")
if slug in self.category_map:
return self.category_map[slug]
cat = entry.get("category", "")
if cat in self.category_map:
return self.category_map[cat]
return None
def _get_entry_geo(self, entry: dict) -> str | None:
"""Return the geo digest category if ``geo_region`` matches."""
region = entry.get("geo_region", "")
for geo_name, geo_val in self.GEO_CATEGORIES.items():
if region == geo_val:
return geo_name
return None
@staticmethod
def _is_within_period(entry: dict, cutoff_iso: str) -> bool:
"""Return *True* when the entry's ``discovered_at`` is on or after
the ISO-formatted *cutoff_iso* string. ISO 8601 strings sort
lexicographically so a simple ``>=`` comparison is sufficient.
"""
discovered = entry.get("discovered_at", "")
if not discovered:
return False
try:
return discovered >= cutoff_iso
except Exception:
return False
# ------------------------------------------------------------------ #
# Prompt builder #
# ------------------------------------------------------------------ #
@staticmethod
def _build_ranking_prompt(
category: str, entries: List[dict], period: str
) -> str:
"""Assemble the Gemini prompt that asks for a ranked TOP-10."""
period_label = period.replace("_", " ")
lines: List[str] = []
for i, e in enumerate(entries[:50]):
summary_fragment = (e.get("ai_summary", "") or "")[:200]
lines.append(
f'{i}. "{e.get("title", "Unknown")}" '
f'({e.get("url", "")}) | '
f'Stars: {e.get("stars", 0)} | '
f'Year: {e.get("year", "N/A")} | '
f'Summary: {summary_fragment}'
)
entries_text = "\n".join(lines)
return (
"You are a Senior Technical Curator for a Cloud Native "
"knowledge portal.\n"
f'Given these resources discovered in the last {period_label} '
f'for "{category}", select the TOP 10 most relevant.\n\n'
"SCORING CRITERIA:\n"
"- Industry Impact (30%): Does this change how teams "
"build/operate?\n"
"- Technical Novelty (25%): New capability, paradigm shift, "
"major release?\n"
"- Enterprise Adoption (20%): GA releases, production-ready?\n"
"- Community Signal (15%): CNCF graduations, major blog posts?\n"
"- Nubenetes Relevance (10%): Directly related to cloud native?\n\n"
'Respond ONLY JSON: {"items": [{"idx": int, '
'"impact": "critical|high|medium", '
'"why": "1 sentence explaining why this matters"}]}\n\n'
f"RESOURCES:\n{entries_text}"
)
# ------------------------------------------------------------------ #
# Star-based fallback (used when Gemini is unavailable / < 3 entries) #
# ------------------------------------------------------------------ #
@staticmethod
def _fallback_items(
entries: List[dict], cat_name: str, limit: int = 10
) -> List[dict]:
"""Return up to *limit* items using a deterministic star-based
ranking (no AI call required)."""
return [
{
"url": e["url"],
"title": e.get("title", "Unknown"),
"date": e.get("discovered_at", "")[:10],
"stars": e.get("stars", 0),
"impact": "high" if e.get("stars", 0) >= 4 else "medium",
"why": (e.get("ai_summary", "") or "")[:200],
"category": cat_name,
}
for e in entries[:limit]
]
# ------------------------------------------------------------------ #
# Core generation loop #
# ------------------------------------------------------------------ #
async def generate_digest(self) -> dict:
"""Generate the full digest for all categories and time periods.
Returns a nested dict::
{
"3_months": { "Kubernetes & Orchestration": [...], ... },
"6_months": { ... },
"12_months": { ... }
}
"""
digest: Dict[str, Dict[str, List[dict]]] = {}
for period_name, days in self.PERIODS.items():
cutoff = (
datetime.now(MADRID_TZ) - timedelta(days=days)
).isoformat()
digest[period_name] = {}
# Bucket entries into their digest categories
category_pools: Dict[str, List[dict]] = {}
for url, entry in self.inventory.items():
if not isinstance(entry, dict):
continue
if not self._is_within_period(entry, cutoff):
continue
# Tech / topic category
cat = self._get_entry_category(entry)
if cat:
category_pools.setdefault(cat, []).append(
dict(entry, url=url)
)
# Geo category (entry may belong to both)
geo = self._get_entry_geo(entry)
if geo:
category_pools.setdefault(geo, []).append(
dict(entry, url=url)
)
# Rank each category pool
for cat_name, entries in category_pools.items():
entries.sort(
key=lambda x: (
x.get("stars", 0),
x.get("discovered_at", ""),
),
reverse=True,
)
if len(entries) < 3:
# Too few entries include all without AI ranking
digest[period_name][cat_name] = self._fallback_items(
entries, cat_name
)
continue
# Ask Gemini to rank
try:
prompt = self._build_ranking_prompt(
cat_name, entries, period_name
)
result = await call_gemini_with_retry(
prompt,
prefer_flash=True,
role="Digest-Analyst",
)
ranked: List[dict] = []
for item in result.get("items", []):
idx = int(item.get("idx", -1))
if 0 <= idx < len(entries):
e = entries[idx]
ranked.append(
{
"url": e["url"],
"title": e.get("title", "Unknown"),
"date": e.get("discovered_at", "")[:10],
"stars": e.get("stars", 0),
"impact": item.get("impact", "medium"),
"why": item.get("why", ""),
"category": cat_name,
}
)
digest[period_name][cat_name] = ranked[:10]
log_event(
f" [Digest] {period_name}/{cat_name}: "
f"{len(ranked)} items ranked"
)
except Exception as exc:
log_event(
f" [Digest WARN] {period_name}/{cat_name}: "
f"Gemini failed ({str(exc)[:80]}), "
"using star-based fallback"
)
digest[period_name][cat_name] = self._fallback_items(
entries, cat_name
)
# Respect Gemini rate limits
await asyncio.sleep(1.0)
return digest
# ------------------------------------------------------------------ #
# Persistence #
# ------------------------------------------------------------------ #
@staticmethod
def save_digest(digest: dict) -> None:
"""Serialise *digest* to ``data/news_digest.json``."""
os.makedirs(os.path.dirname(DIGEST_OUTPUT_PATH), exist_ok=True)
with open(DIGEST_OUTPUT_PATH, "w", encoding="utf-8") as fh:
json.dump(digest, fh, indent=2, ensure_ascii=False)
log_event(f"[Digest] Saved to {DIGEST_OUTPUT_PATH}")
# ====================================================================== #
# CLI / CI entry point #
# ====================================================================== #
async def run_news_digest() -> None:
"""Entry point for the CI pipeline."""
log_event("STARTING NEWS DIGEST GENERATION", section_break=True)
engine = NewsDigestEngine()
digest = await engine.generate_digest()
engine.save_digest(digest)
total_items = sum(
len(items) for period in digest.values() for items in period.values()
)
log_event(
f"NEWS DIGEST COMPLETE: {total_items} total items across all periods"
)
if __name__ == "__main__":
asyncio.run(run_news_digest())
+2 -1
View File
@@ -47,7 +47,8 @@ def auto_format_file(filepath: str):
try:
cleaned = normalize_url(url)
return f"[{text}]({cleaned})"
except:
except Exception as e:
print(f"[WARN] URL normalization failed for '{url[:60]}': {str(e)[:100]}")
return match.group(0)
return match.group(0)
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
import os
import re
import sys
def check_file(file_path):
errors = []
warnings = []
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
lines = content.splitlines()
seen_urls = set()
for line_num, line in enumerate(lines, 1):
# 1. Check section titles (H2-H6) for Emojis, Special Characters, and Ampersands (Mandate 32)
if line.startswith("#"):
header_level = len(line) - len(line.lstrip('#'))
if 2 <= header_level <= 6:
title_text = line.lstrip('#').strip()
# Check for ampersand
if "&" in title_text:
errors.append(f"Line {line_num}: Title contains ampersand '&': '{title_text}' (replace with 'and')")
# Check for emojis or special characters
for char in title_text:
# Allow alphanumeric, spaces, hyphens, colons, parentheses, commas, periods, quotes
if ord(char) > 0x2000 and ord(char) not in (0x2013, 0x2014, 0x2018, 0x2019, 0x201c, 0x201d):
errors.append(f"Line {line_num}: Title contains emojis or special characters: '{char}' in '{title_text}'")
# 2. Check for Markdown link rules (Mandate 33)
# Match [ Link ](URL) - space at start or end of brackets
if re.search(r'\[\s+[^\]]*\]\(', line) or re.search(r'\[[^\]]*\s+\]\(', line):
errors.append(f"Line {line_num}: Link text contains leading or trailing spaces inside brackets: '{line.strip()}'")
# 3. Extract and validate URLs
links = re.findall(r'\[([^\]]+)\]\(([^)]+)\)', line)
for link_text, url in links:
url = url.strip()
if url.startswith("http://") or url.startswith("https://"):
if url.startswith("https:/") and not url.startswith("https://"):
errors.append(f"Line {line_num}: Corrupted protocol prefix: '{url}'")
elif url.startswith("http:/") and not url.startswith("http://"):
errors.append(f"Line {line_num}: Corrupted protocol prefix: '{url}'")
# Check for duplicates in this category file
if os.path.basename(file_path) not in ["index.md", "about.md"]:
# Normalize URL to check duplicate
clean_url = url.split("?")[0].split("#")[0].lower().rstrip('/')
if clean_url in seen_urls:
# Report as warning rather than blocking error
warnings.append(f"Line {line_num}: Duplicate URL found in this file: '{url}'")
else:
seen_urls.add(clean_url)
# Check year tag if present
match_year = re.search(r'\*\*\(([0-9]{4})\)\*\*', line)
if match_year:
year = int(match_year.group(1))
if not (1990 <= year <= 2030):
errors.append(f"Line {line_num}: Year tag '{year}' is outside reasonable bounds: '{line.strip()}'")
# Global multi-line check for link text line breaks
if re.search(r'\[[^\]]*\n[^\]]*\]\(', content):
errors.append("Global: Found a link with a line break inside the link text brackets.")
return errors, warnings
def main():
docs_dir = "docs"
if not os.path.exists(docs_dir):
print(f"Directory {docs_dir} not found.")
sys.exit(0)
total_errors = 0
total_warnings = 0
for root, dirs, files in os.walk(docs_dir):
if "images" in root or "static" in root:
continue
for file in files:
if file.endswith(".md"):
path = os.path.join(root, file)
errors, warnings = check_file(path)
if errors:
print(f"{path}:")
for err in errors:
print(f" {err}")
total_errors += len(errors)
if warnings:
# By default do not flood output with duplicate warnings unless verbose
if "--verbose" in sys.argv:
print(f"⚠️ {path}:")
for warn in warnings:
print(f" {warn}")
total_warnings += len(warnings)
print(f"\nScan complete: Found {total_errors} errors and {total_warnings} warnings in markdown files.")
if total_errors > 0:
sys.exit(1)
else:
print("All Markdown files passed the schema check successfully!")
sys.exit(0)
if __name__ == "__main__":
main()
+5 -2
View File
@@ -12,7 +12,9 @@ V2_DIR = "v2-docs"
def run_command(cmd):
try:
return subprocess.check_output(cmd, shell=True).decode('utf-8').strip()
except: return "0"
except Exception as e:
print(f"[WARN] Command '{cmd[:60]}' failed: {str(e)[:100]}")
return "0"
def clean_text(text: str) -> str:
"""Strips emojis and ampersands for README compatibility."""
@@ -26,7 +28,8 @@ def get_stats():
inventory = {}
try:
inventory = load_inventory()
except: pass
except Exception as e:
print(f"[WARN] Failed to load inventory: {str(e)[:100]}")
# 2. Basic Metrics
total_links = len([u for u in inventory.keys() if not u.startswith("INTRO:")])
+2 -4
View File
@@ -1,5 +1,6 @@
import yaml
import os
from src.inventory_manager import load_inventory
# Map category IDs to their friendly names and outline border colors (V2 only)
CATEGORIES = {
@@ -13,10 +14,7 @@ CATEGORIES = {
}
def load_inventory_channels():
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
inventory_path = os.path.join(repo_root, 'data', 'inventory.yaml')
with open(inventory_path, 'r', encoding='utf-8') as f:
inventory = yaml.safe_load(f) or {}
inventory = load_inventory()
channels = []
for url, entry in inventory.items():
+32 -22
View File
@@ -6,6 +6,12 @@ from datetime import datetime
from src.logger import log_event
from src.gemini_utils import normalize_url, clean_toc_text
from src.config import INVENTORY_PATH
from src.inventory_manager import load_inventory
try:
from yaml import CSafeLoader as Loader
except ImportError:
from yaml import SafeLoader as Loader
V1_DIR = "docs"
V2_DIR = "v2-docs"
@@ -20,19 +26,16 @@ class SafetyGuard:
self.inventory = self._load_inventory()
def _load_inventory(self):
if os.path.exists(INVENTORY_PATH):
try:
with open(INVENTORY_PATH, "r") as f:
return yaml.safe_load(f) or {}
except: return {}
return {}
return load_inventory()
def _load_exempt_files(self):
try:
with open("data/link_rules.yaml", "r") as f:
rules = yaml.safe_load(f)
with open("data/link_rules.yaml", "r", encoding="utf-8") as f:
rules = yaml.load(f, Loader=Loader)
return rules.get("hierarchy_rules", {}).get("toc_exempt_files", [])
except: return []
except Exception as e:
log_event(f"[WARN] load exempt files from link_rules.yaml: {str(e)[:100]}")
return []
def validate_data_integrity(self, old_inventory: dict):
"""Mandate 1: Información Preservation."""
@@ -59,9 +62,11 @@ class SafetyGuard:
"""Mandate 27: Special Assets Exhaustive Inclusion."""
if not os.path.exists(SPECIAL_ASSETS_PATH): return
try:
with open(SPECIAL_ASSETS_PATH, "r") as f:
special = yaml.safe_load(f).get("special_assets", [])
except: return
with open(SPECIAL_ASSETS_PATH, "r", encoding="utf-8") as f:
special = yaml.load(f, Loader=Loader).get("special_assets", [])
except Exception as e:
log_event(f"[WARN] load special_assets.yaml for validation: {str(e)[:100]}")
return
for sa in special:
if "Include 100%" in sa.get("v2_rule", "") or "Exhaustive" in sa.get("v2_rule", ""):
@@ -88,7 +93,8 @@ class SafetyGuard:
stars = meta.get("gh_stars", meta.get("stars", 0) * 100)
if inactive_years > 4 and stars < 30:
self.warnings.append(f"🏚️ **MVQ Violation**: Stale repo `{url}` (>4yrs) in V2 with low impact")
except: pass
except Exception as e:
log_event(f"[WARN] MVQ compliance check for {url}: {str(e)[:100]}")
def validate_linguistic_tagging(self):
"""Mandate 10: Explicit Language Tagging."""
@@ -175,9 +181,11 @@ class SafetyGuard:
"""Mandate 11: Workflow-Config Synchronization."""
if not os.path.exists(WORKFLOW_PATH) or not os.path.exists(CURATION_SOURCES_PATH): return
try:
with open(CURATION_SOURCES_PATH, "r") as f:
sources = yaml.safe_load(f).get("sources", [])
except: return
with open(CURATION_SOURCES_PATH, "r", encoding="utf-8") as f:
sources = yaml.load(f, Loader=Loader).get("sources", [])
except Exception as e:
log_event(f"[WARN] load curation_sources.yaml for nav sync: {str(e)[:100]}")
return
topics = [s["topic"] for s in sources]
with open(WORKFLOW_PATH, "r") as f:
@@ -191,10 +199,11 @@ class SafetyGuard:
def validate_forbidden_tags(self):
"""Mandate 51/Safety: Check for forbidden HTML tags in docs and v2-docs, except allowing iframes in videos."""
try:
with open("data/link_rules.yaml", "r") as f:
rules = yaml.safe_load(f)
with open("data/link_rules.yaml", "r", encoding="utf-8") as f:
rules = yaml.load(f, Loader=Loader)
forbidden = rules.get("safety_guard", {}).get("forbidden_tags", [])
except:
except Exception as e:
log_event(f"[WARN] load forbidden tags from link_rules.yaml: {str(e)[:100]}")
return
if not forbidden:
@@ -275,9 +284,10 @@ class SafetyGuard:
# 2. Run standard validations
if old_inv_path and os.path.exists(old_inv_path):
try:
with open(old_inv_path, "r") as f:
self.validate_data_integrity(yaml.safe_load(f) or {})
except: pass
with open(old_inv_path, "r", encoding="utf-8") as f:
self.validate_data_integrity(yaml.load(f, Loader=Loader) or {})
except Exception as e:
log_event(f"[WARN] load old inventory for data integrity check: {str(e)[:100]}")
self.validate_semantic_interlinking()
self.validate_special_assets_completeness()
+1 -1
View File
@@ -8,7 +8,7 @@ REQUIRED_SECTIONS = [
"3. The Agentic Stack",
"4. The 2026 Architectural Shift",
"5. Dual-Edition Architecture (V1 vs V2)",
"6. The Unified Agentic Database (Knowledge Graph)",
"6. The Unified Agentic Database (Coexistence Knowledge Graph)",
"7. AI Economic Architecture and Cost Analysis",
"8. The Agentic AI Engine",
"9. GitHub Workflows and Automation",
+7 -2
View File
@@ -6,6 +6,11 @@ from src.logger import log_event
CURATION_SOURCES_PATH = "data/curation_sources.yaml"
WORKFLOW_PATH = ".github/workflows/01.1.agentic_cron.yml"
try:
from yaml import CSafeLoader as Loader
except ImportError:
from yaml import SafeLoader as Loader
class WorkflowUISync:
"""
Automates Mandate 11: Workflow-Config Synchronization.
@@ -16,8 +21,8 @@ class WorkflowUISync:
return False
try:
with open(CURATION_SOURCES_PATH, "r") as f:
sources = yaml.safe_load(f).get("sources", [])
with open(CURATION_SOURCES_PATH, "r", encoding="utf-8") as f:
sources = yaml.load(f, Loader=Loader).get("sources", [])
except Exception as e:
log_event(f" [!] Error loading curation sources: {e}")
return False
+43 -10
View File
@@ -31,6 +31,27 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
tags = item.get("tags", [])
initial_score = item.get("impact_score", item.get("stars", 3) * 20) # Fallback mapping if stars is used
# 0. Check cache using content hash (Recommendation #4)
import hashlib
raw_content = f"{title}||{desc}||{','.join(sorted(tags))}"
content_hash = hashlib.sha256(raw_content.encode("utf-8")).hexdigest()
if os.path.exists(DEBATE_MEMORY_FILE):
try:
with open(DEBATE_MEMORY_FILE, "r") as f:
memory_data = json.load(f)
cached = memory_data.get("resolved_debates", {}).get(normalize_url(url))
if cached and cached.get("content_hash") == content_hash:
log_event(f" [⚖️] CACHE HIT: Skipping debate for '{title}'. Returning cached consensus.")
return (
cached["final_consensus_score"],
cached.get("final_tags", tags),
cached.get("refined_summary", desc),
cached
)
except Exception as e:
log_event(f" [!] Error checking debate cache: {e}")
log_event(f" [⚖️] DEBATE TRIGGERED: '{title}' (Initial Score: {initial_score})", section_break=False)
# 0. Check if mock mode is requested or required (no keys configured)
@@ -128,7 +149,10 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
"scores": scores,
"justifications": justifications,
"rebuttals": debate_transcript,
"timestamp": datetime.now().isoformat()
"timestamp": datetime.now().isoformat(),
"final_tags": sorted(list(final_tags)),
"refined_summary": refined_summary,
"content_hash": content_hash
}
try:
@@ -136,13 +160,14 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
if os.path.exists(DEBATE_MEMORY_FILE):
try:
memory_data = json.load(open(DEBATE_MEMORY_FILE, "r"))
except: pass
except Exception as e:
log_event(f"[WARN] load debate memory for mock persist: {str(e)[:100]}")
memory_data.setdefault("resolved_debates", {})[normalize_url(url)] = debate_data
with open(DEBATE_MEMORY_FILE, "w") as f:
json.dump(memory_data, f, indent=2)
except Exception as e:
log_event(f" [!] Failed to persist debate memory: {e}")
return final_score, sorted(list(final_tags)), refined_summary, debate_data
system_mandates = get_system_mandates()
@@ -187,7 +212,10 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
"final_consensus_score": fast_pass_score,
"fast_pass": True,
"justification": fast_pass_justification,
"timestamp": datetime.now().isoformat()
"timestamp": datetime.now().isoformat(),
"final_tags": fast_pass_tags,
"refined_summary": fast_pass_summary,
"content_hash": content_hash
}
try:
@@ -195,13 +223,14 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
if os.path.exists(DEBATE_MEMORY_FILE):
try:
memory_data = json.load(open(DEBATE_MEMORY_FILE, "r"))
except: pass
except Exception as e:
log_event(f"[WARN] load debate memory for fast-pass persist: {str(e)[:100]}")
memory_data.setdefault("resolved_debates", {})[normalize_url(url)] = debate_data
with open(DEBATE_MEMORY_FILE, "w") as f:
json.dump(memory_data, f, indent=2)
except Exception as e:
log_event(f" [!] Failed to persist debate memory: {e}")
return fast_pass_score, fast_pass_tags, fast_pass_summary, debate_data
log_event(f" [⚖️] Borderline score detected ({fast_pass_score}). Escalating to full Multi-Agent Debate Panel...")
@@ -325,7 +354,10 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
"scores": scores,
"justifications": justifications,
"rebuttals": debate_transcript,
"timestamp": datetime.now().isoformat()
"timestamp": datetime.now().isoformat(),
"final_tags": final_tags,
"refined_summary": refined_summary,
"content_hash": content_hash
}
# Persist the resolved debate to memory log (Mandate 3.1)
@@ -334,10 +366,11 @@ async def run_debate_protocol(item: Dict, is_new_link: bool = False) -> Tuple[in
if os.path.exists(DEBATE_MEMORY_FILE):
try:
memory_data = json.load(open(DEBATE_MEMORY_FILE, "r"))
except: pass
except Exception as e:
log_event(f"[WARN] load debate memory for final persist: {str(e)[:100]}")
memory_data.setdefault("resolved_debates", {})[normalize_url(url)] = debate_data
# Keep blacklist and other fields intact
with open(DEBATE_MEMORY_FILE, "w") as f:
json.dump(memory_data, f, indent=2)
+221 -37
View File
@@ -4,6 +4,10 @@ import json
import hashlib
import asyncio
import yaml
try:
from yaml import CSafeLoader as Loader
except ImportError:
from yaml import SafeLoader as Loader
import httpx
from datetime import datetime
from typing import List, Dict, Set, Any, Tuple
@@ -37,14 +41,30 @@ class V2VisionEngine:
"AI": ["ai", "ai-agents-mcp", "chatgpt", "mlops"],
"Architectural Foundations": ["introduction", "faq", "kubernetes", "linux", "git", "cloud-arch-diagrams", "matrix-table", "other-awesome-lists", "about"],
"Platform & Site Reliability": ["sre", "devops", "developerportals", "scaffolding", "finops", "chaos-engineering", "performance-testing-with-jenkins-and-jmeter", "project-management-methodology", "project-management-tools", "qa", "test-automation-frameworks", "testops"],
"Hardened Infrastructure": ["iac", "terraform", "pulumi", "crossplane", "ansible", "securityascode", "kubernetes-security", "aws-security", "oauth", "devsecops", "kustomize", "liquibase", "chef"],
"Cloud Providers (Hyperscalers)": ["aws", "azure", "GoogleCloudPlatform", "ibm_cloud", "oraclecloud", "digitalocean", "cloudflare", "scaleway", "managed-kubernetes-in-public-cloud", "public-cloud-solutions", "private-cloud-solutions", "edge-computing", "aws-architecture", "aws-security", "aws-networking", "aws-databases", "aws-storage", "aws-monitoring", "aws-iac", "aws-tools-scripts", "aws-messaging", "aws-data", "aws-devops", "aws-serverless", "aws-containers", "aws-backup", "aws-training", "aws-newfeatures", "aws-miscellaneous", "aws-pricing", "aws-spain"],
"Hardened Infrastructure": ["iac", "terraform", "pulumi", "crossplane", "ansible", "securityascode", "kubernetes-security", "aws-security", "devsecops", "kustomize", "liquibase"],
"Cloud Providers (Hyperscalers)": ["aws", "azure", "GoogleCloudPlatform", "ibm_cloud", "oraclecloud", "digitalocean", "cloudflare", "managed-kubernetes-in-public-cloud", "public-cloud-solutions", "edge-computing", "aws-architecture", "aws-security", "aws-networking", "aws-databases", "aws-storage", "aws-monitoring", "aws-iac", "aws-tools-scripts", "aws-messaging", "aws-data", "aws-devops", "aws-serverless", "aws-containers", "aws-backup", "aws-training", "aws-newfeatures", "aws-miscellaneous", "aws-pricing"],
"Networking & Service Mesh": ["networking", "kubernetes-networking", "servicemesh", "istio", "caching", "web-servers", "cloudflare"],
"The Container Stack": ["docker", "container-managers", "serverless", "kubernetes-autoscaling", "kubernetes-operators-controllers", "kubernetes-storage", "kubernetes-monitoring", "kubernetes-troubleshooting", "kubernetes-backup-migrations", "kubernetes-on-premise", "kubernetes-bigdata", "kubernetes-client-libraries", "kubernetes-releases", "kubernetes-based-devel", "kubernetes-alternatives", "kubectl-commands", "rancher", "openshift", "ocp3", "ocp4", "noops"],
"Data & Advanced Analytics": ["databases", "nosql", "newsql", "message-queue", "crunchydata", "yaml", "bigdata"],
"Engineering Pipeline": ["cicd", "gitops", "argo", "flux", "tekton", "jenkins", "jenkins-alternatives", "openshift-pipelines", "sonarqube", "registries", "keptn", "stackstorm", "cicd-kubernetes-plugins"],
"Developer Ecosystem": ["visual-studio", "javascript", "golang", "python", "java_frameworks", "java_app_servers", "java-and-java-performance-optimization", "dotnet", "angular", "react", "web3", "api", "swagger-code-generator-for-rest-apis", "postman", "lowcode-nocode", "devel-sites", "dom", "linux-dev-env", "ChromeDevTools", "xamarin", "jvm-parameters-matrix-table", "maven-gradle", "embedded-servlet-containers"],
"Career & Industry": ["recruitment", "hr", "finops", "freelancing", "remote-tech-jobs", "workfromhome", "interview-questions", "elearning", "digital-money", "appointment-scheduling", "newsfeeds"]
"Data & Advanced Analytics": ["databases", "nosql", "message-queue", "crunchydata", "yaml", "bigdata"],
"Engineering Pipeline": ["cicd", "gitops", "argo", "flux", "tekton", "jenkins", "jenkins-alternatives", "openshift-pipelines", "sonarqube", "registries", "keptn", "cicd-kubernetes-plugins"],
"Developer Ecosystem": ["visual-studio", "javascript", "golang", "python", "java_frameworks", "java_app_servers", "java-and-java-performance-optimization", "dotnet", "angular", "web3", "api", "swagger-code-generator-for-rest-apis", "postman", "lowcode-nocode", "devel-sites", "linux-dev-env", "ChromeDevTools", "maven-gradle", "embedded-servlet-containers"],
"Career & Industry": ["recruitment", "hr", "finops", "freelancing", "remote-tech-jobs", "workfromhome", "interview-questions", "elearning", "appointment-scheduling", "newsfeeds"]
}
# Stub page merge map: content from source pages renders on target pages
self.merge_map = {
"jvm-parameters-matrix-table": "java-and-java-performance-optimization",
"private-cloud-solutions": "kubernetes-on-premise",
"stackstorm": "cicd",
"chef": "ansible",
"newsql": "databases",
"scaleway": "digitalocean",
"xamarin": "dotnet",
"dom": "javascript",
"react": "javascript",
"oauth": "securityascode",
"digital-money": "finops",
"aws-spain": "aws",
}
self.library_criteria = (
@@ -74,15 +94,23 @@ class V2VisionEngine:
def _load_special_assets(self) -> Dict:
path = "data/special_assets.yaml"
if os.path.exists(path):
try: return yaml.safe_load(open(path, "r")) or {}
except: return {}
try:
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=Loader) or {}
except Exception as e:
log_event(f"[WARN] load special_assets.yaml: {str(e)[:100]}")
return {}
return {}
def _load_link_rules(self) -> Dict:
path = "data/link_rules.yaml"
if os.path.exists(path):
try: return yaml.safe_load(open(path, "r")) or {}
except: return {}
try:
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=Loader) or {}
except Exception as e:
log_event(f"[WARN] load link_rules.yaml: {str(e)[:100]}")
return {}
return {}
def _load_inventory(self) -> Dict:
@@ -98,18 +126,20 @@ class V2VisionEngine:
# Mandate 30: MD039 - Global Data Sanitization (Purge all whitespace/hidden chars from titles)
for url in list(self.inventory.keys()):
if isinstance(self.inventory[url], dict) and "title" in self.inventory[url]:
# Purge all known whitespace characters (standard, non-breaking, thin, etc.)
if isinstance(self.inventory[url], dict) and self.inventory[url].get("title") is not None:
t = self.inventory[url]["title"]
t = re.sub(r'^[\s\u00a0\u200b\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+', '', t)
t = re.sub(r'[\s\u00a0\u200b\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+$', '', t)
self.inventory[url]["title"] = t
if isinstance(t, str):
# Purge all known whitespace characters (standard, non-breaking, thin, etc.)
t = re.sub(r'^[\s\u00a0\u200b\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+', '', t)
t = re.sub(r'[\s\u00a0\u200b\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+$', '', t)
self.inventory[url]["title"] = t
# 0. Mandate Sync
try:
from src.mandate_ingestor import MandateIngestor
MandateIngestor().save_system_instructions()
except: pass
except Exception as e:
log_event(f"[WARN] mandate sync: {str(e)[:100]}")
all_v1_links, mosaic_html, videos_html = await self._gather_all_v1_content()
@@ -139,12 +169,13 @@ class V2VisionEngine:
# --- SURGICAL GARBAGE COLLECTION ---
# Track every file we generate
generated_files = {"index.md", "audit-log.md", "videos.md", "tags.md"}
generated_files = {"index.md", "audit-log.md", "videos.md", "tags.md", "tech-digest.md", "industry-digest.md"}
for f_name in v2_data.keys():
generated_files.add(f_name)
await self._write_premium_files(v2_data, mosaic_html, videos_html)
self._generate_digest_pages()
await self._generate_global_tag_index(v2_data)
await self._sync_enterprise_navigation(v2_data)
@@ -197,7 +228,11 @@ class V2VisionEngine:
if not url.startswith(("http", "mailto", "#")):
url = f"https://nubenetes.com/{url.replace('.md', '/')}"
# Mandate 30: MD039 - Strip all whitespace (including non-breaking space) from link text
all_links.append({"title": nuclear_strip(title), "url": url.strip(), "description": full_desc.strip(), "original_file": file})
orig_file = file
slug = file.replace(".md", "")
if slug in self.merge_map:
orig_file = self.merge_map[slug] + ".md"
all_links.append({"title": nuclear_strip(title), "url": url.strip(), "description": full_desc.strip(), "original_file": orig_file})
return all_links, mosaic_html, videos_html
async def _verify_link_health(self, links: List[Dict]):
@@ -212,7 +247,11 @@ class V2VisionEngine:
if entry.get("status") == "review_required": continue
if not force_full and entry.get("status") == "online":
fast_online.append(l)
last_checked = entry.get("last_checked", 0)
if isinstance(last_checked, (int, float)) and (datetime.now().timestamp() - last_checked) > 30 * 86400:
needs_check.append(l)
else:
fast_online.append(l)
else:
needs_check.append(l)
@@ -261,7 +300,8 @@ class V2VisionEngine:
# Mandate 22: Update last_checked for the inventory entry
self.inventory[normalize_url(final_url)]["last_checked"] = datetime.now().timestamp()
return link
except: pass
except Exception as e:
log_event(f"[WARN] resilient link check for {url}: {str(e)[:100]}")
return None
async def _evaluate_and_score_resources(self, links: List[Dict]):
@@ -333,7 +373,15 @@ class V2VisionEngine:
if is_special: item["is_special"] = True
# Mandate 30: Hierarchy and AI Summaries are mandatory for ELITE AI curation.
# Optimized Skip Logic: Only skip if we already have BOTH hierarchy and a summary.
if ((cached.get("hierarchy") and cached.get("ai_summary")) or self.render_only) and not force_eval:
last_eval = cached.get("last_ai_eval", "")
eval_stale = False
if last_eval and isinstance(last_eval, str) and len(last_eval) >= 10:
try:
eval_age = (datetime.now(MADRID_TZ) - datetime.fromisoformat(last_eval)).days
eval_stale = eval_age > 180
except Exception:
pass
if ((cached.get("hierarchy") and cached.get("ai_summary") and not eval_stale) or self.render_only) and not force_eval:
if project_id not in project_registry or item.get("stars", 0) > project_registry[project_id].get("stars", 0):
if project_id in project_registry and project_registry[project_id].get("is_special"): item["is_special"] = True
project_registry[project_id] = item
@@ -402,11 +450,15 @@ class V2VisionEngine:
"resource_type": res.get("type", "Reference"), "complexity": res.get("complexity", "Intermediate"),
"hierarchy": res.get("hierarchy", ["General"]), "tags": res.get("tags", []),
"is_microservice": bool(res.get("is_microservice", False)),
"status": "online", "is_special": item.get("is_special", False)
"status": "online", "is_special": item.get("is_special", False),
"last_ai_eval": datetime.now(MADRID_TZ).isoformat()
}
existing_entry = self.inventory.get(normalize_url(item["url"]), {})
if existing_entry.get("discovered_at"):
eval_data["discovered_at"] = existing_entry["discovered_at"]
item.update(eval_data)
batch_results.append(item)
# Incremental Persistence
norm_url = normalize_url(item["url"])
from src.inventory_manager import update_inventory_entry
@@ -488,8 +540,12 @@ class V2VisionEngine:
"resource_type": res.get("type", "Reference"), "complexity": res.get("complexity", "Intermediate"),
"hierarchy": res.get("hierarchy", ["General"]), "tags": res.get("tags", []),
"is_microservice": bool(res.get("is_microservice", False)),
"status": "online", "is_special": item.get("is_special", False)
"status": "online", "is_special": item.get("is_special", False),
"last_ai_eval": datetime.now(MADRID_TZ).isoformat()
}
existing_entry = self.inventory.get(normalize_url(item["url"]), {})
if existing_entry.get("discovered_at"):
eval_data["discovered_at"] = existing_entry["discovered_at"]
item.update(eval_data)
analyst_results.append(item)
except Exception:
@@ -706,9 +762,22 @@ class V2VisionEngine:
return v2_structure
def _collect_tags_from_tree(self, node: Dict) -> List[Set]:
"""Recursively collect maturity/tech tags from a content tree for cross-referencing."""
results = []
if "__links__" in node:
for link in node["__links__"]:
tags = set(link.get("tags", []))
if tags:
results.append(tags)
for key, val in node.items():
if key != "__links__" and isinstance(val, dict):
results.extend(self._collect_tags_from_tree(val))
return results
async def _generate_comparison_table(self, links: List[Dict]) -> str:
standard_tools = [l for l in links if l.get("stars", 0) >= 3]
if len(standard_tools) < 5: return ""
if len(standard_tools) < 8: return ""
table = "\n??? abstract \"Architect's Technical Comparison Table\"\n"
table += " | Solution | Maturity | Primary Focus | Language | Stars |\n"
table += " | :--- | :--- | :--- | :--- | :--- |\n"
@@ -863,10 +932,100 @@ class V2VisionEngine:
def _generate_digest_pages(self):
"""Generate tech-digest.md and industry-digest.md from news_digest.json."""
digest_path = "data/news_digest.json"
if not os.path.exists(digest_path):
log_event("[Digest] No digest data found, skipping page generation")
return
with open(digest_path, "r", encoding="utf-8") as f:
digest_data = json.load(f)
tech_cats = [
"Kubernetes & Orchestration", "Containers & Runtime", "Networking & Service Mesh",
"Architecture & Microservices", "Data, Messaging & Storage", "AI & Agents",
"MLOps & Data Science", "Python, Java & Developer Ecosystem", "Linux & System Foundations",
"Security & Compliance", "Infrastructure as Code", "CI/CD & GitOps",
"Observability, SRE & Testing", "DevOps & Culture", "Platform Engineering & DevEx",
"FinOps & Cloud Cost", "Certification & Training",
"AWS", "Azure", "GCP, OCI & Others", "OpenShift / Red Hat", "Virtualization & Private Cloud"
]
geo_cats = ["Americas", "Europe", "España", "Asia-Pacific"]
period_labels = {"3_months": "Last 3 Months", "6_months": "Last 6 Months", "12_months": "Last 12 Months"}
def render_digest_page(title, categories, digest_data):
md = f"# {title}\n\n"
md += "!!! tip \"Nubenetes Intelligence Digest\"\n"
md += " AI-curated ranking of the most impactful resources, updated monthly.\n\n"
for period_key, period_label in period_labels.items():
md += f'=== "{period_label}"\n\n'
period_data = digest_data.get(period_key, {})
for cat in categories:
items = period_data.get(cat, [])
if not items:
continue
md += f" ## {cat}\n\n"
md += " | Date | Resource | Impact | Why It Matters |\n"
md += " | :--- | :--- | :---: | :--- |\n"
for item in items:
impact_badge = {"critical": "🔴", "high": "🟡", "medium": "🔵"}.get(item.get("impact", "medium"), "🔵")
t = nuclear_strip(item.get("title", "Unknown"))
md += f' | {item.get("date", "")} | [{t}]({item.get("url", "#")}) | {impact_badge} {item.get("impact", "medium")} | {item.get("why", "")} |\n'
md += "\n"
md += "\n"
return md
tech_md = render_digest_page("📊 Nubenetes Tech & Cloud Intelligence Digest", tech_cats, digest_data)
with open(os.path.join(V2_DIR, "tech-digest.md"), "w", encoding="utf-8") as f:
f.write(tech_md)
industry_md = render_digest_page("🌍 Nubenetes Industry & Geo Intelligence Digest", geo_cats, digest_data)
with open(os.path.join(V2_DIR, "industry-digest.md"), "w", encoding="utf-8") as f:
f.write(industry_md)
log_event("[Digest] Generated tech-digest.md and industry-digest.md")
async def _write_premium_files(self, data: Dict[str, Dict], mosaic_html: str, videos_html: str):
# 1. Update Index with Pulse
trending_pool = sorted([dict(meta, url=url) for url, meta in self.inventory.items() if isinstance(meta, dict) and meta.get("stars", 0) >= 4], key=lambda x: (str(x.get("year", "0000")) if str(x.get("year", "")).isdigit() else "0000", -x.get("stars", 0)), reverse=True)
pulse_md = "## The Agentic Pulse\n" + "\n".join([f"- **({l.get('year', 'N/A')})** [**=={nuclear_strip(l['title'])}==**]({l['url'].strip()}) {'🌟'*l.get('stars',3)}" for l in trending_pool[:5]])
# 1. Build Trending Now from digest data, or fallback to star-based pulse
digest_data = {}
digest_path = "data/news_digest.json"
if os.path.exists(digest_path):
try:
with open(digest_path, "r", encoding="utf-8") as df:
digest_data = json.load(df)
except Exception:
pass
if digest_data and "3_months" in digest_data:
top_items = []
for cat_name, items in digest_data.get("3_months", {}).items():
for item in items[:2]:
top_items.append({**item, "digest_category": cat_name})
top_items.sort(key=lambda x: {"critical": 3, "high": 2, "medium": 1}.get(x.get("impact", "medium"), 0), reverse=True)
top_items = top_items[:6]
impact_icons = {"critical": "🔴", "high": "🟡", "medium": "🔵"}
cards_html = '<div class="trending-section">\n<div class="trending-section__title">🔥 Trending Now — Cloud Native Intelligence</div>\n<div class="trending-grid">\n'
for item in top_items:
impact = item.get("impact", "medium")
cards_html += (
f'<div class="trending-card">\n'
f' <div class="trending-card__impact trending-card__impact--{impact}">{impact_icons.get(impact, "🔵")} {impact.upper()}</div>\n'
f' <div class="trending-card__category">{item.get("digest_category", "")}</div>\n'
f' <div class="trending-card__title"><a href="{item.get("url", "#")}">{nuclear_strip(item.get("title", "Unknown"))}</a></div>\n'
f' <div class="trending-card__meta">{item.get("date", "")} · {"🌟" * item.get("stars", 0)}</div>\n'
f' <div class="trending-card__why">{item.get("why", "")}</div>\n'
f'</div>\n'
)
cards_html += '</div>\n'
cards_html += '<div class="digest-links">\n'
cards_html += ' <a href="./tech-digest/" class="digest-link-card">📊 Full Tech & Cloud Digest →</a>\n'
cards_html += ' <a href="./industry-digest/" class="digest-link-card">🌍 Industry & Geo Digest →</a>\n'
cards_html += '</div>\n</div>\n'
pulse_md = cards_html
else:
trending_pool = sorted([dict(meta, url=url) for url, meta in self.inventory.items() if isinstance(meta, dict) and meta.get("stars", 0) >= 4], key=lambda x: (str(x.get("year", "0000")) if str(x.get("year", "")).isdigit() else "0000", -x.get("stars", 0)), reverse=True)
pulse_md = "## The Agentic Pulse\n" + "\n".join([f"- **({l.get('year', 'N/A')})** [**=={nuclear_strip(l['title'])}==**]({l['url'].strip()}) {'🌟'*l.get('stars',3)}" for l in trending_pool[:5]])
# Calculate coverage for the index
total_v1 = len(self.inventory)
@@ -902,7 +1061,7 @@ class V2VisionEngine:
"<center markdown=\"1\">\n"
"<div class=\"hero-showcase-wrapper\">\n"
" <a href=\"https://www.cncf.io/certification/software-conformance\" class=\"hero-showcase-link\">\n"
" <img src=\"images/container_with_cars_v2.png\" alt=\"container_with_cars\" class=\"hero-showcase-image\" />\n"
" <img src=\"/images/container_with_cars_v2.png\" alt=\"container_with_cars\" class=\"hero-showcase-image\" />\n"
" <div class=\"hero-showcase-footer\">\n"
" <span class=\"hero-showcase-badge\">CNCF Conformance</span>\n"
" <span class=\"hero-showcase-caption\">Standardized conformance guarantees seamless workload portability across the Cloud Native landscape.</span>\n"
@@ -1158,10 +1317,30 @@ class V2VisionEngine:
md += await render_node(info["content"], -1, f_name.replace(".md", ""), used_headers, is_intro=(f_name=="introduction.md" or f_name=="about.md"))
# Add Semantic "See Also" ONLY ONCE at the end of the page
related = [f"[{data[f]['title']}](./{f})" for f in data if f != f_name and data[f]["dim"] == info["dim"]]
if related:
md += f"\n---\n💡 **Explore Related:** {' | '.join(related[:3])}\n\n"
# Add Semantic "See Also" — same dimension + cross-dimension by shared tags
same_dim = [f for f in data if f != f_name and data[f]["dim"] == info["dim"]]
cross_dim = []
if info.get("content") and isinstance(info["content"], dict):
page_tags = set()
for node_links in self._collect_tags_from_tree(info["content"]):
page_tags.update(node_links)
if page_tags:
for f in data:
if f != f_name and data[f]["dim"] != info["dim"]:
other_tags = set()
if isinstance(data[f].get("content"), dict):
for t in self._collect_tags_from_tree(data[f]["content"]):
other_tags.update(t)
if page_tags & other_tags:
cross_dim.append(f)
related = [f"[{data[f]['title']}](./{f})" for f in same_dim[:3]]
cross = [f"[{data[f]['title']}](./{f})" for f in cross_dim[:2]]
if related or cross:
md += "\n---\n"
if related:
md += f"💡 **Explore Related:** {' | '.join(related)}\n\n"
if cross:
md += f"🔗 **See Also:** {' | '.join(cross)}\n\n"
# Smart Write: Only update disk if content changed
target_path = os.path.join(V2_DIR, f_name)
@@ -1284,10 +1463,13 @@ class V2VisionEngine:
try:
with open("v2-mkdocs.yml", "r") as f: content = f.read()
nav = [
"nav:",
" - \"🔙 Back to V1 (Exhaustive)\": https://nubenetes.com/v1/",
"nav:",
" - \"🔙 Back to V1 (Exhaustive)\": https://nubenetes.com/v1/",
" - \"The 2026 Vision\": index.md",
" - \"Technical Tags\": tags.md",
" - \"Intelligence Digest\":",
" - \"Tech & Cloud Digest\": tech-digest.md",
" - \"Industry & Geo Digest\": industry-digest.md",
" - \"Agentic Video Hub\":",
" - videos/index.md",
" - \"AI Agents and MCP\": videos/ai-agents.md",
@@ -1309,7 +1491,8 @@ class V2VisionEngine:
nav.extend(dim_nav)
updated = re.sub(r'nav:.*', "\n".join(nav), content, flags=re.DOTALL)
with open("v2-mkdocs.yml", "w") as f: f.write(updated)
except: pass
except Exception as e:
log_event(f"[WARN] sync enterprise navigation: {str(e)[:100]}")
import argparse
if __name__ == "__main__":
@@ -1361,7 +1544,8 @@ if __name__ == "__main__":
with open(os.path.join(V2_DIR, f), "r") as doc:
line = doc.readline()
if line.startswith("# "): title = line.replace("# ", "").strip()
except: pass
except Exception as e:
log_event(f"[WARN] extract title from V2 file {f}: {str(e)[:100]}")
file_list_md += f"| {i} | `{f}` | {title} |\n"
# 3. Decision Matrix (Maturity Audit)
+28 -9
View File
@@ -78,22 +78,41 @@ def extract_youtube_id(url):
def get_target_file(category, technology):
cat_lower = category.lower()
tech_lower = technology.lower()
if "agent" in tech_lower or "mcp" in tech_lower or "ai and future operations" in cat_lower:
if "agent" in tech_lower or "mcp" in tech_lower or "ai and future operations" in cat_lower or "llm" in tech_lower or "chatgpt" in tech_lower:
return "ai-agents.md", "AI Agents and MCP"
elif "infrastructure as code" in cat_lower or "security" in cat_lower or "observability" in cat_lower or "monitoring" in cat_lower or "devops" in tech_lower or "iac" in tech_lower or "sre" in tech_lower:
elif "mlops" in tech_lower or "data science" in cat_lower or "machine learning" in tech_lower:
return "ai-agents.md", "AI Agents and MCP"
elif "security" in cat_lower or "devsecops" in tech_lower or "zero trust" in tech_lower or "vulnerability" in tech_lower:
return "devops-iac.md", "DevOps, IaC, and SRE"
elif "fundamentals" in cat_lower:
elif "infrastructure as code" in cat_lower or "observability" in cat_lower or "monitoring" in cat_lower or "devops" in tech_lower or "iac" in tech_lower or "sre" in tech_lower:
return "devops-iac.md", "DevOps, IaC, and SRE"
elif "terraform" in tech_lower or "ansible" in tech_lower or "pulumi" in tech_lower or "crossplane" in tech_lower:
return "devops-iac.md", "DevOps, IaC, and SRE"
elif "gitops" in tech_lower or "argo" in tech_lower or "flux" in tech_lower or "tekton" in tech_lower or "jenkins" in tech_lower or "cicd" in tech_lower:
return "devops-iac.md", "DevOps, IaC, and SRE"
elif "prometheus" in tech_lower or "grafana" in tech_lower or "opentelemetry" in tech_lower or "otel" in tech_lower:
return "devops-iac.md", "DevOps, IaC, and SRE"
elif "finops" in tech_lower or "cost" in tech_lower or "kubecost" in tech_lower:
return "devops-iac.md", "DevOps, IaC, and SRE"
elif "fundamentals" in cat_lower or "certification" in tech_lower or "cka" in tech_lower or "training" in cat_lower:
return "fundamentals.md", "Fundamentals"
elif "aws" in tech_lower or "azure" in tech_lower or "gcp" in tech_lower or "google cloud" in tech_lower:
return "cloud-native.md", "Cloud Native Core"
elif "openshift" in tech_lower or "red hat" in tech_lower or "rancher" in tech_lower:
return "cloud-native.md", "Cloud Native Core"
elif "vmware" in tech_lower or "proxmox" in tech_lower or "virtualization" in tech_lower:
return "cloud-native.md", "Cloud Native Core"
elif "docker" in tech_lower or "container" in tech_lower or "podman" in tech_lower:
return "cloud-native.md", "Cloud Native Core"
elif "python" in tech_lower or "golang" in tech_lower or "java" in tech_lower or "javascript" in tech_lower:
return "fundamentals.md", "Fundamentals"
else:
return "cloud-native.md", "Cloud Native Core"
def generate_v2_videos():
if not os.path.exists(INVENTORY_PATH):
return
with open(INVENTORY_PATH, "r") as f:
inventory = yaml.safe_load(f)
from src.inventory_manager import load_inventory
inventory = load_inventory()
featured_videos = []
for url, entry in inventory.items():
+9
View File
@@ -0,0 +1,9 @@
# 🌍 Nubenetes Industry & Geo Intelligence Digest
!!! tip "Nubenetes Intelligence Digest"
AI-curated ranking of the most impactful resources, updated monthly.
!!! info "Coming Soon"
The Industry & Geo Digest will be populated automatically when the monthly pipeline runs with Gemini AI ranking. Check back after the next curation cycle.
**4 geographic categories** (Americas, Europe, Spain, Asia-Pacific) will surface industry-relevant resources classified by company and region.
+9
View File
@@ -0,0 +1,9 @@
# 📊 Nubenetes Tech & Cloud Intelligence Digest
!!! tip "Nubenetes Intelligence Digest"
AI-curated ranking of the most impactful resources, updated monthly.
!!! info "Coming Soon"
The Intelligence Digest will be populated automatically when the monthly pipeline runs with Gemini AI ranking. Check back after the next curation cycle.
**26 categories** across Tech Core, Platform & Ops, and Cloud & Enterprise dimensions will surface the most relevant resources from the last 3, 6, and 12 months.
+46 -15
View File
@@ -12,6 +12,7 @@ edit_uri: "edit/master/v2-docs/"
theme:
name: material
custom_dir: docs/overrides
language: en
favicon: images/favicon-ultra.png
palette:
@@ -35,6 +36,11 @@ theme:
- navigation.sections
- navigation.expand
- navigation.indexes
- navigation.instant
- navigation.instant.prefetch
- navigation.path
- navigation.footer
- announce.dismiss
- search.suggest
- search.highlight
- search.share
@@ -45,7 +51,7 @@ theme:
- navigation.prune
- toc.integrate
plugins:
plugins:
- search
- privacy
- social:
@@ -55,8 +61,29 @@ theme:
font_family: "Inter"
custom_dir: "docs/images/"
logo: favicon-ultra.png
- tags:
tags_file: tags.md
- minify:
minify_html: true
- rss:
match_path: "(tech-digest|industry-digest).*"
date_from_meta:
as_creation: "date"
abstract_chars_count: 200
- redirects:
redirect_maps: {}
redirect_maps:
jvm-parameters-matrix-table.md: java-and-java-performance-optimization.md
private-cloud-solutions.md: kubernetes-on-premise.md
stackstorm.md: cicd.md
chef.md: ansible.md
newsql.md: databases.md
scaleway.md: digitalocean.md
xamarin.md: dotnet.md
dom.md: javascript.md
react.md: javascript.md
oauth.md: securityascode.md
digital-money.md: finops.md
aws-spain.md: aws.md
extra:
social:
@@ -72,7 +99,7 @@ extra:
extra_css:
- https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap
- static/extra.css
- static/v2_elite.css?v=2.3.44
- static/v2_elite.css?v=2.4.0
extra_javascript:
- static/v2_filter.js
@@ -95,6 +122,19 @@ markdown_extensions:
- pymdownx.tabbed:
alternate_style: true
- pymdownx.mark
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.keys
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.smartsymbols
- pymdownx.caret
- pymdownx.tilde
- tables
- footnotes
- abbr
- def_list
nav:
- "🔙 Back to V1 (Exhaustive)": https://nubenetes.com/v1/
@@ -106,6 +146,9 @@ nav:
- "DevOps, IaC, and SRE": videos/devops-iac.md
- "Cloud Native Core": videos/cloud-native.md
- "Fundamentals": videos/fundamentals.md
- "Intelligence Digest":
- "Tech & Cloud Digest": tech-digest.md
- "Industry & Geo Digest": industry-digest.md
- "AI":
- "AI Agents MCP": ai-agents-mcp.md
- "AI": ai.md
@@ -147,14 +190,12 @@ nav:
- "Testops": testops.md
- "Hardened Infrastructure":
- "Ansible": ansible.md
- "Chef": chef.md
- "Crossplane": crossplane.md
- "Devsecops": devsecops.md
- "IaC": iac.md
- "Kubernetes Security": kubernetes-security.md
- "Kustomize": kustomize.md
- "Liquibase": liquibase.md
- "Oauth": oauth.md
- "Pulumi": pulumi.md
- "Securityascode": securityascode.md
- "Terraform": terraform.md
@@ -175,7 +216,6 @@ nav:
- "AWS Pricing": aws-pricing.md
- "AWS Security": aws-security.md
- "AWS Serverless": aws-serverless.md
- "AWS Spain": aws-spain.md
- "AWS Storage": aws-storage.md
- "AWS Tools Scripts": aws-tools-scripts.md
- "AWS Training": aws-training.md
@@ -186,9 +226,7 @@ nav:
- "Ibm_Cloud": ibm_cloud.md
- "Managed Kubernetes In Public Cloud": managed-kubernetes-in-public-cloud.md
- "Oraclecloud": oraclecloud.md
- "Private Cloud Solutions": private-cloud-solutions.md
- "Public Cloud Solutions": public-cloud-solutions.md
- "Scaleway": scaleway.md
- "Networking & Service Mesh":
- "Caching": caching.md
- "Cloudflare": cloudflare.md
@@ -223,7 +261,6 @@ nav:
- "Crunchydata": crunchydata.md
- "Databases": databases.md
- "Message Queue": message-queue.md
- "Newsql": newsql.md
- "NoSQL": nosql.md
- "Yaml": yaml.md
- "Engineering Pipeline":
@@ -238,14 +275,12 @@ nav:
- "Openshift Pipelines": openshift-pipelines.md
- "Registries": registries.md
- "Sonarqube": sonarqube.md
- "Stackstorm": stackstorm.md
- "Tekton": tekton.md
- "Developer Ecosystem":
- "Chromedevtools": ChromeDevTools.md
- "Angular": angular.md
- "API": api.md
- "Devel Sites": devel-sites.md
- "Dom": dom.md
- "Dotnet": dotnet.md
- "Embedded Servlet Containers": embedded-servlet-containers.md
- "Golang": golang.md
@@ -253,20 +288,16 @@ nav:
- "Java_App_Servers": java_app_servers.md
- "Java_Frameworks": java_frameworks.md
- "Javascript": javascript.md
- "JVM Parameters Matrix Table": jvm-parameters-matrix-table.md
- "Linux Dev Env": linux-dev-env.md
- "Lowcode Nocode": lowcode-nocode.md
- "Maven Gradle": maven-gradle.md
- "Postman": postman.md
- "Python": python.md
- "React": react.md
- "Swagger Code Generator For Rest APIs": swagger-code-generator-for-rest-apis.md
- "Visual Studio": visual-studio.md
- "Web3": web3.md
- "Xamarin": xamarin.md
- "Career & Industry":
- "Appointment Scheduling": appointment-scheduling.md
- "Digital Money": digital-money.md
- "Elearning": elearning.md
- "Finops": finops.md
- "Freelancing": freelancing.md