@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,666 @@
|
|
|
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
|
+
"""One-way NavGator lessons -> local SQLite semantic-facts sync.
|
|
5
|
+
|
|
6
|
+
Reads project-local + global NavGator lessons.json files and upserts each
|
|
7
|
+
real (non-template) lesson into the local build-loop-memory SQLite index
|
|
8
|
+
with ``subject='lesson:nav:<id>'`` and ``domain='architecture'``.
|
|
9
|
+
|
|
10
|
+
Cross-project lesson recall happens through the existing recall pipeline;
|
|
11
|
+
this script only writes.
|
|
12
|
+
|
|
13
|
+
Inputs
|
|
14
|
+
------
|
|
15
|
+
* Project-local: ``<workdir>/.navgator/lessons/lessons.json``
|
|
16
|
+
* Global: ``~/.navgator/lessons/global-lessons.json``
|
|
17
|
+
|
|
18
|
+
Both files share a common shape (``{"lessons": [...]}``) with an optional
|
|
19
|
+
``_template`` sibling object that is always skipped. Inside ``lessons``,
|
|
20
|
+
any entry whose ``id`` starts with ``_template`` or is empty is also
|
|
21
|
+
skipped.
|
|
22
|
+
|
|
23
|
+
Project identity
|
|
24
|
+
----------------
|
|
25
|
+
Project tag is derived once per run from:
|
|
26
|
+
1. ``git remote get-url origin`` (basename minus ``.git``)
|
|
27
|
+
2. ``Path.cwd().name`` fallback
|
|
28
|
+
|
|
29
|
+
For project-local lessons the row's ``project`` column holds this tag.
|
|
30
|
+
For global lessons ``project`` is NULL — that is the cross-project
|
|
31
|
+
sentinel ``recall.py`` already understands.
|
|
32
|
+
|
|
33
|
+
SQLite upsert
|
|
34
|
+
-------------
|
|
35
|
+
The local index lives at ``<memory-root>/indexes/semantic_facts.sqlite`` by
|
|
36
|
+
default. It has a real ``PRIMARY KEY(subject, project_key)`` and uses an
|
|
37
|
+
idempotent upsert.
|
|
38
|
+
|
|
39
|
+
Postgres mirror
|
|
40
|
+
---------------
|
|
41
|
+
Postgres is optional. Pass ``--postgres-mirror`` to also write
|
|
42
|
+
``agent_memory.<schema>.semantic_facts``. Postgres-side, ``semantic_facts``
|
|
43
|
+
has no UNIQUE constraint on ``(subject, project)``. We follow the
|
|
44
|
+
``sync_db_from_files.py`` pattern: DELETE-then-INSERT in a single transaction.
|
|
45
|
+
|
|
46
|
+
Embeddings
|
|
47
|
+
----------
|
|
48
|
+
The SQLite default does not require embeddings. When ``--postgres-mirror`` is
|
|
49
|
+
set, ``scripts/embed_backend.embed`` is attempted for the Postgres vector
|
|
50
|
+
column. On embedding failure the mirror row is still written with
|
|
51
|
+
``embedding = NULL`` and the JSON output records the per-lesson error.
|
|
52
|
+
|
|
53
|
+
Failure modes
|
|
54
|
+
-------------
|
|
55
|
+
* SQLite write failure -> log to ``.build-loop/sync_errors.log`` and continue
|
|
56
|
+
with ``errors: ["sqlite_upsert_failed:<lesson_id>"]``.
|
|
57
|
+
* Postgres mirror unreachable -> log to ``.build-loop/sync_errors.log`` and
|
|
58
|
+
exit 0 with SQLite counts intact plus ``errors: ["postgres_unavailable"]``.
|
|
59
|
+
* Missing/empty/template-only ``lessons.json`` → ``synced: 0`` cleanly.
|
|
60
|
+
* Embedding subsystem unavailable → row written with NULL embedding;
|
|
61
|
+
``errors`` lists ``embed_unavailable:<lesson_id>``.
|
|
62
|
+
|
|
63
|
+
Stdout (always JSON):
|
|
64
|
+
{
|
|
65
|
+
"synced": <int>, # project-local SQLite rows written
|
|
66
|
+
"global_synced": <int>, # global SQLite rows written
|
|
67
|
+
"postgres_mirrored": <int>, # project-local Postgres mirror rows
|
|
68
|
+
"global_postgres_mirrored": <int>,
|
|
69
|
+
"skipped_templates": <int>,
|
|
70
|
+
"errors": [str],
|
|
71
|
+
"schema_version": "1.0.0"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
CLI
|
|
75
|
+
---
|
|
76
|
+
--project-only only sync project-local lessons
|
|
77
|
+
--global-only only sync ~/.navgator/lessons/global-lessons.json
|
|
78
|
+
--dry-run don't open a DB connection or write anything
|
|
79
|
+
--postgres-mirror also write Postgres semantic_facts
|
|
80
|
+
--sqlite-db PATH override local SQLite index path
|
|
81
|
+
--workdir PATH project root (default: cwd)
|
|
82
|
+
--lessons-file PATH override the lessons source — treat the given file as the
|
|
83
|
+
single project-local lessons.json and skip both the
|
|
84
|
+
NavGator project-local + global discovery. Used by
|
|
85
|
+
Chunk 8's promotion pipeline to feed
|
|
86
|
+
``.build-loop/architecture/lessons.json`` through the
|
|
87
|
+
same write path with a different subject prefix.
|
|
88
|
+
--source-prefix STR override the ``subject = '<prefix><lesson_id>'`` mapping.
|
|
89
|
+
Default ``lesson:nav:`` (NavGator origin). Chunk 8's
|
|
90
|
+
promotion script passes ``lesson:bl:`` so build-loop-
|
|
91
|
+
native lessons land in distinct semantic_facts rows.
|
|
92
|
+
|
|
93
|
+
Postgres DSN resolution (mirror only; matches build-loop convention):
|
|
94
|
+
1. $BUILD_LOOP_DATABASE_URL (preferred, plan-doc convention)
|
|
95
|
+
2. $DATABASE_URL (fallback, ``db.py`` default)
|
|
96
|
+
3. ~/.config/agent-memory/connection.env
|
|
97
|
+
"""
|
|
98
|
+
from __future__ import annotations
|
|
99
|
+
|
|
100
|
+
import argparse
|
|
101
|
+
import json
|
|
102
|
+
import os
|
|
103
|
+
import re
|
|
104
|
+
import subprocess
|
|
105
|
+
import sys
|
|
106
|
+
from datetime import datetime, timezone
|
|
107
|
+
from pathlib import Path
|
|
108
|
+
from typing import Any
|
|
109
|
+
|
|
110
|
+
SCHEMA_VERSION = "1.0.0"
|
|
111
|
+
DOMAIN = "architecture"
|
|
112
|
+
TOOL = "navgator"
|
|
113
|
+
SOURCE = "migration" # one-way external import per write_decision taxonomy
|
|
114
|
+
SYNC_ERRORS_LOG = ".build-loop/sync_errors.log"
|
|
115
|
+
|
|
116
|
+
# Make scripts/ importable as a sibling module (mirrors capture_arch_violation.py).
|
|
117
|
+
HERE = Path(__file__).resolve().parent
|
|
118
|
+
if str(HERE) not in sys.path:
|
|
119
|
+
sys.path.insert(0, str(HERE))
|
|
120
|
+
|
|
121
|
+
from semantic_index import upsert_lesson as _upsert_sqlite_lesson # type: ignore # noqa: E402
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------- helpers ----------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _now_iso() -> str:
|
|
128
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _log(msg: str) -> None:
|
|
132
|
+
print(f"[sync_navgator_lessons] {msg}", file=sys.stderr, flush=True)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _append_sync_error(workdir: Path, message: str) -> None:
|
|
136
|
+
"""Best-effort append to .build-loop/sync_errors.log; never raises."""
|
|
137
|
+
try:
|
|
138
|
+
log_path = workdir / SYNC_ERRORS_LOG
|
|
139
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
with log_path.open("a", encoding="utf-8") as fh:
|
|
141
|
+
fh.write(f"{_now_iso()}: {message}\n")
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
_log(f"WARN: could not write sync_errors.log ({exc})")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _detect_project_tag(workdir: Path) -> str:
|
|
147
|
+
"""Best-effort project identifier: git remote basename, then cwd name."""
|
|
148
|
+
try:
|
|
149
|
+
cp = subprocess.run(
|
|
150
|
+
["git", "remote", "get-url", "origin"],
|
|
151
|
+
cwd=str(workdir),
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
timeout=5,
|
|
155
|
+
)
|
|
156
|
+
if cp.returncode == 0:
|
|
157
|
+
url = cp.stdout.strip()
|
|
158
|
+
if url:
|
|
159
|
+
# Strip trailing .git, take final path segment.
|
|
160
|
+
stem = url.rstrip("/")
|
|
161
|
+
if stem.endswith(".git"):
|
|
162
|
+
stem = stem[: -len(".git")]
|
|
163
|
+
# Handle both git@host:owner/repo and https://host/owner/repo
|
|
164
|
+
for sep in (":", "/"):
|
|
165
|
+
if sep in stem:
|
|
166
|
+
stem = stem.rsplit(sep, 1)[-1]
|
|
167
|
+
if stem:
|
|
168
|
+
return stem
|
|
169
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
170
|
+
pass
|
|
171
|
+
name = workdir.name
|
|
172
|
+
return name or "unknown"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _read_lessons_file(path: Path) -> tuple[list[dict], int]:
|
|
176
|
+
"""Return (real_lessons, skipped_template_count). Missing/invalid → ([], 0)."""
|
|
177
|
+
if not path.exists():
|
|
178
|
+
return [], 0
|
|
179
|
+
try:
|
|
180
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
181
|
+
data = json.load(fh)
|
|
182
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
183
|
+
_log(f"WARN: lessons file unreadable ({path}): {exc}")
|
|
184
|
+
return [], 0
|
|
185
|
+
if not isinstance(data, dict):
|
|
186
|
+
return [], 0
|
|
187
|
+
raw_lessons = data.get("lessons", []) or []
|
|
188
|
+
if not isinstance(raw_lessons, list):
|
|
189
|
+
return [], 0
|
|
190
|
+
real: list[dict] = []
|
|
191
|
+
skipped = 0
|
|
192
|
+
for entry in raw_lessons:
|
|
193
|
+
if not isinstance(entry, dict):
|
|
194
|
+
continue
|
|
195
|
+
lid = str(entry.get("id", "") or "").strip()
|
|
196
|
+
if not lid or lid.startswith("_template"):
|
|
197
|
+
skipped += 1
|
|
198
|
+
continue
|
|
199
|
+
real.append(entry)
|
|
200
|
+
return real, skipped
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _confidence_source_for(promoted: bool) -> str:
|
|
204
|
+
"""Map NavGator's ``promoted`` flag to write_decision's closed taxonomy.
|
|
205
|
+
|
|
206
|
+
Aligns with Chunk 6 helper inside ``capture_arch_violation.py``: every
|
|
207
|
+
automated capture uses the ``auto-*`` prefix. Promoted lessons have
|
|
208
|
+
been hand-validated → ``auto-confirmed``; un-promoted are heuristic →
|
|
209
|
+
``auto-inferred``.
|
|
210
|
+
"""
|
|
211
|
+
return "auto-confirmed" if promoted else "auto-inferred"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _confidence_float_for(promoted: bool) -> float:
|
|
215
|
+
"""Mirror ``write_decision._confidence_to_float`` so DB readers see the
|
|
216
|
+
same numeric scale they get for decisions."""
|
|
217
|
+
return 0.75 if promoted else 0.5
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _is_safe_schema(schema: str) -> bool:
|
|
221
|
+
return bool(re.match(r"^[a-z][a-z0-9_]*$", schema))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------- DB layer ----------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _resolve_dsn() -> str | None:
|
|
228
|
+
"""Plan-doc DSN order: BUILD_LOOP_DATABASE_URL → DATABASE_URL → connection.env.
|
|
229
|
+
|
|
230
|
+
Delegates to the shared resolver (`scripts/_db_url.py`). Returns None
|
|
231
|
+
when nothing is configured (the shared resolver returns ""); caller
|
|
232
|
+
treats None as 'postgres unavailable' only when --postgres-mirror is set.
|
|
233
|
+
"""
|
|
234
|
+
from _db_url import resolve_db_url # noqa: PLC0415
|
|
235
|
+
|
|
236
|
+
return resolve_db_url() or None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _open_connection():
|
|
240
|
+
"""Open a psycopg connection. Raises on any failure.
|
|
241
|
+
|
|
242
|
+
DSN comes from `_resolve_dsn`, which delegates to the shared
|
|
243
|
+
`scripts/_db_url.py` resolver (BUILD_LOOP_DATABASE_URL → DATABASE_URL →
|
|
244
|
+
connection.env). We open psycopg directly here rather than via
|
|
245
|
+
``scripts.db`` to keep this script's soft-failure (return-None →
|
|
246
|
+
exit-0) envelope instead of db.py's raise-on-missing contract.
|
|
247
|
+
"""
|
|
248
|
+
import psycopg # type: ignore # noqa: PLC0415
|
|
249
|
+
|
|
250
|
+
dsn = _resolve_dsn()
|
|
251
|
+
if not dsn:
|
|
252
|
+
raise RuntimeError("DATABASE_URL/BUILD_LOOP_DATABASE_URL not configured")
|
|
253
|
+
return psycopg.connect(dsn, autocommit=False)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _vector_literal(embedding: list[float] | None) -> str | None:
|
|
257
|
+
if not embedding:
|
|
258
|
+
return None
|
|
259
|
+
return "[" + ",".join(f"{x:.6f}" for x in embedding) + "]"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _upsert_lesson(
|
|
263
|
+
conn,
|
|
264
|
+
*,
|
|
265
|
+
schema: str,
|
|
266
|
+
lesson: dict,
|
|
267
|
+
project: str | None,
|
|
268
|
+
embedding: list[float] | None,
|
|
269
|
+
subject_prefix: str = "lesson:nav:",
|
|
270
|
+
) -> None:
|
|
271
|
+
"""DELETE-then-INSERT a single lesson row in one transaction.
|
|
272
|
+
|
|
273
|
+
Mirrors ``sync_db_from_files.upsert_decision`` — the schema has no
|
|
274
|
+
UNIQUE(subject, project) so we cannot use a real ON CONFLICT. The
|
|
275
|
+
DELETE+INSERT pair runs inside the caller's transaction; commit is
|
|
276
|
+
issued by the caller after all lessons are processed (or on error).
|
|
277
|
+
"""
|
|
278
|
+
if not _is_safe_schema(schema):
|
|
279
|
+
raise ValueError(f"unsafe schema name: {schema!r}")
|
|
280
|
+
|
|
281
|
+
lesson_id = str(lesson.get("id", ""))
|
|
282
|
+
subject = f"{subject_prefix}{lesson_id}"
|
|
283
|
+
predicate = str(lesson.get("category", "") or "uncategorized")
|
|
284
|
+
obj_summary = str(lesson.get("pattern", "") or "")
|
|
285
|
+
promoted = bool(lesson.get("promoted", False))
|
|
286
|
+
confidence_source = _confidence_source_for(promoted)
|
|
287
|
+
confidence = _confidence_float_for(promoted)
|
|
288
|
+
|
|
289
|
+
metadata = {
|
|
290
|
+
"lesson_id": lesson_id,
|
|
291
|
+
"promoted": promoted,
|
|
292
|
+
"navgator_lesson": lesson, # full original payload
|
|
293
|
+
"synced_at": _now_iso(),
|
|
294
|
+
"schema_version": SCHEMA_VERSION,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Files affected → array column when present, else empty list.
|
|
298
|
+
ctx = lesson.get("context") or {}
|
|
299
|
+
files_affected = []
|
|
300
|
+
if isinstance(ctx, dict):
|
|
301
|
+
fa = ctx.get("files_affected") or []
|
|
302
|
+
if isinstance(fa, list):
|
|
303
|
+
files_affected = [str(x) for x in fa]
|
|
304
|
+
|
|
305
|
+
# last_synced lives in metadata, not as its own column (semantic_facts
|
|
306
|
+
# already has last_validated/last_accessed; we don't want to clobber
|
|
307
|
+
# those for facts that pre-existed in the table).
|
|
308
|
+
metadata["last_synced"] = _now_iso()
|
|
309
|
+
|
|
310
|
+
vec_lit = _vector_literal(embedding)
|
|
311
|
+
|
|
312
|
+
# DELETE + INSERT inside one cursor for transactional symmetry.
|
|
313
|
+
with conn.cursor() as cur:
|
|
314
|
+
if project is None:
|
|
315
|
+
cur.execute(
|
|
316
|
+
f"DELETE FROM {schema}.semantic_facts "
|
|
317
|
+
"WHERE subject = %s AND project IS NULL;",
|
|
318
|
+
(subject,),
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
cur.execute(
|
|
322
|
+
f"DELETE FROM {schema}.semantic_facts "
|
|
323
|
+
"WHERE subject = %s AND project = %s;",
|
|
324
|
+
(subject, project),
|
|
325
|
+
)
|
|
326
|
+
if vec_lit is None:
|
|
327
|
+
sql = (
|
|
328
|
+
f"INSERT INTO {schema}.semantic_facts "
|
|
329
|
+
"(subject, predicate, object, confidence, status, embedding, metadata, "
|
|
330
|
+
" project, tool, task_category, files_touched, "
|
|
331
|
+
" confidence_source, domain) "
|
|
332
|
+
"VALUES (%s, %s, %s, %s, 'active', NULL, %s::jsonb, "
|
|
333
|
+
" %s, %s, %s, %s, "
|
|
334
|
+
" %s, %s);"
|
|
335
|
+
)
|
|
336
|
+
params = (
|
|
337
|
+
subject,
|
|
338
|
+
predicate,
|
|
339
|
+
obj_summary,
|
|
340
|
+
confidence,
|
|
341
|
+
json.dumps(metadata, ensure_ascii=False),
|
|
342
|
+
project,
|
|
343
|
+
TOOL,
|
|
344
|
+
"research",
|
|
345
|
+
files_affected,
|
|
346
|
+
confidence_source,
|
|
347
|
+
DOMAIN,
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
sql = (
|
|
351
|
+
f"INSERT INTO {schema}.semantic_facts "
|
|
352
|
+
"(subject, predicate, object, confidence, status, embedding, metadata, "
|
|
353
|
+
" project, tool, task_category, files_touched, "
|
|
354
|
+
" confidence_source, domain) "
|
|
355
|
+
"VALUES (%s, %s, %s, %s, 'active', %s::vector, %s::jsonb, "
|
|
356
|
+
" %s, %s, %s, %s, "
|
|
357
|
+
" %s, %s);"
|
|
358
|
+
)
|
|
359
|
+
params = (
|
|
360
|
+
subject,
|
|
361
|
+
predicate,
|
|
362
|
+
obj_summary,
|
|
363
|
+
confidence,
|
|
364
|
+
vec_lit,
|
|
365
|
+
json.dumps(metadata, ensure_ascii=False),
|
|
366
|
+
project,
|
|
367
|
+
TOOL,
|
|
368
|
+
"research",
|
|
369
|
+
files_affected,
|
|
370
|
+
confidence_source,
|
|
371
|
+
DOMAIN,
|
|
372
|
+
)
|
|
373
|
+
cur.execute(sql, params)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------- main pipeline ----------
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
380
|
+
p = argparse.ArgumentParser(
|
|
381
|
+
description="One-way NavGator lessons -> local SQLite semantic-facts sync"
|
|
382
|
+
)
|
|
383
|
+
p.add_argument(
|
|
384
|
+
"--workdir",
|
|
385
|
+
default=".",
|
|
386
|
+
help="Project root (default: cwd). Project-local lessons live under <workdir>/.navgator/lessons/lessons.json.",
|
|
387
|
+
)
|
|
388
|
+
p.add_argument(
|
|
389
|
+
"--project-only",
|
|
390
|
+
action="store_true",
|
|
391
|
+
help="Sync only project-local lessons.json.",
|
|
392
|
+
)
|
|
393
|
+
p.add_argument(
|
|
394
|
+
"--global-only",
|
|
395
|
+
action="store_true",
|
|
396
|
+
help="Sync only ~/.navgator/lessons/global-lessons.json.",
|
|
397
|
+
)
|
|
398
|
+
p.add_argument(
|
|
399
|
+
"--dry-run",
|
|
400
|
+
action="store_true",
|
|
401
|
+
help="Do not open SQLite or Postgres. Reports what would be synced.",
|
|
402
|
+
)
|
|
403
|
+
p.add_argument(
|
|
404
|
+
"--sqlite-db",
|
|
405
|
+
default=None,
|
|
406
|
+
help="Override local SQLite semantic index path. Default: <memory-root>/indexes/semantic_facts.sqlite.",
|
|
407
|
+
)
|
|
408
|
+
p.add_argument(
|
|
409
|
+
"--postgres-mirror",
|
|
410
|
+
action="store_true",
|
|
411
|
+
help="Also mirror rows into Postgres semantic_facts. Default: SQLite only.",
|
|
412
|
+
)
|
|
413
|
+
p.add_argument(
|
|
414
|
+
"--schema",
|
|
415
|
+
default=None,
|
|
416
|
+
help="Postgres schema. Default: $AGENT_MEMORY_SCHEMA or 'personal_memory'.",
|
|
417
|
+
)
|
|
418
|
+
p.add_argument(
|
|
419
|
+
"--lessons-file",
|
|
420
|
+
default=None,
|
|
421
|
+
help=(
|
|
422
|
+
"Override lessons source. When set, the given file is treated as the "
|
|
423
|
+
"single project-local lessons.json and the NavGator project-local + "
|
|
424
|
+
"global discovery is skipped. Used by Chunk 8's build-loop-native "
|
|
425
|
+
"promotion pipeline."
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
p.add_argument(
|
|
429
|
+
"--source-prefix",
|
|
430
|
+
default="lesson:nav:",
|
|
431
|
+
help=(
|
|
432
|
+
"Subject prefix written into semantic_facts.subject "
|
|
433
|
+
"(default 'lesson:nav:'). Chunk 8's promotion script passes "
|
|
434
|
+
"'lesson:bl:' to keep build-loop-native lessons in distinct rows."
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
return p.parse_args(argv)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _default_schema() -> str:
|
|
441
|
+
return os.environ.get("AGENT_MEMORY_SCHEMA") or "personal_memory"
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _embed_safely(text: str, errors: list[str], lesson_id: str) -> list[float] | None:
|
|
445
|
+
"""Call embed_backend; on any failure log to ``errors`` and return None."""
|
|
446
|
+
if not text or not text.strip():
|
|
447
|
+
return None
|
|
448
|
+
try:
|
|
449
|
+
from embed_backend import embed as _embed # type: ignore # noqa: PLC0415
|
|
450
|
+
except Exception as exc: # noqa: BLE001
|
|
451
|
+
errors.append(f"embed_unavailable:{lesson_id}")
|
|
452
|
+
_log(f"WARN: embed_backend import failed for {lesson_id}: {exc}")
|
|
453
|
+
return None
|
|
454
|
+
try:
|
|
455
|
+
return _embed(text)
|
|
456
|
+
except Exception as exc: # noqa: BLE001
|
|
457
|
+
errors.append(f"embed_failed:{lesson_id}")
|
|
458
|
+
_log(f"WARN: embedding {lesson_id} failed: {exc}")
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def main(argv: list[str] | None = None) -> int:
|
|
463
|
+
args = parse_args(argv)
|
|
464
|
+
workdir = Path(args.workdir).resolve()
|
|
465
|
+
schema = args.schema or _default_schema()
|
|
466
|
+
|
|
467
|
+
if args.project_only and args.global_only:
|
|
468
|
+
print(
|
|
469
|
+
json.dumps(
|
|
470
|
+
{
|
|
471
|
+
"synced": 0,
|
|
472
|
+
"global_synced": 0,
|
|
473
|
+
"postgres_mirrored": 0,
|
|
474
|
+
"global_postgres_mirrored": 0,
|
|
475
|
+
"skipped_templates": 0,
|
|
476
|
+
"errors": ["invalid_flags:project_only_and_global_only_mutually_exclusive"],
|
|
477
|
+
"schema_version": SCHEMA_VERSION,
|
|
478
|
+
},
|
|
479
|
+
sort_keys=True,
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
project_tag = _detect_project_tag(workdir)
|
|
485
|
+
subject_prefix = args.source_prefix or "lesson:nav:"
|
|
486
|
+
|
|
487
|
+
project_lessons: list[dict] = []
|
|
488
|
+
global_lessons: list[dict] = []
|
|
489
|
+
skipped_templates = 0
|
|
490
|
+
|
|
491
|
+
if args.lessons_file:
|
|
492
|
+
# Override mode: treat --lessons-file as the sole project-local
|
|
493
|
+
# source. Skip NavGator's project-local + global discovery so
|
|
494
|
+
# promotion-fed lessons don't double-sync.
|
|
495
|
+
path = Path(args.lessons_file)
|
|
496
|
+
if not path.is_absolute():
|
|
497
|
+
path = (workdir / path).resolve()
|
|
498
|
+
real, skipped = _read_lessons_file(path)
|
|
499
|
+
project_lessons = real
|
|
500
|
+
skipped_templates += skipped
|
|
501
|
+
else:
|
|
502
|
+
if not args.global_only:
|
|
503
|
+
path = workdir / ".navgator" / "lessons" / "lessons.json"
|
|
504
|
+
real, skipped = _read_lessons_file(path)
|
|
505
|
+
project_lessons = real
|
|
506
|
+
skipped_templates += skipped
|
|
507
|
+
|
|
508
|
+
if not args.project_only:
|
|
509
|
+
path = Path.home() / ".navgator" / "lessons" / "global-lessons.json"
|
|
510
|
+
real, skipped = _read_lessons_file(path)
|
|
511
|
+
global_lessons = real
|
|
512
|
+
skipped_templates += skipped
|
|
513
|
+
|
|
514
|
+
errors: list[str] = []
|
|
515
|
+
synced = 0
|
|
516
|
+
global_synced = 0
|
|
517
|
+
postgres_mirrored = 0
|
|
518
|
+
global_postgres_mirrored = 0
|
|
519
|
+
|
|
520
|
+
if args.dry_run:
|
|
521
|
+
out = {
|
|
522
|
+
"synced": len(project_lessons),
|
|
523
|
+
"global_synced": len(global_lessons),
|
|
524
|
+
"postgres_mirrored": 0,
|
|
525
|
+
"global_postgres_mirrored": 0,
|
|
526
|
+
"skipped_templates": skipped_templates,
|
|
527
|
+
"errors": errors,
|
|
528
|
+
"schema_version": SCHEMA_VERSION,
|
|
529
|
+
"dry_run": True,
|
|
530
|
+
"postgres_mirror": bool(args.postgres_mirror),
|
|
531
|
+
}
|
|
532
|
+
print(json.dumps(out, sort_keys=True))
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
if not project_lessons and not global_lessons:
|
|
536
|
+
out = {
|
|
537
|
+
"synced": 0,
|
|
538
|
+
"global_synced": 0,
|
|
539
|
+
"postgres_mirrored": 0,
|
|
540
|
+
"global_postgres_mirrored": 0,
|
|
541
|
+
"skipped_templates": skipped_templates,
|
|
542
|
+
"errors": errors,
|
|
543
|
+
"schema_version": SCHEMA_VERSION,
|
|
544
|
+
"postgres_mirror": bool(args.postgres_mirror),
|
|
545
|
+
}
|
|
546
|
+
print(json.dumps(out, sort_keys=True))
|
|
547
|
+
return 0
|
|
548
|
+
|
|
549
|
+
sqlite_db = Path(args.sqlite_db).expanduser().resolve() if args.sqlite_db else None
|
|
550
|
+
|
|
551
|
+
for lesson in project_lessons:
|
|
552
|
+
lid = str(lesson.get("id", ""))
|
|
553
|
+
promoted = bool(lesson.get("promoted", False))
|
|
554
|
+
try:
|
|
555
|
+
_upsert_sqlite_lesson(
|
|
556
|
+
lesson=lesson,
|
|
557
|
+
project=project_tag,
|
|
558
|
+
subject_prefix=subject_prefix,
|
|
559
|
+
confidence=_confidence_float_for(promoted),
|
|
560
|
+
confidence_source=_confidence_source_for(promoted),
|
|
561
|
+
db_path=sqlite_db,
|
|
562
|
+
tool=TOOL,
|
|
563
|
+
domain=DOMAIN,
|
|
564
|
+
)
|
|
565
|
+
synced += 1
|
|
566
|
+
except Exception as exc: # noqa: BLE001
|
|
567
|
+
msg = f"sqlite_upsert_failed:{lid}: {type(exc).__name__}: {exc}"
|
|
568
|
+
errors.append(f"sqlite_upsert_failed:{lid}")
|
|
569
|
+
_append_sync_error(workdir, msg)
|
|
570
|
+
|
|
571
|
+
for lesson in global_lessons:
|
|
572
|
+
lid = str(lesson.get("id", ""))
|
|
573
|
+
promoted = bool(lesson.get("promoted", False))
|
|
574
|
+
try:
|
|
575
|
+
_upsert_sqlite_lesson(
|
|
576
|
+
lesson=lesson,
|
|
577
|
+
project=None,
|
|
578
|
+
subject_prefix=subject_prefix,
|
|
579
|
+
confidence=_confidence_float_for(promoted),
|
|
580
|
+
confidence_source=_confidence_source_for(promoted),
|
|
581
|
+
db_path=sqlite_db,
|
|
582
|
+
tool=TOOL,
|
|
583
|
+
domain=DOMAIN,
|
|
584
|
+
)
|
|
585
|
+
global_synced += 1
|
|
586
|
+
except Exception as exc: # noqa: BLE001
|
|
587
|
+
msg = f"sqlite_upsert_failed_global:{lid}: {type(exc).__name__}: {exc}"
|
|
588
|
+
errors.append(f"sqlite_upsert_failed:{lid}")
|
|
589
|
+
_append_sync_error(workdir, msg)
|
|
590
|
+
|
|
591
|
+
if args.postgres_mirror:
|
|
592
|
+
# Open Postgres. Soft-fail to graceful exit on any connection error.
|
|
593
|
+
try:
|
|
594
|
+
conn = _open_connection()
|
|
595
|
+
except Exception as exc: # noqa: BLE001 - psycopg.OperationalError, RuntimeError, etc.
|
|
596
|
+
msg = f"postgres_unavailable: {type(exc).__name__}: {exc}"
|
|
597
|
+
_append_sync_error(workdir, msg)
|
|
598
|
+
errors.append("postgres_unavailable")
|
|
599
|
+
conn = None
|
|
600
|
+
|
|
601
|
+
if conn is not None:
|
|
602
|
+
try:
|
|
603
|
+
for lesson in project_lessons:
|
|
604
|
+
lid = str(lesson.get("id", ""))
|
|
605
|
+
embedding = _embed_safely(
|
|
606
|
+
str(lesson.get("pattern", "") or ""), errors, lid
|
|
607
|
+
)
|
|
608
|
+
try:
|
|
609
|
+
_upsert_lesson(
|
|
610
|
+
conn,
|
|
611
|
+
schema=schema,
|
|
612
|
+
lesson=lesson,
|
|
613
|
+
project=project_tag,
|
|
614
|
+
embedding=embedding,
|
|
615
|
+
subject_prefix=subject_prefix,
|
|
616
|
+
)
|
|
617
|
+
postgres_mirrored += 1
|
|
618
|
+
except Exception as exc: # noqa: BLE001 - psycopg + unforeseen
|
|
619
|
+
conn.rollback()
|
|
620
|
+
msg = f"upsert_failed:{lid}: {type(exc).__name__}: {exc}"
|
|
621
|
+
errors.append(f"upsert_failed:{lid}")
|
|
622
|
+
_append_sync_error(workdir, msg)
|
|
623
|
+
for lesson in global_lessons:
|
|
624
|
+
lid = str(lesson.get("id", ""))
|
|
625
|
+
embedding = _embed_safely(
|
|
626
|
+
str(lesson.get("pattern", "") or ""), errors, lid
|
|
627
|
+
)
|
|
628
|
+
try:
|
|
629
|
+
_upsert_lesson(
|
|
630
|
+
conn,
|
|
631
|
+
schema=schema,
|
|
632
|
+
lesson=lesson,
|
|
633
|
+
project=None,
|
|
634
|
+
embedding=embedding,
|
|
635
|
+
subject_prefix=subject_prefix,
|
|
636
|
+
)
|
|
637
|
+
global_postgres_mirrored += 1
|
|
638
|
+
except Exception as exc: # noqa: BLE001
|
|
639
|
+
conn.rollback()
|
|
640
|
+
msg = f"upsert_failed_global:{lid}: {type(exc).__name__}: {exc}"
|
|
641
|
+
errors.append(f"upsert_failed:{lid}")
|
|
642
|
+
_append_sync_error(workdir, msg)
|
|
643
|
+
conn.commit()
|
|
644
|
+
finally:
|
|
645
|
+
try:
|
|
646
|
+
conn.close()
|
|
647
|
+
except Exception: # noqa: BLE001
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
out = {
|
|
651
|
+
"synced": synced,
|
|
652
|
+
"global_synced": global_synced,
|
|
653
|
+
"postgres_mirrored": postgres_mirrored,
|
|
654
|
+
"global_postgres_mirrored": global_postgres_mirrored,
|
|
655
|
+
"skipped_templates": skipped_templates,
|
|
656
|
+
"errors": errors,
|
|
657
|
+
"schema_version": SCHEMA_VERSION,
|
|
658
|
+
"postgres_mirror": bool(args.postgres_mirror),
|
|
659
|
+
"sqlite_db": str(sqlite_db) if sqlite_db else None,
|
|
660
|
+
}
|
|
661
|
+
print(json.dumps(out, sort_keys=True))
|
|
662
|
+
return 0
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
if __name__ == "__main__":
|
|
666
|
+
raise SystemExit(main())
|