@tyroneross/build-loop 0.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.codex-plugin/plugin.json +41 -0
- package/.cursor/rules/build-loop-surface.mdc +22 -0
- package/AGENTS.md +498 -0
- package/LICENSE +202 -0
- package/README.md +403 -0
- package/agents/alignment-checker.md +124 -0
- package/agents/api-assessor.md +136 -0
- package/agents/architecture-scout.md +185 -0
- package/agents/assessment-orchestrator.md +163 -0
- package/agents/build-orchestrator.md +198 -0
- package/agents/database-assessor.md +133 -0
- package/agents/design-contract-specialist.md +239 -0
- package/agents/fact-checker.md +69 -0
- package/agents/fix-critique.md +161 -0
- package/agents/frontend-assessor.md +136 -0
- package/agents/implementer.md +185 -0
- package/agents/independent-auditor.md +143 -0
- package/agents/mock-scanner.md +78 -0
- package/agents/optimize-runner.md +112 -0
- package/agents/overfitting-reviewer.md +96 -0
- package/agents/performance-assessor.md +159 -0
- package/agents/plan-critic.md +85 -0
- package/agents/promotion-reviewer.md +121 -0
- package/agents/recurring-pattern-detector.md +248 -0
- package/agents/retrospective-synthesizer.md +109 -0
- package/agents/root-cause-investigator.md +285 -0
- package/agents/scope-auditor.md +121 -0
- package/agents/security-reviewer.md +151 -0
- package/agents/self-improvement-architect.md +125 -0
- package/agents/synthesis-critic.md +99 -0
- package/agents/transcript-pattern-miner.md +165 -0
- package/agents/ui-validator.md +193 -0
- package/codex-skills/build-loop/SKILL.md +20 -0
- package/codex-skills/debug-loop/SKILL.md +18 -0
- package/codex-skills/handoff/SKILL.md +22 -0
- package/codex-skills/knowledge/SKILL.md +21 -0
- package/codex-skills/optimize/SKILL.md +18 -0
- package/codex-skills/research/SKILL.md +18 -0
- package/commands/agent-rally-point.md +301 -0
- package/commands/assess.md +41 -0
- package/commands/debug.md +20 -0
- package/commands/debugger-detail.md +34 -0
- package/commands/debugger-scan.md +15 -0
- package/commands/debugger-status.md +20 -0
- package/commands/debugger.md +32 -0
- package/commands/handoff.md +113 -0
- package/commands/knowledge-review.md +16 -0
- package/commands/optimize.md +31 -0
- package/commands/plan-verify.md +39 -0
- package/commands/promote-experiment.md +129 -0
- package/commands/research.md +22 -0
- package/commands/run.md +12 -0
- package/commands/self-review.md +51 -0
- package/commands/start-prd.md +57 -0
- package/commands/test.md +34 -0
- package/dist/src/assessment-orchestrator.d.ts +78 -0
- package/dist/src/assessment-orchestrator.d.ts.map +1 -0
- package/dist/src/assessment-orchestrator.js +304 -0
- package/dist/src/assessment-orchestrator.js.map +1 -0
- package/dist/src/audit-miner.d.ts +21 -0
- package/dist/src/audit-miner.d.ts.map +1 -0
- package/dist/src/audit-miner.js +421 -0
- package/dist/src/audit-miner.js.map +1 -0
- package/dist/src/batch-operations.d.ts +30 -0
- package/dist/src/batch-operations.d.ts.map +1 -0
- package/dist/src/batch-operations.js +339 -0
- package/dist/src/batch-operations.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +176 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-engine.d.ts +45 -0
- package/dist/src/context-engine.d.ts.map +1 -0
- package/dist/src/context-engine.js +135 -0
- package/dist/src/context-engine.js.map +1 -0
- package/dist/src/debug-wrapper.d.ts +58 -0
- package/dist/src/debug-wrapper.d.ts.map +1 -0
- package/dist/src/debug-wrapper.js +285 -0
- package/dist/src/debug-wrapper.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +149 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interactive-verifier.d.ts +26 -0
- package/dist/src/interactive-verifier.d.ts.map +1 -0
- package/dist/src/interactive-verifier.js +435 -0
- package/dist/src/interactive-verifier.js.map +1 -0
- package/dist/src/lessons.d.ts +59 -0
- package/dist/src/lessons.d.ts.map +1 -0
- package/dist/src/lessons.js +172 -0
- package/dist/src/lessons.js.map +1 -0
- package/dist/src/log-reader.d.ts +39 -0
- package/dist/src/log-reader.d.ts.map +1 -0
- package/dist/src/log-reader.js +291 -0
- package/dist/src/log-reader.js.map +1 -0
- package/dist/src/logger.d.ts +59 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +328 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/parallel-retrieval.d.ts +52 -0
- package/dist/src/parallel-retrieval.d.ts.map +1 -0
- package/dist/src/parallel-retrieval.js +309 -0
- package/dist/src/parallel-retrieval.js.map +1 -0
- package/dist/src/pattern-extractor.d.ts +30 -0
- package/dist/src/pattern-extractor.d.ts.map +1 -0
- package/dist/src/pattern-extractor.js +386 -0
- package/dist/src/pattern-extractor.js.map +1 -0
- package/dist/src/result-aggregator.d.ts +44 -0
- package/dist/src/result-aggregator.d.ts.map +1 -0
- package/dist/src/result-aggregator.js +315 -0
- package/dist/src/result-aggregator.js.map +1 -0
- package/dist/src/retrieval.d.ts +139 -0
- package/dist/src/retrieval.d.ts.map +1 -0
- package/dist/src/retrieval.js +724 -0
- package/dist/src/retrieval.js.map +1 -0
- package/dist/src/setup/auto-setup.d.ts +15 -0
- package/dist/src/setup/auto-setup.d.ts.map +1 -0
- package/dist/src/setup/auto-setup.js +157 -0
- package/dist/src/setup/auto-setup.js.map +1 -0
- package/dist/src/setup/configure-hooks.d.ts +8 -0
- package/dist/src/setup/configure-hooks.d.ts.map +1 -0
- package/dist/src/setup/configure-hooks.js +16 -0
- package/dist/src/setup/configure-hooks.js.map +1 -0
- package/dist/src/setup/create-slash-commands.d.ts +8 -0
- package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
- package/dist/src/setup/create-slash-commands.js +101 -0
- package/dist/src/setup/create-slash-commands.js.map +1 -0
- package/dist/src/setup/index.d.ts +8 -0
- package/dist/src/setup/index.d.ts.map +1 -0
- package/dist/src/setup/index.js +17 -0
- package/dist/src/setup/index.js.map +1 -0
- package/dist/src/setup/inject-claude-md.d.ts +10 -0
- package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
- package/dist/src/setup/inject-claude-md.js +150 -0
- package/dist/src/setup/inject-claude-md.js.map +1 -0
- package/dist/src/setup/uninstall.d.ts +22 -0
- package/dist/src/setup/uninstall.d.ts.map +1 -0
- package/dist/src/setup/uninstall.js +235 -0
- package/dist/src/setup/uninstall.js.map +1 -0
- package/dist/src/storage.d.ts +192 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +1037 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/string-similarity.d.ts +8 -0
- package/dist/src/string-similarity.d.ts.map +1 -0
- package/dist/src/string-similarity.js +57 -0
- package/dist/src/string-similarity.js.map +1 -0
- package/dist/src/traces/adapters/browser.d.ts +69 -0
- package/dist/src/traces/adapters/browser.d.ts.map +1 -0
- package/dist/src/traces/adapters/browser.js +280 -0
- package/dist/src/traces/adapters/browser.js.map +1 -0
- package/dist/src/traces/adapters/index.d.ts +23 -0
- package/dist/src/traces/adapters/index.d.ts.map +1 -0
- package/dist/src/traces/adapters/index.js +67 -0
- package/dist/src/traces/adapters/index.js.map +1 -0
- package/dist/src/traces/adapters/langchain.d.ts +42 -0
- package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
- package/dist/src/traces/adapters/langchain.js +151 -0
- package/dist/src/traces/adapters/langchain.js.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.js +164 -0
- package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
- package/dist/src/traces/adapters/sentry.d.ts +58 -0
- package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
- package/dist/src/traces/adapters/sentry.js +212 -0
- package/dist/src/traces/adapters/sentry.js.map +1 -0
- package/dist/src/traces/index.d.ts +10 -0
- package/dist/src/traces/index.d.ts.map +1 -0
- package/dist/src/traces/index.js +45 -0
- package/dist/src/traces/index.js.map +1 -0
- package/dist/src/traces/storage.d.ts +78 -0
- package/dist/src/traces/storage.d.ts.map +1 -0
- package/dist/src/traces/storage.js +399 -0
- package/dist/src/traces/storage.js.map +1 -0
- package/dist/src/traces/summarizer.d.ts +83 -0
- package/dist/src/traces/summarizer.d.ts.map +1 -0
- package/dist/src/traces/summarizer.js +247 -0
- package/dist/src/traces/summarizer.js.map +1 -0
- package/dist/src/traces/types.d.ts +365 -0
- package/dist/src/traces/types.d.ts.map +1 -0
- package/dist/src/traces/types.js +14 -0
- package/dist/src/traces/types.js.map +1 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +27 -0
- package/dist/src/types.js.map +1 -0
- package/docs/agent-surface-policy.md +45 -0
- package/docs/memory-setup.md +226 -0
- package/hooks/_arch_scan_bg.py +88 -0
- package/hooks/_plugin_drift_check_bg.py +85 -0
- package/hooks/_session_start_lib.sh +42 -0
- package/hooks/git/pre-push +141 -0
- package/hooks/hooks.json +126 -0
- package/hooks/post-commit +30 -0
- package/hooks/pre-commit +29 -0
- package/hooks/pre-edit-apppulse.sh +6 -0
- package/hooks/pre-edit-architecture.sh +106 -0
- package/hooks/pre-edit-rally-point.sh +92 -0
- package/hooks/session-start-apppulse.sh +6 -0
- package/hooks/session-start-architecture.sh +10 -0
- package/hooks/session-start-git-hooks.sh +12 -0
- package/hooks/session-start-plugin-drift.sh +27 -0
- package/hooks/session-start-rally-point.sh +44 -0
- package/hooks/session-start-retrieval.sh +24 -0
- package/hooks/session-start-worktree-gc.sh +105 -0
- package/hooks/test_apppulse_hooks.sh +6 -0
- package/hooks/test_rally_point_hooks.sh +388 -0
- package/hooks/test_sec008_hooks.sh +66 -0
- package/package.json +111 -0
- package/scripts/README.md +301 -0
- package/scripts/_db_url.py +61 -0
- package/scripts/_paths.py +445 -0
- package/scripts/blm.py +162 -0
- package/scripts/blm_api.py +263 -0
- package/scripts/check_cache_sync.py +490 -0
- package/scripts/embed_backend.py +475 -0
- package/scripts/install_memory.py +556 -0
- package/scripts/lessons_index/__init__.py +77 -0
- package/scripts/lessons_index/__main__.py +135 -0
- package/scripts/lessons_index/ingest.py +288 -0
- package/scripts/lessons_index/query.py +285 -0
- package/scripts/lessons_index/schema.py +114 -0
- package/scripts/memory_context/__init__.py +610 -0
- package/scripts/memory_graph/__init__.py +393 -0
- package/scripts/project_resolver.py +172 -0
- package/scripts/prune_codex_plugin_cache.py +23 -0
- package/scripts/prune_plugin_cache.py +359 -0
- package/scripts/semantic_index/__init__.py +482 -0
- package/scripts/semantic_index/_bench_hybrid.py +175 -0
- package/scripts/semantic_index/backfill.py +263 -0
- package/scripts/semantic_index/hybrid.py +174 -0
- package/scripts/semantic_index/test_backfill.py +292 -0
- package/scripts/semantic_index/test_hybrid.py +541 -0
- package/scripts/sync_navgator_lessons.py +666 -0
- package/scripts/sync_plugin_cache.py +388 -0
- package/skills/agent-rally-point/SKILL.md +165 -0
- package/skills/agent-rally-watcher/SKILL.md +83 -0
- package/skills/api-registry-bridge/SKILL.md +59 -0
- package/skills/architecture/dead/SKILL.md +56 -0
- package/skills/architecture/impact/SKILL.md +88 -0
- package/skills/architecture/review/SKILL.md +227 -0
- package/skills/architecture/rules/SKILL.md +72 -0
- package/skills/architecture/scan/SKILL.md +79 -0
- package/skills/architecture/trace/SKILL.md +65 -0
- package/skills/attribution-standard/SKILL.md +122 -0
- package/skills/authentication/SKILL.md +81 -0
- package/skills/authentication/references/better-auth-magic-link.md +135 -0
- package/skills/authentication/references/better-auth-setup.md +224 -0
- package/skills/authentication/references/google-calendar-sync.md +278 -0
- package/skills/authentication/references/google-cloud-console.md +159 -0
- package/skills/authentication/references/google-geocoding-directions.md +126 -0
- package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
- package/skills/authentication/references/google-maps.md +162 -0
- package/skills/authentication/references/google-oauth-setup.md +343 -0
- package/skills/authentication/references/google-places.md +173 -0
- package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
- package/skills/authentication/references/resend-email.md +227 -0
- package/skills/authentication/references/resend-otp-magic-link.md +153 -0
- package/skills/authentication/references/supabase-auth.md +172 -0
- package/skills/auto-decision-capture/SKILL.md +419 -0
- package/skills/build-loop/SKILL.md +516 -0
- package/skills/build-loop/detect-plugins.mjs +82 -0
- package/skills/build-loop/eval-guide.md +65 -0
- package/skills/build-loop/fallbacks.md +549 -0
- package/skills/build-loop/phases/fact-check.md +42 -0
- package/skills/build-loop/phases/ui-validation.md +267 -0
- package/skills/build-loop/references/apple-native-planning.md +439 -0
- package/skills/build-loop/references/capability-routing.md +181 -0
- package/skills/build-loop/references/codex-subagents.md +98 -0
- package/skills/build-loop/references/coordination.md +161 -0
- package/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/skills/build-loop/references/independent-auditor.md +72 -0
- package/skills/build-loop/references/intent-capability-pack.md +170 -0
- package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/skills/build-loop/references/leadership.md +72 -0
- package/skills/build-loop/references/memory.md +284 -0
- package/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/skills/build-loop/references/output-style.md +129 -0
- package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/skills/build-loop/references/phase-1-assess.md +218 -0
- package/skills/build-loop/references/phase-2-plan.md +77 -0
- package/skills/build-loop/references/phase-3-execute.md +47 -0
- package/skills/build-loop/references/phase-4-review.md +310 -0
- package/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/skills/build-loop/references/phase-6-learn.md +56 -0
- package/skills/build-loop/references/recent-design-structures.md +274 -0
- package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/skills/build-loop/references/self-review.md +232 -0
- package/skills/build-loop/references/ui-io-contract.md +116 -0
- package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/skills/build-loop/templates/backlog-item.md +35 -0
- package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
- package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/skills/building-with-deepagents/SKILL.md +195 -0
- package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
- package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
- package/skills/capabilities/SKILL.md +91 -0
- package/skills/debug-loop/SKILL.md +278 -0
- package/skills/debug-loop/references/convergence-rules.md +145 -0
- package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
- package/skills/debugging/assess/SKILL.md +111 -0
- package/skills/debugging/debug-loop/SKILL.md +231 -0
- package/skills/debugging/memory/SKILL.md +147 -0
- package/skills/debugging/store/SKILL.md +153 -0
- package/skills/debugging-memory/SKILL.md +440 -0
- package/skills/debugging-memory/examples/incident-example.json +67 -0
- package/skills/debugging-memory/references/incident-documentation.md +280 -0
- package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
- package/skills/debugging-memory/references/pattern-extraction.md +163 -0
- package/skills/debugging-memory/references/subagent-integration.md +312 -0
- package/skills/defenseclaw-bridge/SKILL.md +156 -0
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
- package/skills/defenseclaw-bridge/references/output-format.md +108 -0
- package/skills/handoff/SKILL.md +94 -0
- package/skills/ibr-bridge/SKILL.md +68 -0
- package/skills/knowledge/SKILL.md +127 -0
- package/skills/knowledge/references/recall-integration.md +83 -0
- package/skills/knowledge/templates/madr-minimal.md +56 -0
- package/skills/knowledge-review/SKILL.md +112 -0
- package/skills/logging-tracer/SKILL.md +202 -0
- package/skills/logging-tracer/references/log-analysis.md +128 -0
- package/skills/logging-tracer/references/stack-templates.md +274 -0
- package/skills/logging-tracer-bridge/SKILL.md +70 -0
- package/skills/mcp-builder/SKILL.md +278 -0
- package/skills/mcp-builder/references/mcp-security.md +106 -0
- package/skills/model-tiering/SKILL.md +216 -0
- package/skills/native-ax-driver/SKILL.md +220 -0
- package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
- package/skills/native-ax-driver/scripts/native_driver.py +495 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
- package/skills/optimize/SKILL.md +202 -0
- package/skills/optimize/profiles.md +61 -0
- package/skills/plan-verify/SKILL.md +115 -0
- package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
- package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
- package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
- package/skills/plugin-builder/SKILL.md +329 -0
- package/skills/plugin-builder/references/authoritative-sources.md +104 -0
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
- package/skills/plugin-builder/references/components-guide.md +221 -0
- package/skills/plugin-builder/references/distribution.md +317 -0
- package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
- package/skills/plugin-builder/references/hooks-reference.md +210 -0
- package/skills/plugin-builder/references/manifest-schema.md +111 -0
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
- package/skills/plugin-tests/SKILL.md +94 -0
- package/skills/prd-bridge/SKILL.md +135 -0
- package/skills/research/SKILL.md +80 -0
- package/skills/security-methodology/SKILL.md +105 -0
- package/skills/security-methodology/references/cross-source-matrix.md +77 -0
- package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
- package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
- package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
- package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
- package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
- package/skills/self-improve/SKILL.md +298 -0
- package/skills/spec-writing/SKILL.md +548 -0
- package/skills/spec-writing/scripts/check_checklist.py +478 -0
- package/skills/sync-skills/SKILL.md +132 -0
- package/skills/telemetry/SKILL.md +66 -0
- package/skills/ui-design/SKILL.md +75 -0
- package/skills/ui-design/references/design-patterns-multi.md +165 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
- package/skills/ui-design/references/evidence-capture-policy.md +55 -0
- package/skills/ui-design/references/ui-guidance-sources.md +330 -0
- package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
- package/skills/ui-design/references/universal-design-principles.md +419 -0
- package/templates/memory/MEMORY.md.template +55 -0
- package/templates/memory/constitution.md.template +64 -0
- package/templates/memory/manifest.json +47 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Portable graph adapter for build-loop-memory.
|
|
5
|
+
|
|
6
|
+
The canonical graph is the rebuildable JSONL pair in
|
|
7
|
+
``<memory-root>/indexes/graph-nodes.jsonl`` and ``graph-edges.jsonl``.
|
|
8
|
+
``sqlite_edges`` is the default runtime backend: it mirrors that JSONL into a
|
|
9
|
+
small SQLite index and answers bounded related-node traversals. ``ladybug`` is
|
|
10
|
+
opt-in only; when unavailable or smoke-gate incompatible, callers fall back to
|
|
11
|
+
``sqlite_edges`` with a reason in the response.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
import sys
|
|
19
|
+
from collections import deque
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Iterable
|
|
22
|
+
|
|
23
|
+
HERE = Path(__file__).resolve().parent
|
|
24
|
+
SCRIPTS_DIR = HERE.parent
|
|
25
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
26
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
27
|
+
|
|
28
|
+
from _paths import memory_indexes_dir, memory_store_root # type: ignore # noqa: E402
|
|
29
|
+
|
|
30
|
+
SCHEMA_VERSION = "1.0.0"
|
|
31
|
+
DEFAULT_BACKEND = "sqlite_edges"
|
|
32
|
+
VALID_DIRECTIONS = {"out", "in", "both"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
36
|
+
rows: list[dict[str, Any]] = []
|
|
37
|
+
if not path.is_file():
|
|
38
|
+
return rows
|
|
39
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
40
|
+
raw = raw.strip()
|
|
41
|
+
if not raw:
|
|
42
|
+
continue
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads(raw)
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
continue
|
|
47
|
+
if isinstance(data, dict):
|
|
48
|
+
rows.append(data)
|
|
49
|
+
return rows
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _source_signature(nodes_path: Path, edges_path: Path) -> str:
|
|
53
|
+
parts: list[dict[str, Any]] = []
|
|
54
|
+
for path in (nodes_path, edges_path):
|
|
55
|
+
try:
|
|
56
|
+
stat = path.stat()
|
|
57
|
+
except OSError:
|
|
58
|
+
parts.append({"path": str(path), "exists": False})
|
|
59
|
+
continue
|
|
60
|
+
parts.append(
|
|
61
|
+
{
|
|
62
|
+
"path": str(path),
|
|
63
|
+
"exists": True,
|
|
64
|
+
"mtime_ns": stat.st_mtime_ns,
|
|
65
|
+
"size": stat.st_size,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
return json.dumps(parts, sort_keys=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _text(value: Any, default: str = "") -> str:
|
|
72
|
+
if value is None:
|
|
73
|
+
return default
|
|
74
|
+
return str(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _node_from_row(row: dict[str, Any]) -> dict[str, Any] | None:
|
|
78
|
+
node_id = _text(row.get("id") or row.get("canonical_id") or row.get("path")).strip()
|
|
79
|
+
if not node_id:
|
|
80
|
+
return None
|
|
81
|
+
return {
|
|
82
|
+
"id": node_id,
|
|
83
|
+
"title": _text(row.get("title") or row.get("name") or node_id, node_id),
|
|
84
|
+
"path": _text(row.get("path") or row.get("canonical_path")),
|
|
85
|
+
"project": _text(row.get("project")),
|
|
86
|
+
"memory_type": _text(row.get("memory_type") or row.get("type")),
|
|
87
|
+
"status": _text(row.get("status")),
|
|
88
|
+
"data": row,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _edge_from_row(row: dict[str, Any]) -> dict[str, Any] | None:
|
|
93
|
+
source = _text(row.get("source")).strip()
|
|
94
|
+
target = _text(row.get("target")).strip()
|
|
95
|
+
if not source or not target:
|
|
96
|
+
return None
|
|
97
|
+
edge_id = _text(row.get("id") or f"{source}->{target}:{row.get('relation', '')}")
|
|
98
|
+
return {
|
|
99
|
+
"id": edge_id,
|
|
100
|
+
"source": source,
|
|
101
|
+
"target": target,
|
|
102
|
+
"relation": _text(row.get("relation") or row.get("type") or "related"),
|
|
103
|
+
"data": row,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SQLiteEdgesStore:
|
|
108
|
+
"""SQLite-backed adapter over rebuildable graph JSONL files."""
|
|
109
|
+
|
|
110
|
+
backend = DEFAULT_BACKEND
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
root: Path,
|
|
116
|
+
db_path: Path | str | None = None,
|
|
117
|
+
reasons: Iterable[str] = (),
|
|
118
|
+
) -> None:
|
|
119
|
+
self.root = root
|
|
120
|
+
self.index_dir = root / "indexes"
|
|
121
|
+
self.nodes_path = self.index_dir / "graph-nodes.jsonl"
|
|
122
|
+
self.edges_path = self.index_dir / "graph-edges.jsonl"
|
|
123
|
+
self.db_path = db_path or self.index_dir / "graph.sqlite"
|
|
124
|
+
self.reasons = list(reasons)
|
|
125
|
+
self._conn = sqlite3.connect(str(self.db_path))
|
|
126
|
+
self._conn.row_factory = sqlite3.Row
|
|
127
|
+
self._sync_if_needed()
|
|
128
|
+
|
|
129
|
+
def close(self) -> None:
|
|
130
|
+
self._conn.close()
|
|
131
|
+
|
|
132
|
+
def _ensure_schema(self) -> None:
|
|
133
|
+
self._conn.executescript(
|
|
134
|
+
"""
|
|
135
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
136
|
+
key TEXT PRIMARY KEY,
|
|
137
|
+
value TEXT NOT NULL
|
|
138
|
+
);
|
|
139
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
140
|
+
id TEXT PRIMARY KEY,
|
|
141
|
+
title TEXT NOT NULL DEFAULT '',
|
|
142
|
+
path TEXT NOT NULL DEFAULT '',
|
|
143
|
+
project TEXT NOT NULL DEFAULT '',
|
|
144
|
+
memory_type TEXT NOT NULL DEFAULT '',
|
|
145
|
+
status TEXT NOT NULL DEFAULT '',
|
|
146
|
+
data TEXT NOT NULL DEFAULT '{}'
|
|
147
|
+
);
|
|
148
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
149
|
+
id TEXT PRIMARY KEY,
|
|
150
|
+
source TEXT NOT NULL,
|
|
151
|
+
target TEXT NOT NULL,
|
|
152
|
+
relation TEXT NOT NULL DEFAULT 'related',
|
|
153
|
+
data TEXT NOT NULL DEFAULT '{}'
|
|
154
|
+
);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation);
|
|
158
|
+
"""
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _sync_if_needed(self) -> None:
|
|
162
|
+
self.index_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
self._ensure_schema()
|
|
164
|
+
signature = _source_signature(self.nodes_path, self.edges_path)
|
|
165
|
+
current = self._conn.execute(
|
|
166
|
+
"SELECT value FROM meta WHERE key = 'source_signature'"
|
|
167
|
+
).fetchone()
|
|
168
|
+
if current and current["value"] == signature:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
node_rows = [_node_from_row(row) for row in _read_jsonl(self.nodes_path)]
|
|
172
|
+
edge_rows = [_edge_from_row(row) for row in _read_jsonl(self.edges_path)]
|
|
173
|
+
nodes = [row for row in node_rows if row is not None]
|
|
174
|
+
edges = [row for row in edge_rows if row is not None]
|
|
175
|
+
|
|
176
|
+
with self._conn:
|
|
177
|
+
self._conn.execute("DELETE FROM edges")
|
|
178
|
+
self._conn.execute("DELETE FROM nodes")
|
|
179
|
+
self._conn.executemany(
|
|
180
|
+
"""
|
|
181
|
+
INSERT OR REPLACE INTO nodes
|
|
182
|
+
(id, title, path, project, memory_type, status, data)
|
|
183
|
+
VALUES (:id, :title, :path, :project, :memory_type, :status, :data)
|
|
184
|
+
""",
|
|
185
|
+
[
|
|
186
|
+
{
|
|
187
|
+
**row,
|
|
188
|
+
"data": json.dumps(row["data"], sort_keys=True, default=str),
|
|
189
|
+
}
|
|
190
|
+
for row in nodes
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
self._conn.executemany(
|
|
194
|
+
"""
|
|
195
|
+
INSERT OR REPLACE INTO edges (id, source, target, relation, data)
|
|
196
|
+
VALUES (:id, :source, :target, :relation, :data)
|
|
197
|
+
""",
|
|
198
|
+
[
|
|
199
|
+
{
|
|
200
|
+
**row,
|
|
201
|
+
"data": json.dumps(row["data"], sort_keys=True, default=str),
|
|
202
|
+
}
|
|
203
|
+
for row in edges
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
for edge in edges:
|
|
207
|
+
for endpoint in (edge["source"], edge["target"]):
|
|
208
|
+
self._conn.execute(
|
|
209
|
+
"""
|
|
210
|
+
INSERT OR IGNORE INTO nodes
|
|
211
|
+
(id, title, path, project, memory_type, status, data)
|
|
212
|
+
VALUES (?, ?, '', '', '', '', '{}')
|
|
213
|
+
""",
|
|
214
|
+
(endpoint, endpoint),
|
|
215
|
+
)
|
|
216
|
+
self._conn.execute(
|
|
217
|
+
"INSERT OR REPLACE INTO meta(key, value) VALUES('source_signature', ?)",
|
|
218
|
+
(signature,),
|
|
219
|
+
)
|
|
220
|
+
self._conn.execute(
|
|
221
|
+
"INSERT OR REPLACE INTO meta(key, value) VALUES('schema_version', ?)",
|
|
222
|
+
(SCHEMA_VERSION,),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def stats(self) -> dict[str, int]:
|
|
226
|
+
nodes = self._conn.execute("SELECT COUNT(*) AS n FROM nodes").fetchone()["n"]
|
|
227
|
+
edges = self._conn.execute("SELECT COUNT(*) AS n FROM edges").fetchone()["n"]
|
|
228
|
+
return {"nodes": int(nodes), "edges": int(edges)}
|
|
229
|
+
|
|
230
|
+
def _edges_for(
|
|
231
|
+
self,
|
|
232
|
+
node_id: str,
|
|
233
|
+
*,
|
|
234
|
+
direction: str,
|
|
235
|
+
relation: str | None,
|
|
236
|
+
) -> list[sqlite3.Row]:
|
|
237
|
+
if direction not in VALID_DIRECTIONS:
|
|
238
|
+
raise ValueError(f"invalid direction {direction!r}; expected one of {sorted(VALID_DIRECTIONS)}")
|
|
239
|
+
rel_sql = " AND relation = ?" if relation else ""
|
|
240
|
+
rel_args: tuple[str, ...] = (relation,) if relation else ()
|
|
241
|
+
rows: list[sqlite3.Row] = []
|
|
242
|
+
if direction in {"out", "both"}:
|
|
243
|
+
rows.extend(
|
|
244
|
+
self._conn.execute(
|
|
245
|
+
f"SELECT * FROM edges WHERE source = ?{rel_sql}",
|
|
246
|
+
(node_id, *rel_args),
|
|
247
|
+
).fetchall()
|
|
248
|
+
)
|
|
249
|
+
if direction in {"in", "both"}:
|
|
250
|
+
rows.extend(
|
|
251
|
+
self._conn.execute(
|
|
252
|
+
f"SELECT * FROM edges WHERE target = ?{rel_sql}",
|
|
253
|
+
(node_id, *rel_args),
|
|
254
|
+
).fetchall()
|
|
255
|
+
)
|
|
256
|
+
return rows
|
|
257
|
+
|
|
258
|
+
def _node(self, node_id: str) -> dict[str, Any] | None:
|
|
259
|
+
row = self._conn.execute("SELECT * FROM nodes WHERE id = ?", (node_id,)).fetchone()
|
|
260
|
+
if row is None:
|
|
261
|
+
return None
|
|
262
|
+
try:
|
|
263
|
+
data = json.loads(row["data"])
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
data = {}
|
|
266
|
+
return {
|
|
267
|
+
"id": row["id"],
|
|
268
|
+
"title": row["title"],
|
|
269
|
+
"path": row["path"],
|
|
270
|
+
"project": row["project"],
|
|
271
|
+
"memory_type": row["memory_type"],
|
|
272
|
+
"status": row["status"],
|
|
273
|
+
"data": data,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
def related(
|
|
277
|
+
self,
|
|
278
|
+
seeds: str | Iterable[str],
|
|
279
|
+
*,
|
|
280
|
+
depth: int = 1,
|
|
281
|
+
limit: int = 10,
|
|
282
|
+
direction: str = "both",
|
|
283
|
+
relation: str | None = None,
|
|
284
|
+
project: str | None = None,
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
"""Return nodes and edges reachable from ``seeds`` within ``depth``.
|
|
287
|
+
|
|
288
|
+
The response shape follows the graph-query convention
|
|
289
|
+
``MATCH (seed)-[r*1..depth]-(related) RETURN related, r`` without
|
|
290
|
+
binding callers to a non-portable graph database.
|
|
291
|
+
"""
|
|
292
|
+
self._sync_if_needed()
|
|
293
|
+
seed_list = [seeds] if isinstance(seeds, str) else [str(seed) for seed in seeds]
|
|
294
|
+
seed_list = [seed for seed in seed_list if seed]
|
|
295
|
+
max_depth = max(0, int(depth))
|
|
296
|
+
max_limit = max(0, int(limit))
|
|
297
|
+
|
|
298
|
+
queue: deque[tuple[str, int]] = deque((seed, 0) for seed in seed_list)
|
|
299
|
+
seen = set(seed_list)
|
|
300
|
+
node_hits: dict[str, dict[str, Any]] = {}
|
|
301
|
+
edge_hits: dict[str, dict[str, Any]] = {}
|
|
302
|
+
|
|
303
|
+
while queue:
|
|
304
|
+
node_id, hop = queue.popleft()
|
|
305
|
+
if hop >= max_depth:
|
|
306
|
+
continue
|
|
307
|
+
for edge in self._edges_for(node_id, direction=direction, relation=relation):
|
|
308
|
+
source = edge["source"]
|
|
309
|
+
target = edge["target"]
|
|
310
|
+
neighbor = target if source == node_id else source
|
|
311
|
+
next_hop = hop + 1
|
|
312
|
+
edge_hits.setdefault(
|
|
313
|
+
edge["id"],
|
|
314
|
+
{
|
|
315
|
+
"id": edge["id"],
|
|
316
|
+
"source": source,
|
|
317
|
+
"target": target,
|
|
318
|
+
"relation": edge["relation"],
|
|
319
|
+
"hop": next_hop,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
if neighbor not in seen:
|
|
323
|
+
seen.add(neighbor)
|
|
324
|
+
queue.append((neighbor, next_hop))
|
|
325
|
+
node = self._node(neighbor)
|
|
326
|
+
if node is None:
|
|
327
|
+
continue
|
|
328
|
+
if project and node.get("project") and node["project"] != project:
|
|
329
|
+
continue
|
|
330
|
+
prior = node_hits.get(neighbor)
|
|
331
|
+
score = 1.0 / (next_hop + 1)
|
|
332
|
+
if prior is None or next_hop < prior["hop"]:
|
|
333
|
+
node_hits[neighbor] = {**node, "hop": next_hop, "score": score}
|
|
334
|
+
elif prior is not None:
|
|
335
|
+
prior["score"] += score
|
|
336
|
+
|
|
337
|
+
nodes = [
|
|
338
|
+
row
|
|
339
|
+
for row in node_hits.values()
|
|
340
|
+
if row["id"] not in seed_list
|
|
341
|
+
]
|
|
342
|
+
nodes.sort(key=lambda row: (-float(row.get("score", 0.0)), int(row.get("hop", 0)), row.get("title", "")))
|
|
343
|
+
if max_limit:
|
|
344
|
+
nodes = nodes[:max_limit]
|
|
345
|
+
return {
|
|
346
|
+
"schema_version": SCHEMA_VERSION,
|
|
347
|
+
"backend": self.backend,
|
|
348
|
+
"query_shape": f"MATCH (seed)-[r*1..{max_depth}]-(related) RETURN related, r",
|
|
349
|
+
"seeds": seed_list,
|
|
350
|
+
"depth": max_depth,
|
|
351
|
+
"direction": direction,
|
|
352
|
+
"relation": relation,
|
|
353
|
+
"nodes": nodes,
|
|
354
|
+
"edges": list(edge_hits.values()),
|
|
355
|
+
"stats": self.stats(),
|
|
356
|
+
"reasons": list(self.reasons),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class GraphStore:
|
|
361
|
+
"""Backend-selecting graph adapter.
|
|
362
|
+
|
|
363
|
+
``sqlite_edges`` is the default. ``ladybug`` is accepted only as an opt-in
|
|
364
|
+
request and falls back automatically unless its import + smoke gate succeed.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
@classmethod
|
|
368
|
+
def open(
|
|
369
|
+
cls,
|
|
370
|
+
*,
|
|
371
|
+
root: Path | str | None = None,
|
|
372
|
+
backend: str | None = None,
|
|
373
|
+
db_path: Path | str | None = None,
|
|
374
|
+
) -> SQLiteEdgesStore:
|
|
375
|
+
memory_root = Path(root).resolve() if root else memory_store_root().resolve()
|
|
376
|
+
requested = backend or os.environ.get("BUILD_LOOP_MEMORY_GRAPH_BACKEND") or DEFAULT_BACKEND
|
|
377
|
+
reasons: list[str] = []
|
|
378
|
+
if requested in {"ladybug", "auto"}:
|
|
379
|
+
reasons.extend(_ladybug_smoke_reasons())
|
|
380
|
+
elif requested != DEFAULT_BACKEND:
|
|
381
|
+
reasons.append(f"unsupported_backend:{requested}; using {DEFAULT_BACKEND}")
|
|
382
|
+
return SQLiteEdgesStore(root=memory_root, db_path=db_path, reasons=reasons)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _ladybug_smoke_reasons() -> list[str]:
|
|
386
|
+
try:
|
|
387
|
+
__import__("ladybug")
|
|
388
|
+
except Exception as exc: # noqa: BLE001
|
|
389
|
+
return [f"ladybug_unavailable:{type(exc).__name__}"]
|
|
390
|
+
return ["ladybug_smoke_unimplemented; using sqlite_edges"]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
__all__ = ["GraphStore", "SQLiteEdgesStore"]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Resolve a current working directory to a project tag.
|
|
5
|
+
|
|
6
|
+
Reads ``<memory_store_root>/config/projects.yaml`` when present, falling
|
|
7
|
+
back to ``<memory_store_root>/.config/projects.yaml`` for migration, and returns the
|
|
8
|
+
project tag whose ``path:`` is the longest prefix match against the
|
|
9
|
+
given ``cwd``. Falls back to the YAML's ``default:`` key, which itself
|
|
10
|
+
defaults to ``_unscoped`` if absent.
|
|
11
|
+
|
|
12
|
+
Pure stdlib parser — projects.yaml is a small file we control, so we
|
|
13
|
+
parse just the subset we emit (top-level ``default:`` scalar plus a
|
|
14
|
+
``projects:`` list of ``- path: ...\\n project: ...`` blocks).
|
|
15
|
+
|
|
16
|
+
Public API:
|
|
17
|
+
resolve_project(cwd: Path) -> str
|
|
18
|
+
load_projects_yaml(path: Path | None = None) -> dict
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
HERE = Path(__file__).resolve().parent
|
|
27
|
+
import sys
|
|
28
|
+
if str(HERE) not in sys.path:
|
|
29
|
+
sys.path.insert(0, str(HERE))
|
|
30
|
+
from _paths import derive_slug_from_cwd, memory_store_root # type: ignore # noqa: E402
|
|
31
|
+
|
|
32
|
+
DEFAULT_PROJECT_TAG = "_unscoped"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _projects_yaml_path() -> Path:
|
|
36
|
+
canonical = memory_store_root() / "config" / "projects.yaml"
|
|
37
|
+
if canonical.exists():
|
|
38
|
+
return canonical
|
|
39
|
+
return memory_store_root() / ".config" / "projects.yaml"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_projects_yaml(path: Path | None = None) -> dict[str, Any]:
|
|
43
|
+
"""Parse the small subset of YAML we emit.
|
|
44
|
+
|
|
45
|
+
Returns ``{"default": <tag>, "projects": [{"path": <abs>, "project": <tag>}, ...]}``.
|
|
46
|
+
Missing file → ``{"default": "_unscoped", "projects": []}``.
|
|
47
|
+
"""
|
|
48
|
+
if path is None:
|
|
49
|
+
path = _projects_yaml_path()
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return {"default": DEFAULT_PROJECT_TAG, "projects": []}
|
|
52
|
+
text = path.read_text(encoding="utf-8")
|
|
53
|
+
return _parse_projects_yaml(text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_projects_yaml(text: str) -> dict[str, Any]:
|
|
57
|
+
"""Parse the subset of YAML used in projects.yaml.
|
|
58
|
+
|
|
59
|
+
Recognized shapes:
|
|
60
|
+
default: <scalar>
|
|
61
|
+
projects:
|
|
62
|
+
- path: <scalar>
|
|
63
|
+
project: <scalar>
|
|
64
|
+
Lines starting with ``#`` and blank lines are ignored.
|
|
65
|
+
"""
|
|
66
|
+
default_tag = DEFAULT_PROJECT_TAG
|
|
67
|
+
projects: list[dict[str, str]] = []
|
|
68
|
+
in_projects = False
|
|
69
|
+
cur: dict[str, str] | None = None
|
|
70
|
+
for raw_line in text.splitlines():
|
|
71
|
+
line = raw_line.rstrip()
|
|
72
|
+
# Strip inline comments (very simple: a `#` not inside quotes).
|
|
73
|
+
# projects.yaml never quotes values, so this is safe.
|
|
74
|
+
if "#" in line:
|
|
75
|
+
stripped = line.lstrip()
|
|
76
|
+
if stripped.startswith("#"):
|
|
77
|
+
continue
|
|
78
|
+
line = line.split("#", 1)[0].rstrip()
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
# Top-level key
|
|
82
|
+
if not line[0].isspace():
|
|
83
|
+
if line.startswith("default:"):
|
|
84
|
+
val = line.split(":", 1)[1].strip()
|
|
85
|
+
if val:
|
|
86
|
+
default_tag = val
|
|
87
|
+
in_projects = False
|
|
88
|
+
cur = None
|
|
89
|
+
elif line.startswith("projects:"):
|
|
90
|
+
in_projects = True
|
|
91
|
+
cur = None
|
|
92
|
+
else:
|
|
93
|
+
in_projects = False
|
|
94
|
+
cur = None
|
|
95
|
+
continue
|
|
96
|
+
# Indented line under projects:
|
|
97
|
+
if not in_projects:
|
|
98
|
+
continue
|
|
99
|
+
stripped = line.strip()
|
|
100
|
+
if stripped.startswith("- "):
|
|
101
|
+
# Start of a new project entry. The "- " line might itself
|
|
102
|
+
# carry the first key (e.g. "- path: ~/dev/foo").
|
|
103
|
+
if cur is not None:
|
|
104
|
+
projects.append(cur)
|
|
105
|
+
cur = {}
|
|
106
|
+
after = stripped[2:].strip()
|
|
107
|
+
if ":" in after:
|
|
108
|
+
k, _, v = after.partition(":")
|
|
109
|
+
cur[k.strip()] = v.strip()
|
|
110
|
+
elif ":" in stripped and cur is not None:
|
|
111
|
+
k, _, v = stripped.partition(":")
|
|
112
|
+
cur[k.strip()] = v.strip()
|
|
113
|
+
if cur is not None:
|
|
114
|
+
projects.append(cur)
|
|
115
|
+
# Filter out incomplete entries.
|
|
116
|
+
projects = [p for p in projects if "path" in p and "project" in p]
|
|
117
|
+
return {"default": default_tag or DEFAULT_PROJECT_TAG, "projects": projects}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _normalize(p: str | Path) -> str:
|
|
121
|
+
"""Expand ``~`` and resolve symlinks to an absolute path string."""
|
|
122
|
+
try:
|
|
123
|
+
return os.path.normpath(str(Path(os.path.expanduser(str(p))).resolve()))
|
|
124
|
+
except (OSError, RuntimeError):
|
|
125
|
+
return os.path.normpath(os.path.expanduser(str(p)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve_project(cwd: Path | str) -> str:
|
|
129
|
+
"""Return the project tag for ``cwd``.
|
|
130
|
+
|
|
131
|
+
Resolution order (post memory-consolidation PR 1):
|
|
132
|
+
1. ``derive_slug_from_cwd(cwd)`` — filesystem-driven slug from git
|
|
133
|
+
toplevel + ``_safe_project_tag`` normalization. The PRIMARY path.
|
|
134
|
+
Returns ``_unscoped`` when ``cwd`` is outside any git repo.
|
|
135
|
+
2. ``projects.yaml`` lookup (exact then longest-prefix) — used ONLY
|
|
136
|
+
when (a) the filesystem rule returned ``_unscoped`` AND (b) the
|
|
137
|
+
operator has registered an explicit alias for this cwd. This
|
|
138
|
+
covers the rare case where the working directory isn't a git
|
|
139
|
+
repo but should still map to a known project tag.
|
|
140
|
+
3. ``projects.yaml`` ``default:`` key, else ``_unscoped``.
|
|
141
|
+
|
|
142
|
+
Aligns by construction with Postgres ``semantic_facts.project`` —
|
|
143
|
+
``derive_slug_from_cwd`` routes through ``_safe_project_tag``, which
|
|
144
|
+
is the same validator used by ``decisions_dir_for_project``.
|
|
145
|
+
"""
|
|
146
|
+
# Step 1 — filesystem-driven derivation (the new primary path).
|
|
147
|
+
slug = derive_slug_from_cwd(cwd)
|
|
148
|
+
if slug != DEFAULT_PROJECT_TAG:
|
|
149
|
+
return slug
|
|
150
|
+
|
|
151
|
+
# Step 2 — fall back to projects.yaml only when filesystem returned
|
|
152
|
+
# _unscoped. This preserves backward-compat for explicit aliases.
|
|
153
|
+
data = load_projects_yaml()
|
|
154
|
+
cwd_norm = _normalize(cwd)
|
|
155
|
+
best_match: tuple[int, str] | None = None
|
|
156
|
+
for entry in data["projects"]:
|
|
157
|
+
path_norm = _normalize(entry["path"])
|
|
158
|
+
if path_norm == cwd_norm:
|
|
159
|
+
return entry["project"]
|
|
160
|
+
if cwd_norm.startswith(path_norm + os.sep):
|
|
161
|
+
length = len(path_norm)
|
|
162
|
+
if best_match is None or length > best_match[0]:
|
|
163
|
+
best_match = (length, entry["project"])
|
|
164
|
+
if best_match is not None:
|
|
165
|
+
return best_match[1]
|
|
166
|
+
return data["default"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__": # pragma: no cover - manual smoke tool
|
|
170
|
+
import sys
|
|
171
|
+
target = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
|
|
172
|
+
print(resolve_project(target))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Compatibility wrapper for pruning Codex plugin cache versions."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
from prune_plugin_cache import main as prune_main # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> int:
|
|
15
|
+
args = sys.argv[1:]
|
|
16
|
+
if "--host" not in args:
|
|
17
|
+
args = ["--host", "codex", *args]
|
|
18
|
+
sys.argv = [str(Path(__file__).with_name("prune_plugin_cache.py")), *args]
|
|
19
|
+
return prune_main()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
sys.exit(main())
|