arkaos 4.22.0 → 4.23.0
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/THE-ARKAOS-GUIDE.md +72 -0
- package/VERSION +1 -1
- package/arka/SKILL.md +28 -6
- package/arka/skills/bootstrap-agent/SKILL.md +3 -2
- package/arka/skills/flow/SKILL.md +3 -2
- package/arka/skills/forge/SKILL.md +3 -2
- package/arka/skills/recipes/SKILL.md +3 -2
- package/arka/skills/refine/SKILL.md +3 -2
- package/arka/skills/research/SKILL.md +3 -2
- package/bin/arka-doctor +8 -0
- package/core/cognition/insights/store.py +15 -4
- package/core/forge/persistence.py +27 -9
- package/core/forge/schema.py +45 -22
- package/core/governance/evidence_checks.py +101 -9
- package/core/governance/kb_cite_check.py +16 -6
- package/core/governance/stop_lint.py +38 -11
- package/core/governance/stop_lint_telemetry.py +11 -2
- package/core/hooks/user_prompt_submit.py +21 -23
- package/core/synapse/engine.py +46 -16
- package/core/synapse/graph_context_layer.py +7 -2
- package/core/synapse/kb_cache.py +63 -16
- package/core/synapse/layers.py +93 -30
- package/core/synapse/layers_base.py +16 -0
- package/core/synapse/layers_kb.py +325 -19
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/plan-canvas.vue +630 -0
- package/departments/brand/SKILL.md +3 -2
- package/departments/brand/skills/archetype-finder/SKILL.md +3 -2
- package/departments/brand/skills/colors/SKILL.md +3 -2
- package/departments/brand/skills/design-system/SKILL.md +3 -2
- package/departments/brand/skills/identity-system/SKILL.md +3 -2
- package/departments/brand/skills/logo-brief/SKILL.md +3 -2
- package/departments/brand/skills/mockup-generate/SKILL.md +3 -2
- package/departments/brand/skills/naming-evaluate/SKILL.md +3 -2
- package/departments/brand/skills/positioning-statement/SKILL.md +3 -2
- package/departments/brand/skills/primal-audit/SKILL.md +3 -2
- package/departments/brand/skills/ux-audit/SKILL.md +3 -2
- package/departments/brand/skills/voice-guide/SKILL.md +3 -2
- package/departments/brand/skills/wireframe/SKILL.md +3 -2
- package/departments/community/SKILL.md +3 -2
- package/departments/community/skills/ai-community/SKILL.md +3 -2
- package/departments/community/skills/betting-setup/SKILL.md +3 -2
- package/departments/community/skills/business-model/SKILL.md +3 -2
- package/departments/community/skills/content-calendar/SKILL.md +3 -2
- package/departments/community/skills/events-plan/SKILL.md +3 -2
- package/departments/community/skills/gamification-design/SKILL.md +3 -2
- package/departments/community/skills/growth-plan/SKILL.md +3 -2
- package/departments/community/skills/metrics-track/SKILL.md +3 -2
- package/departments/community/skills/moderation/SKILL.md +3 -2
- package/departments/community/skills/monetize-plan/SKILL.md +3 -2
- package/departments/community/skills/niche-setup/SKILL.md +3 -2
- package/departments/community/skills/onboarding-flow/SKILL.md +3 -2
- package/departments/community/skills/platform-select/SKILL.md +3 -2
- package/departments/content/SKILL.md +3 -2
- package/departments/content/skills/ai-workflow/SKILL.md +3 -2
- package/departments/content/skills/analytics/SKILL.md +3 -2
- package/departments/content/skills/calendar/SKILL.md +3 -2
- package/departments/content/skills/content-system/SKILL.md +3 -2
- package/departments/content/skills/monetization-plan/SKILL.md +3 -2
- package/departments/content/skills/newsletter-write/SKILL.md +3 -2
- package/departments/content/skills/platform-optimize/SKILL.md +3 -2
- package/departments/content/skills/repurpose-plan/SKILL.md +3 -2
- package/departments/content/skills/research-compile/SKILL.md +3 -2
- package/departments/content/skills/script-structure/SKILL.md +3 -2
- package/departments/content/skills/short-form/SKILL.md +3 -2
- package/departments/content/skills/thumbnail-package/SKILL.md +3 -2
- package/departments/content/skills/trend-hunt/SKILL.md +3 -2
- package/departments/content/skills/video-produce/SKILL.md +3 -2
- package/departments/content/skills/video-setup/SKILL.md +3 -2
- package/departments/content/skills/viral-design/SKILL.md +3 -2
- package/departments/content/skills/youtube-strategy/SKILL.md +3 -2
- package/departments/dev/SKILL.md +3 -2
- package/departments/dev/skills/ai-assisted-dev/SKILL.md +3 -2
- package/departments/dev/skills/animated-website/SKILL.md +7 -0
- package/departments/dev/skills/architecture-design/SKILL.md +3 -2
- package/departments/dev/skills/code-review/SKILL.md +3 -2
- package/departments/dev/skills/db-design/SKILL.md +3 -2
- package/departments/dev/skills/ddd-model/SKILL.md +3 -2
- package/departments/dev/skills/demo-gif/SKILL.md +3 -2
- package/departments/dev/skills/deploy/SKILL.md +3 -2
- package/departments/dev/skills/devops-pipeline/SKILL.md +3 -2
- package/departments/dev/skills/docs/SKILL.md +3 -2
- package/departments/dev/skills/mcp/SKILL.md +3 -2
- package/departments/dev/skills/performance-audit/SKILL.md +3 -2
- package/departments/dev/skills/refactor-plan/SKILL.md +3 -2
- package/departments/dev/skills/research/SKILL.md +3 -2
- package/departments/dev/skills/security-compliance/SKILL.md +3 -2
- package/departments/dev/skills/stack-check/SKILL.md +3 -2
- package/departments/ecom/SKILL.md +3 -2
- package/departments/ecom/skills/analytics/SKILL.md +3 -2
- package/departments/ecom/skills/browse-competitor/SKILL.md +3 -2
- package/departments/ecom/skills/cart-recovery/SKILL.md +3 -2
- package/departments/ecom/skills/cro-optimize/SKILL.md +3 -2
- package/departments/ecom/skills/customer-journey/SKILL.md +3 -2
- package/departments/ecom/skills/fulfillment-plan/SKILL.md +3 -2
- package/departments/ecom/skills/marketplace-manage/SKILL.md +3 -2
- package/departments/ecom/skills/pricing-strategy/SKILL.md +3 -2
- package/departments/ecom/skills/product-launch/SKILL.md +3 -2
- package/departments/ecom/skills/rfm-segment/SKILL.md +3 -2
- package/departments/ecom/skills/social-commerce/SKILL.md +3 -2
- package/departments/ecom/skills/store-audit/SKILL.md +3 -2
- package/departments/ecom/skills/subscription-model/SKILL.md +3 -2
- package/departments/finance/SKILL.md +3 -2
- package/departments/finance/skills/budget-plan/SKILL.md +3 -2
- package/departments/finance/skills/cashflow-forecast/SKILL.md +3 -2
- package/departments/finance/skills/ciso-advisor/SKILL.md +3 -2
- package/departments/finance/skills/financial-model/SKILL.md +3 -2
- package/departments/finance/skills/pitch-deck/SKILL.md +3 -2
- package/departments/finance/skills/scenario-analysis/SKILL.md +3 -2
- package/departments/finance/skills/unit-economics/SKILL.md +3 -2
- package/departments/finance/skills/valuation-model/SKILL.md +3 -2
- package/departments/kb/SKILL.md +3 -2
- package/departments/kb/skills/ai-research/SKILL.md +3 -2
- package/departments/kb/skills/competitive-intel/SKILL.md +3 -2
- package/departments/kb/skills/doc-extraction/SKILL.md +3 -2
- package/departments/kb/skills/doc-redaction/SKILL.md +3 -2
- package/departments/kb/skills/knowledge-ops/SKILL.md +3 -2
- package/departments/kb/skills/knowledge-review/SKILL.md +3 -2
- package/departments/kb/skills/learn-content/SKILL.md +3 -2
- package/departments/kb/skills/moc-create/SKILL.md +3 -2
- package/departments/kb/skills/persona-build/SKILL.md +3 -2
- package/departments/kb/skills/research-plan/SKILL.md +3 -2
- package/departments/kb/skills/search-kb/SKILL.md +3 -2
- package/departments/kb/skills/source-evaluate/SKILL.md +3 -2
- package/departments/kb/skills/taxonomy-manage/SKILL.md +3 -2
- package/departments/kb/skills/write-as-persona/SKILL.md +3 -2
- package/departments/landing/SKILL.md +3 -2
- package/departments/landing/skills/ab-test/SKILL.md +3 -2
- package/departments/landing/skills/affiliate-bridge/SKILL.md +3 -2
- package/departments/landing/skills/awareness-diagnose/SKILL.md +3 -2
- package/departments/landing/skills/email-sequence/SKILL.md +3 -2
- package/departments/landing/skills/funnel-metrics/SKILL.md +3 -2
- package/departments/landing/skills/headline-write/SKILL.md +3 -2
- package/departments/landing/skills/launch-sequence/SKILL.md +3 -2
- package/departments/landing/skills/offer-create/SKILL.md +3 -2
- package/departments/landing/skills/optimize-page/SKILL.md +3 -2
- package/departments/landing/skills/page-architect/SKILL.md +3 -2
- package/departments/landing/skills/persuasion-apply/SKILL.md +3 -2
- package/departments/landing/skills/webinar-funnel/SKILL.md +3 -2
- package/departments/leadership/SKILL.md +3 -2
- package/departments/leadership/skills/change-manage/SKILL.md +3 -2
- package/departments/leadership/skills/conflict-resolve/SKILL.md +3 -2
- package/departments/leadership/skills/culture-audit/SKILL.md +3 -2
- package/departments/leadership/skills/delegation-matrix/SKILL.md +3 -2
- package/departments/leadership/skills/disc-assess/SKILL.md +3 -2
- package/departments/leadership/skills/feedback-give/SKILL.md +3 -2
- package/departments/leadership/skills/hiring-plan/SKILL.md +3 -2
- package/departments/leadership/skills/performance-review/SKILL.md +3 -2
- package/departments/marketing/SKILL.md +3 -2
- package/departments/marketing/skills/ab-test/SKILL.md +3 -2
- package/departments/marketing/skills/analytics-report/SKILL.md +3 -2
- package/departments/marketing/skills/audience-segment/SKILL.md +3 -2
- package/departments/marketing/skills/calendar-plan/SKILL.md +3 -2
- package/departments/marketing/skills/competitor-analysis/SKILL.md +3 -2
- package/departments/marketing/skills/content-audit/SKILL.md +3 -2
- package/departments/marketing/skills/email-sequence/SKILL.md +3 -2
- package/departments/marketing/skills/growth-loop/SKILL.md +3 -2
- package/departments/marketing/skills/marketing-automation/SKILL.md +3 -2
- package/departments/marketing/skills/paid-campaign/SKILL.md +3 -2
- package/departments/marketing/skills/programmatic-seo/SKILL.md +3 -2
- package/departments/marketing/skills/seo-audit/SKILL.md +3 -2
- package/departments/marketing/skills/social-strategy/SKILL.md +3 -2
- package/departments/ops/SKILL.md +3 -2
- package/departments/ops/skills/bottleneck-find/SKILL.md +3 -2
- package/departments/ops/skills/dashboard-build/SKILL.md +3 -2
- package/departments/ops/skills/gdpr-compliance/SKILL.md +3 -2
- package/departments/ops/skills/github-ops/SKILL.md +3 -2
- package/departments/ops/skills/gtd-setup/SKILL.md +3 -2
- package/departments/ops/skills/integration-design/SKILL.md +3 -2
- package/departments/ops/skills/iso27001/SKILL.md +3 -2
- package/departments/ops/skills/lean-audit/SKILL.md +3 -2
- package/departments/ops/skills/metrics-dashboard/SKILL.md +3 -2
- package/departments/ops/skills/n8n-flow/SKILL.md +3 -2
- package/departments/ops/skills/quality-management/SKILL.md +3 -2
- package/departments/ops/skills/risk-management/SKILL.md +3 -2
- package/departments/ops/skills/soc2-compliance/SKILL.md +3 -2
- package/departments/ops/skills/sop-create/SKILL.md +3 -2
- package/departments/ops/skills/terminal-ops/SKILL.md +3 -2
- package/departments/ops/skills/workflow-automate/SKILL.md +3 -2
- package/departments/ops/skills/zapier-flow/SKILL.md +3 -2
- package/departments/org/SKILL.md +3 -2
- package/departments/org/skills/compensation-plan/SKILL.md +3 -2
- package/departments/org/skills/culture-define/SKILL.md +3 -2
- package/departments/org/skills/decision-framework/SKILL.md +3 -2
- package/departments/org/skills/hiring-plan/SKILL.md +3 -2
- package/departments/org/skills/meeting-optimize/SKILL.md +3 -2
- package/departments/org/skills/onboarding-design/SKILL.md +3 -2
- package/departments/org/skills/org-design/SKILL.md +3 -2
- package/departments/org/skills/remote-setup/SKILL.md +3 -2
- package/departments/org/skills/sop-process/SKILL.md +3 -2
- package/departments/org/skills/team-assess/SKILL.md +3 -2
- package/departments/pm/SKILL.md +3 -2
- package/departments/pm/skills/backlog-groom/SKILL.md +3 -2
- package/departments/pm/skills/discovery-plan/SKILL.md +3 -2
- package/departments/pm/skills/estimate-forecast/SKILL.md +3 -2
- package/departments/pm/skills/impact-map/SKILL.md +3 -2
- package/departments/pm/skills/kanban-setup/SKILL.md +3 -2
- package/departments/pm/skills/risk-register/SKILL.md +3 -2
- package/departments/pm/skills/roadmap-build/SKILL.md +3 -2
- package/departments/pm/skills/sprint-plan/SKILL.md +3 -2
- package/departments/pm/skills/stakeholder-map/SKILL.md +3 -2
- package/departments/pm/skills/standup-run/SKILL.md +3 -2
- package/departments/pm/skills/story-write/SKILL.md +3 -2
- package/departments/saas/SKILL.md +3 -2
- package/departments/saas/skills/benchmark-compare/SKILL.md +3 -2
- package/departments/saas/skills/churn-analysis/SKILL.md +3 -2
- package/departments/saas/skills/customer-success/SKILL.md +3 -2
- package/departments/saas/skills/growth-plan/SKILL.md +3 -2
- package/departments/saas/skills/gtm-strategy/SKILL.md +3 -2
- package/departments/saas/skills/launch-execute/SKILL.md +3 -2
- package/departments/saas/skills/metrics-dashboard/SKILL.md +3 -2
- package/departments/saas/skills/micro-saas-stack/SKILL.md +3 -2
- package/departments/saas/skills/mvp-build/SKILL.md +3 -2
- package/departments/saas/skills/niche-evaluate/SKILL.md +3 -2
- package/departments/saas/skills/onboarding-optimize/SKILL.md +3 -2
- package/departments/saas/skills/plg-setup/SKILL.md +3 -2
- package/departments/saas/skills/pricing-strategy/SKILL.md +3 -2
- package/departments/saas/skills/validate-idea/SKILL.md +3 -2
- package/departments/sales/SKILL.md +3 -2
- package/departments/sales/skills/challenger-sell/SKILL.md +3 -2
- package/departments/sales/skills/deal-qualify/SKILL.md +3 -2
- package/departments/sales/skills/discovery-call/SKILL.md +3 -2
- package/departments/sales/skills/forecast-revenue/SKILL.md +3 -2
- package/departments/sales/skills/negotiate-plan/SKILL.md +3 -2
- package/departments/sales/skills/objection-handle/SKILL.md +3 -2
- package/departments/sales/skills/pipeline-manage/SKILL.md +3 -2
- package/departments/sales/skills/pricing-negotiate/SKILL.md +3 -2
- package/departments/strategy/SKILL.md +3 -2
- package/departments/strategy/skills/blue-ocean/SKILL.md +3 -2
- package/departments/strategy/skills/bmc/SKILL.md +3 -2
- package/departments/strategy/skills/board-advisor/SKILL.md +3 -2
- package/departments/strategy/skills/cto-advisor/SKILL.md +3 -2
- package/departments/strategy/skills/extract-data/SKILL.md +3 -2
- package/departments/strategy/skills/five-forces/SKILL.md +3 -2
- package/departments/strategy/skills/growth-strategy/SKILL.md +3 -2
- package/departments/strategy/skills/moat-analysis/SKILL.md +3 -2
- package/departments/strategy/skills/position/SKILL.md +3 -2
- package/departments/strategy/skills/scenario-plan/SKILL.md +3 -2
- package/installer/config-seed.js +81 -25
- package/installer/graphify.js +340 -0
- package/installer/index.js +8 -1
- package/installer/keys.js +18 -3
- package/installer/update.js +8 -1
- package/knowledge/skills-manifest.json +1 -1
- package/mcps/registry.json +9 -9
- package/mcps/scripts/apply-mcps.sh +6 -2
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/scripts/dashboard-api.py +336 -124
- package/scripts/guide_gen.py +139 -0
- package/scripts/migrate_skills_kb_first.py +3 -2
- package/scripts/synapse-bridge.py +9 -1
package/core/synapse/layers.py
CHANGED
|
@@ -28,20 +28,20 @@ from typing import Any
|
|
|
28
28
|
from core.synapse.layers_base import Layer, LayerResult, PromptContext
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
31
|
-
"Layer",
|
|
32
|
-
"LayerResult",
|
|
33
|
-
"PromptContext",
|
|
34
|
-
"ConstitutionLayer",
|
|
35
|
-
"DepartmentLayer",
|
|
36
31
|
"AgentLayer",
|
|
37
|
-
"ProjectLayer",
|
|
38
32
|
"BranchLayer",
|
|
39
33
|
"CommandHintsLayer",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
34
|
+
"ConstitutionLayer",
|
|
35
|
+
"DepartmentLayer",
|
|
42
36
|
"ForgeContextLayer",
|
|
43
|
-
"SessionContextLayer",
|
|
44
37
|
"KBContextLayer",
|
|
38
|
+
"KnowledgeRetrievalLayer",
|
|
39
|
+
"Layer",
|
|
40
|
+
"LayerResult",
|
|
41
|
+
"ProjectLayer",
|
|
42
|
+
"PromptContext",
|
|
43
|
+
"QualityGateLayer",
|
|
44
|
+
"SessionContextLayer",
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
|
|
@@ -90,22 +90,75 @@ class ConstitutionLayer(Layer):
|
|
|
90
90
|
# --- L1: Department Detection ---
|
|
91
91
|
|
|
92
92
|
DEPARTMENT_PATTERNS: dict[str, str] = {
|
|
93
|
-
"dev":
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
"dev": (
|
|
94
|
+
r"\b("
|
|
95
|
+
r"build|code|feature|deploy|test|review|scaffold|"
|
|
96
|
+
r"debug|refactor|api|migration|stack|implement|fix|"
|
|
97
|
+
r"bug)\b"
|
|
98
|
+
),
|
|
99
|
+
"marketing": (
|
|
100
|
+
r"\b("
|
|
101
|
+
r"social|content|campaign|post|instagram|linkedin|"
|
|
102
|
+
r"twitter|tiktok|seo|marketing|ads|email.?campaign|"
|
|
103
|
+
r"growth|viral)\b"
|
|
104
|
+
),
|
|
105
|
+
"finance": (
|
|
106
|
+
r"\b("
|
|
107
|
+
r"budget|invoice|revenue|forecast|profit|loss|roi|"
|
|
108
|
+
r"margin|cash.?flow|financial|invest|valuation|"
|
|
109
|
+
r"pricing)\b"
|
|
110
|
+
),
|
|
111
|
+
"ecom": (
|
|
112
|
+
r"\b("
|
|
113
|
+
r"store|product|shop|shopify|ecommerce|catalog|"
|
|
114
|
+
r"inventory|cart|checkout|pricing|marketplace)\b"
|
|
115
|
+
),
|
|
116
|
+
"strategy": (
|
|
117
|
+
r"\b("
|
|
118
|
+
r"strategy|brainstorm|market|swot|competitors?|"
|
|
119
|
+
r"roadmap|pivot|growth|porter|blue.?ocean|"
|
|
120
|
+
r"positioning)\b"
|
|
121
|
+
),
|
|
98
122
|
"ops": r"\b(task|automate|meeting|workflow|process|schedule|sop|integration|zapier|n8n)\b",
|
|
99
123
|
"kb": r"\b(learn|persona|knowledge|youtube|transcribe|article|research|zettelkasten|note)\b",
|
|
100
|
-
"brand":
|
|
124
|
+
"brand": (
|
|
125
|
+
r"\b("
|
|
126
|
+
r"brand|logo|colors|palette|mockup|photoshoot|brand.?identity|"
|
|
127
|
+
r"brand.?guide|mood.?board|naming|visual.?design|motion|ux|ui|"
|
|
128
|
+
r"wireframe)\b"
|
|
129
|
+
),
|
|
101
130
|
"saas": r"\b(saas|micro.?saas|plg|freemium|churn|mrr|arr|subscription|onboarding|metrics)\b",
|
|
102
|
-
"landing":
|
|
103
|
-
|
|
131
|
+
"landing": (
|
|
132
|
+
r"\b("
|
|
133
|
+
r"landing|funnel|copy|headline|offer|launch|"
|
|
134
|
+
r"affiliate|webinar|conversion|sales.?page)\b"
|
|
135
|
+
),
|
|
136
|
+
"community": (
|
|
137
|
+
r"\b("
|
|
138
|
+
r"community|group|membership|discord|"
|
|
139
|
+
r"telegram|skool|circle|gamification|"
|
|
140
|
+
r"engagement)\b"
|
|
141
|
+
),
|
|
104
142
|
"content": r"\b(viral|hook|script|repurpose|youtube|tiktok|reels|shorts|newsletter|creator)\b",
|
|
105
143
|
"pm": r"\b(sprint|backlog|standup|retro|scrum|kanban|story|estimate|roadmap|agile)\b",
|
|
106
|
-
"lead":
|
|
107
|
-
|
|
108
|
-
|
|
144
|
+
"lead": (
|
|
145
|
+
r"\b("
|
|
146
|
+
r"leadership|delegation|1on1|feedback|"
|
|
147
|
+
r"culture|hiring|performance.?review|"
|
|
148
|
+
r"team.?build)\b"
|
|
149
|
+
),
|
|
150
|
+
"sales": (
|
|
151
|
+
r"\b("
|
|
152
|
+
r"pipeline|proposal|discovery.?call|objection|negotiate|"
|
|
153
|
+
r"deal|close|prospect|spin|challenger|cold.?email|"
|
|
154
|
+
r"outreach)\b"
|
|
155
|
+
),
|
|
156
|
+
"org": (
|
|
157
|
+
r"\b("
|
|
158
|
+
r"org.?design|hiring.?plan|onboarding|remote|"
|
|
159
|
+
r"meeting.?optimize|compensation|"
|
|
160
|
+
r"decision.?framework)\b"
|
|
161
|
+
),
|
|
109
162
|
}
|
|
110
163
|
|
|
111
164
|
|
|
@@ -317,10 +370,7 @@ class BranchLayer(Layer):
|
|
|
317
370
|
start = time.time()
|
|
318
371
|
branch = ctx.git_branch
|
|
319
372
|
# Hide main/master/dev branches
|
|
320
|
-
if branch in ("main", "master", "dev", ""):
|
|
321
|
-
tag = ""
|
|
322
|
-
else:
|
|
323
|
-
tag = f"[branch:{branch}]"
|
|
373
|
+
tag = "" if branch in ("main", "master", "dev", "") else f"[branch:{branch}]"
|
|
324
374
|
|
|
325
375
|
ms = int((time.time() - start) * 1000)
|
|
326
376
|
return LayerResult(
|
|
@@ -427,7 +477,7 @@ class QualityGateLayer(Layer):
|
|
|
427
477
|
def compute(self, ctx: PromptContext) -> LayerResult:
|
|
428
478
|
start = time.time()
|
|
429
479
|
try:
|
|
430
|
-
from core.governance.quality_api import
|
|
480
|
+
from core.governance.quality_api import list_approved, list_pending
|
|
431
481
|
|
|
432
482
|
pending = list_pending()
|
|
433
483
|
approved = list_approved(limit=3)
|
|
@@ -496,6 +546,17 @@ class KnowledgeRetrievalLayer(Layer):
|
|
|
496
546
|
def cache_ttl(self) -> int:
|
|
497
547
|
return 30
|
|
498
548
|
|
|
549
|
+
@property
|
|
550
|
+
def emits_block(self) -> bool:
|
|
551
|
+
"""The retrieved chunks are a real block, not the tag's value.
|
|
552
|
+
|
|
553
|
+
`[knowledge:N chunks]` makes a QUANTIFIED claim about content the
|
|
554
|
+
model never received before the content channel existed — the
|
|
555
|
+
loudest unbacked claim in the tag line (QG review). Opting in
|
|
556
|
+
delivers what the tag advertises.
|
|
557
|
+
"""
|
|
558
|
+
return True
|
|
559
|
+
|
|
499
560
|
@property
|
|
500
561
|
def priority(self) -> int:
|
|
501
562
|
return 35
|
|
@@ -580,7 +641,10 @@ class KnowledgeRetrievalLayer(Layer):
|
|
|
580
641
|
parts.append(f"[cached] {src}: {text}" if src else f"[cached] {text}")
|
|
581
642
|
parts.extend(snippets)
|
|
582
643
|
|
|
583
|
-
content
|
|
644
|
+
# Self-naming, because the content channel delivers this verbatim to
|
|
645
|
+
# the model: an unlabelled `[cached] file.md: …` fragment would read
|
|
646
|
+
# as stray text (the exact defect the opt-in channel exists to avoid).
|
|
647
|
+
content = f"[arka:knowledge] {' | '.join(parts)}" if parts else ""
|
|
584
648
|
chunk_count = len(snippets) + (len(overlapping) if overlapping else 0)
|
|
585
649
|
# RAG honesty (PR-3 v4.1): keyword-degraded results must never be
|
|
586
650
|
# presented as semantic similarity — label the tag explicitly.
|
|
@@ -639,7 +703,7 @@ class ForgeContextLayer(Layer):
|
|
|
639
703
|
parts = [f"Forge plan: {plan.id} ({plan.status.value})"]
|
|
640
704
|
if plan.critic.confidence > 0:
|
|
641
705
|
decisions = []
|
|
642
|
-
for
|
|
706
|
+
for _source, elements in plan.critic.synthesis.items():
|
|
643
707
|
decisions.extend(elements)
|
|
644
708
|
if decisions:
|
|
645
709
|
parts.append(f"Decisions: {'; '.join(decisions[:5])}")
|
|
@@ -701,7 +765,6 @@ class SessionContextLayer(Layer):
|
|
|
701
765
|
layer_id=self.id, tag="", content="", tokens_est=0, compute_ms=0, cached=False
|
|
702
766
|
)
|
|
703
767
|
|
|
704
|
-
lines = content.split("\n")
|
|
705
768
|
tag = "[session:resume]"
|
|
706
769
|
tokens = len(content.split())
|
|
707
770
|
|
|
@@ -720,6 +783,8 @@ class SessionContextLayer(Layer):
|
|
|
720
783
|
# `core.synapse.layers` import paths and helper references keep working.
|
|
721
784
|
|
|
722
785
|
from core.synapse.layers_kb import ( # noqa: E402,F401
|
|
786
|
+
_KB_CONFIG_PATH,
|
|
787
|
+
_MAX_FALLBACK_NOTES,
|
|
723
788
|
KBContextLayer,
|
|
724
789
|
_apply_grounding_policy,
|
|
725
790
|
_build_note_entry,
|
|
@@ -732,10 +797,8 @@ from core.synapse.layers_kb import ( # noqa: E402,F401
|
|
|
732
797
|
_hit_is_inferred,
|
|
733
798
|
_jaccard,
|
|
734
799
|
_jaccard_fallback,
|
|
735
|
-
_KB_CONFIG_PATH,
|
|
736
800
|
_l25_feature_flag_on,
|
|
737
801
|
_load_fallback_notes,
|
|
738
|
-
_MAX_FALLBACK_NOTES,
|
|
739
802
|
_note_from_vector_hit,
|
|
740
803
|
_tokenize_for_jaccard,
|
|
741
804
|
_vector_search,
|
|
@@ -69,6 +69,22 @@ class Layer(ABC):
|
|
|
69
69
|
"""
|
|
70
70
|
return False
|
|
71
71
|
|
|
72
|
+
@property
|
|
73
|
+
def emits_block(self) -> bool:
|
|
74
|
+
"""True when ``content`` is a self-describing full-text block.
|
|
75
|
+
|
|
76
|
+
OPT-IN, and deliberately so. The engine forwards ``content`` to the
|
|
77
|
+
model only for layers that declare this. Inferring it from
|
|
78
|
+
``content != tag`` looked equivalent and was not: most layers set
|
|
79
|
+
``content`` to their tag's VALUE (``dev``, ``active``,
|
|
80
|
+
``feat/plan-canvas``), so every prompt gained stray unlabeled lines
|
|
81
|
+
carrying nothing the tag had not already said (QG review).
|
|
82
|
+
|
|
83
|
+
A block must stand alone: it names itself (``[arka:kb-context]``,
|
|
84
|
+
``[arka:graph-remote unverified]``) and is meaningful without the tag.
|
|
85
|
+
"""
|
|
86
|
+
return False
|
|
87
|
+
|
|
72
88
|
@property
|
|
73
89
|
def session_sensitive(self) -> bool:
|
|
74
90
|
"""True when compute() has per-session side effects or output.
|
|
@@ -6,12 +6,14 @@ All names are re-exported by core.synapse.layers for backward
|
|
|
6
6
|
compatibility — import from there unless you need the module itself.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import contextlib
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
11
12
|
import re
|
|
12
13
|
import time
|
|
14
|
+
import urllib.request
|
|
13
15
|
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
16
|
+
from typing import Any
|
|
15
17
|
|
|
16
18
|
from core.synapse.layers_base import Layer, LayerResult, PromptContext
|
|
17
19
|
|
|
@@ -23,6 +25,9 @@ _KB_CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
|
|
|
23
25
|
# (Jaccard ranks the top few notes; scanning 2000 sorted-by-name first
|
|
24
26
|
# is plenty — see `_load_fallback_notes`) while still bounding worst-case latency.
|
|
25
27
|
_MAX_FALLBACK_NOTES = 2000
|
|
28
|
+
# A config.json past this is not a config — refuse it rather than parse it
|
|
29
|
+
# inside the Synapse budget. 4 MiB is ~1000x the real file.
|
|
30
|
+
_MAX_CONFIG_BYTES = 4 * 1024 * 1024
|
|
26
31
|
_KB_STOPWORDS: frozenset[str] = frozenset({
|
|
27
32
|
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for", "of",
|
|
28
33
|
"with", "by", "from", "as", "is", "was", "are", "were", "be", "been", "being",
|
|
@@ -34,19 +39,288 @@ _KB_STOPWORDS: frozenset[str] = frozenset({
|
|
|
34
39
|
})
|
|
35
40
|
|
|
36
41
|
|
|
42
|
+
def _read_json_dict(path: Path) -> dict:
|
|
43
|
+
"""Parse ``path`` into a dict, or ``{}``. The single never-raises reader.
|
|
44
|
+
|
|
45
|
+
Synapse does not wrap ``layer.compute``, so ANY exception escaping a
|
|
46
|
+
config read breaks all 12 layers for the turn. Three failure modes are
|
|
47
|
+
covered together, which is why this is one helper and not three
|
|
48
|
+
try/excepts: OSError (unreadable), ValueError (bad JSON *and* the
|
|
49
|
+
UnicodeDecodeError a non-UTF-8 file raises), and RecursionError (deeply
|
|
50
|
+
nested JSON — neither of the other two, and it escaped in QG review).
|
|
51
|
+
|
|
52
|
+
A fourth case needs no ``except`` at all: well-formed JSON whose root is
|
|
53
|
+
not an object — ``[1,2,3]`` or ``"yes"`` — would raise AttributeError on
|
|
54
|
+
the first ``.get``, so the return is isinstance-guarded instead.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
# is_file() excludes a FIFO, whose read_text() blocks FOREVER with no
|
|
58
|
+
# exception for the net below to catch (QG review). The size cap keeps
|
|
59
|
+
# a huge file from stalling the ~87ms budget. Both are stat-only.
|
|
60
|
+
if not path.is_file() or path.stat().st_size > _MAX_CONFIG_BYTES:
|
|
61
|
+
return {}
|
|
62
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
63
|
+
except (ValueError, OSError, RecursionError):
|
|
64
|
+
return {}
|
|
65
|
+
return data if isinstance(data, dict) else {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _section(data: dict, key: str) -> dict:
|
|
69
|
+
"""A dict-valued section of a config, or ``{}`` — never raises."""
|
|
70
|
+
value = data.get(key)
|
|
71
|
+
return value if isinstance(value, dict) else {}
|
|
72
|
+
|
|
73
|
+
|
|
37
74
|
def _l25_feature_flag_on() -> bool:
|
|
38
75
|
if os.environ.get("ARKA_BYPASS_L25", "").strip() == "1":
|
|
39
76
|
return False
|
|
40
77
|
if not _KB_CONFIG_PATH.exists():
|
|
41
78
|
return True
|
|
42
|
-
|
|
43
|
-
data = json.loads(_KB_CONFIG_PATH.read_text(encoding="utf-8"))
|
|
44
|
-
except (json.JSONDecodeError, OSError):
|
|
45
|
-
return True
|
|
46
|
-
synapse_cfg = data.get("synapse") or {}
|
|
79
|
+
synapse_cfg = _section(_read_json_dict(_KB_CONFIG_PATH), "synapse")
|
|
47
80
|
return bool(synapse_cfg.get("l25KbContext", True))
|
|
48
81
|
|
|
49
82
|
|
|
83
|
+
# --- Graphify graph-context pre-injection (opt-in, default OFF) ------------
|
|
84
|
+
# The knowledge graph is a per-user HTTP endpoint. Pre-injecting it costs up
|
|
85
|
+
# to THREE bounded round-trips (initialize → initialized → tools/call), i.e.
|
|
86
|
+
# ~3x _GRAPHIFY_TIMEOUT_S worst case against a ~87ms Synapse budget. That is
|
|
87
|
+
# why it is OFF by default (synapse.l25Graphify) and always fail-open: any
|
|
88
|
+
# missing config, corrupt config, unreachable host, or slow response yields
|
|
89
|
+
# no block and never raises.
|
|
90
|
+
_KEYS_PATH = Path.home() / ".arkaos" / "keys.json"
|
|
91
|
+
_GRAPHIFY_TIMEOUT_S = 0.3
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _l25_graphify_flag_on() -> bool:
|
|
95
|
+
if os.environ.get("ARKA_BYPASS_L25_GRAPHIFY", "").strip() == "1":
|
|
96
|
+
return False
|
|
97
|
+
if not _KB_CONFIG_PATH.exists():
|
|
98
|
+
return False
|
|
99
|
+
synapse_cfg = _section(_read_json_dict(_KB_CONFIG_PATH), "synapse")
|
|
100
|
+
return bool(synapse_cfg.get("l25Graphify", False))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# The exact ranges ``isPrivateHost`` in installer/graphify.js accepts. Kept
|
|
104
|
+
# as literals rather than ``ip.is_private`` (a strict superset: 0.0.0.0/8,
|
|
105
|
+
# TEST-NET-1/2/3, 240/4, 255.255.255.255 …) so the two implementations cannot
|
|
106
|
+
# drift — tests/python/test_graphify_url_parity.py drives one shared corpus
|
|
107
|
+
# through both and fails on any divergence.
|
|
108
|
+
_PRIVATE_V4 = ("127.0.0.0/8", "10.0.0.0/8", "192.168.0.0/16",
|
|
109
|
+
"172.16.0.0/12", "169.254.0.0/16")
|
|
110
|
+
_PRIVATE_V6 = ("::1/128", "fc00::/7", "fe80::/10")
|
|
111
|
+
# A whole-host label that is a bare number or 0x-hex, or any dotted form with
|
|
112
|
+
# a 0-prefixed (octal) octet: 134744072, 0x08080808, 0177.0.0.1.
|
|
113
|
+
_OBFUSCATED_IP_RE = re.compile(r"^(?:0[xX][0-9a-fA-F]+|\d+)$|(?:^|\.)0\d")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _ip_is_private(ip: Any) -> bool:
|
|
117
|
+
"""Membership in the explicit private ranges — no is_private shortcut."""
|
|
118
|
+
import ipaddress
|
|
119
|
+
|
|
120
|
+
nets = _PRIVATE_V6 if ip.version == 6 else _PRIVATE_V4
|
|
121
|
+
return any(ip in ipaddress.ip_network(n) for n in nets)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_private_host(raw_host: str) -> bool:
|
|
125
|
+
"""True only for hosts that cannot reach a public endpoint.
|
|
126
|
+
|
|
127
|
+
Python mirror of ``isPrivateHost`` in ``installer/graphify.js`` — and the
|
|
128
|
+
side that resolves the name before opening the socket, so it validates
|
|
129
|
+
independently of any registration-time check. It is NOT the only sender:
|
|
130
|
+
``registerGraphifyHttpMcp`` hands the same token to Claude Code, which
|
|
131
|
+
then talks to the endpoint on every session (installer/graphify.js).
|
|
132
|
+
|
|
133
|
+
A NAME is resolved rather than pattern-matched. Judging the string alone
|
|
134
|
+
let ``http://134744072/`` through (8.8.8.8 in decimal is a single-label
|
|
135
|
+
"name" with no dot) and the token went out over plaintext HTTP — captured
|
|
136
|
+
in QG review. Resolving also closes hex literals and DNS rebinding, since
|
|
137
|
+
every returned address must be private.
|
|
138
|
+
"""
|
|
139
|
+
import ipaddress
|
|
140
|
+
import socket
|
|
141
|
+
|
|
142
|
+
host = raw_host.strip()
|
|
143
|
+
if host.startswith("[") and host.endswith("]"):
|
|
144
|
+
host = host[1:-1]
|
|
145
|
+
host = host.rstrip(".") # trailing-dot FQDN is the same host
|
|
146
|
+
if not host:
|
|
147
|
+
return False
|
|
148
|
+
if "%" in host: # IPv6 zone-id: strip before parsing
|
|
149
|
+
host = host.split("%", 1)[0]
|
|
150
|
+
# Obfuscated IP encodings (decimal 134744072, hex 0x08080808, octal
|
|
151
|
+
# 0177.0.0.1) have no legitimate use in a configured endpoint and are the
|
|
152
|
+
# classic filter bypass. Rejected outright, matching the JS twin, rather
|
|
153
|
+
# than resolved — even when they decode to a private address.
|
|
154
|
+
if _OBFUSCATED_IP_RE.match(host):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
return _ip_is_private(ipaddress.ip_address(host))
|
|
159
|
+
except ValueError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
infos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
|
|
164
|
+
except (OSError, UnicodeError):
|
|
165
|
+
return False # unresolvable → never send a credential to it
|
|
166
|
+
if not infos:
|
|
167
|
+
return False
|
|
168
|
+
for info in infos:
|
|
169
|
+
try:
|
|
170
|
+
addr = ipaddress.ip_address(info[4][0].split("%", 1)[0])
|
|
171
|
+
except ValueError:
|
|
172
|
+
return False
|
|
173
|
+
if not _ip_is_private(addr):
|
|
174
|
+
return False # ANY public answer disqualifies the name
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _valid_graphify_url(raw: str) -> str:
|
|
179
|
+
"""``raw`` when safe to send a credential to, else ''."""
|
|
180
|
+
if not raw:
|
|
181
|
+
return ""
|
|
182
|
+
from urllib.parse import urlsplit
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
parts = urlsplit(raw)
|
|
186
|
+
except ValueError:
|
|
187
|
+
return ""
|
|
188
|
+
if parts.scheme == "https":
|
|
189
|
+
return raw
|
|
190
|
+
if parts.scheme != "http" or not parts.hostname:
|
|
191
|
+
return ""
|
|
192
|
+
return raw if _is_private_host(parts.hostname) else ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _resolve_graphify() -> tuple[str, str]:
|
|
196
|
+
"""(url, token) from config.json + keys.json + env. Empty when unset.
|
|
197
|
+
|
|
198
|
+
The URL is validated here and not only in the installer: an unvalidated
|
|
199
|
+
endpoint plus a token from keys.json is credential redirection, and this
|
|
200
|
+
function reads ``GRAPHIFY_URL`` straight from the environment.
|
|
201
|
+
"""
|
|
202
|
+
url = os.environ.get("GRAPHIFY_URL", "").strip()
|
|
203
|
+
token = os.environ.get("GRAPHIFY_TOKEN", "").strip()
|
|
204
|
+
gcfg = _section(_section(_read_json_dict(_KB_CONFIG_PATH), "knowledge"), "graphify")
|
|
205
|
+
if gcfg.get("enabled") is False:
|
|
206
|
+
return "", ""
|
|
207
|
+
url = _valid_graphify_url(url or str(gcfg.get("url") or "").strip())
|
|
208
|
+
if not url:
|
|
209
|
+
return "", ""
|
|
210
|
+
if not token and _KEYS_PATH.exists():
|
|
211
|
+
token = str(_read_json_dict(_KEYS_PATH).get("GRAPHIFY_TOKEN") or "").strip()
|
|
212
|
+
return url, token
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
216
|
+
"""Refuse every redirect.
|
|
217
|
+
|
|
218
|
+
urllib copies non-content headers across a redirect, so a 302 from the
|
|
219
|
+
graph server would forward ``Authorization: Bearer <token>`` to whatever
|
|
220
|
+
host it names — reproduced end-to-end in QG review. An MCP endpoint has
|
|
221
|
+
no legitimate reason to redirect, so the safe answer is to never follow.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _graphify_post(url: str, headers: dict, payload: dict, extra: dict | None = None):
|
|
229
|
+
"""One bounded MCP POST. Raises on transport failure — callers guard."""
|
|
230
|
+
body = json.dumps(payload).encode("utf-8")
|
|
231
|
+
req = urllib.request.Request(url, data=body, headers={**headers, **(extra or {})})
|
|
232
|
+
opener = urllib.request.build_opener(_NoRedirect)
|
|
233
|
+
return opener.open(req, timeout=_GRAPHIFY_TIMEOUT_S)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _graphify_open_session(url: str, headers: dict) -> dict:
|
|
237
|
+
"""Initialize the MCP session; returns the session-id header dict."""
|
|
238
|
+
init = {
|
|
239
|
+
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
240
|
+
"params": {
|
|
241
|
+
"protocolVersion": "2024-11-05", "capabilities": {},
|
|
242
|
+
"clientInfo": {"name": "synapse-l25", "version": "1.0"},
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
with _graphify_post(url, headers, init) as resp:
|
|
246
|
+
session_id = resp.headers.get("Mcp-Session-Id", "")
|
|
247
|
+
sid_header = {"Mcp-Session-Id": session_id} if session_id else {}
|
|
248
|
+
# Some MCP servers require the initialized notification before a
|
|
249
|
+
# tools/call; servers that do not need it ignore the extra request.
|
|
250
|
+
with contextlib.suppress(Exception):
|
|
251
|
+
_graphify_post(
|
|
252
|
+
url, headers,
|
|
253
|
+
{"jsonrpc": "2.0", "method": "notifications/initialized"}, sid_header,
|
|
254
|
+
).close()
|
|
255
|
+
return sid_header
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _graphify_parse_sse(raw: str) -> dict | None:
|
|
259
|
+
"""First JSON object in an SSE or plain JSON-RPC response body."""
|
|
260
|
+
for line in raw.splitlines():
|
|
261
|
+
stripped = line.strip()
|
|
262
|
+
if stripped.startswith("data:"):
|
|
263
|
+
stripped = stripped[5:].strip()
|
|
264
|
+
if not stripped:
|
|
265
|
+
continue
|
|
266
|
+
try:
|
|
267
|
+
return json.loads(stripped)
|
|
268
|
+
except ValueError:
|
|
269
|
+
continue
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _graphify_query(prompt: str, url: str, token: str) -> str | None:
|
|
274
|
+
"""Best-effort MCP `query_graph` over streamable HTTP. Never raises.
|
|
275
|
+
|
|
276
|
+
Returns the graph text-context, or None on any failure/timeout. Each of
|
|
277
|
+
the up-to-three requests is bounded by ``_GRAPHIFY_TIMEOUT_S``, so a fully
|
|
278
|
+
unresponsive endpoint costs ~3x that before this gives up and falls open.
|
|
279
|
+
"""
|
|
280
|
+
headers = {
|
|
281
|
+
"Authorization": f"Bearer {token}",
|
|
282
|
+
"Content-Type": "application/json",
|
|
283
|
+
"Accept": "application/json, text/event-stream",
|
|
284
|
+
}
|
|
285
|
+
try:
|
|
286
|
+
sid_header = _graphify_open_session(url, headers)
|
|
287
|
+
call = {
|
|
288
|
+
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
|
289
|
+
"params": {"name": "query_graph", "arguments": {"query": prompt[:200]}},
|
|
290
|
+
}
|
|
291
|
+
with _graphify_post(url, headers, call, sid_header) as resp:
|
|
292
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
293
|
+
data = _graphify_parse_sse(raw)
|
|
294
|
+
if not data:
|
|
295
|
+
return None
|
|
296
|
+
content = (data.get("result") or {}).get("content") or []
|
|
297
|
+
texts = [c.get("text", "") for c in content if c.get("type") == "text"]
|
|
298
|
+
return "\n".join(t for t in texts if t).strip() or None
|
|
299
|
+
except Exception:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _graphify_context(prompt: str) -> str:
|
|
304
|
+
"""Formatted ``[arka:graph-remote]`` block, or '' — fail-open.
|
|
305
|
+
|
|
306
|
+
Deliberately NOT ``[arka:graph-context]``: L2.7 GraphContextLayer already
|
|
307
|
+
owns that marker for the LOCAL graph, under a stricter contract (never
|
|
308
|
+
truncates a source location, never injects AMBIGUOUS nodes). This block
|
|
309
|
+
comes from a remote server, is not confidence-filtered, and is clipped to
|
|
310
|
+
a byte budget — same marker, contradictory guarantees, so it gets its own.
|
|
311
|
+
"""
|
|
312
|
+
if not prompt or not _l25_graphify_flag_on():
|
|
313
|
+
return ""
|
|
314
|
+
url, token = _resolve_graphify()
|
|
315
|
+
if not url or not token:
|
|
316
|
+
return ""
|
|
317
|
+
text = _graphify_query(prompt, url, token)
|
|
318
|
+
if not text:
|
|
319
|
+
return ""
|
|
320
|
+
excerpt = text[:600].rstrip()
|
|
321
|
+
return f"[arka:graph-remote unverified]\n{excerpt}\n"
|
|
322
|
+
|
|
323
|
+
|
|
50
324
|
def _tokenize_for_jaccard(text: str) -> set[str]:
|
|
51
325
|
if not text:
|
|
52
326
|
return set()
|
|
@@ -121,7 +395,13 @@ def _format_kb_block(notes: list[dict], degraded: bool = False) -> str:
|
|
|
121
395
|
path = note.get("path", "")
|
|
122
396
|
excerpt = note.get("excerpt", "")
|
|
123
397
|
relates = note.get("relates", []) or []
|
|
124
|
-
|
|
398
|
+
# Deliberately pt-PT: this block is operator-facing and every other
|
|
399
|
+
# string in it is pt-PT (header, Excerto, Relacionada, the degraded
|
|
400
|
+
# warning). The label was the lone English fragment. The RAG-honesty
|
|
401
|
+
# contract is unchanged — only the wording — and the two assertions
|
|
402
|
+
# guarding it in test_rag_honesty.py were updated in the same change
|
|
403
|
+
# for that reason, not to make a failing test pass.
|
|
404
|
+
suffix = " (inferida — não autoritativa)" if note.get("inferred") else ""
|
|
125
405
|
lines.append(f"- [[{title}]]{suffix} (path: `{path}`)")
|
|
126
406
|
if excerpt:
|
|
127
407
|
lines.append(f" Excerto: {excerpt}")
|
|
@@ -160,7 +440,7 @@ def _jaccard_fallback(
|
|
|
160
440
|
return [n for _, n in scored[:top_k]]
|
|
161
441
|
|
|
162
442
|
|
|
163
|
-
def _load_fallback_notes(vault_path:
|
|
443
|
+
def _load_fallback_notes(vault_path: Path | None) -> list[dict]:
|
|
164
444
|
if vault_path is None or not vault_path.exists() or not vault_path.is_dir():
|
|
165
445
|
return []
|
|
166
446
|
notes: list[dict] = []
|
|
@@ -211,7 +491,7 @@ def _hit_is_inferred(hit: dict) -> bool:
|
|
|
211
491
|
if not source:
|
|
212
492
|
return False
|
|
213
493
|
try:
|
|
214
|
-
with open(source,
|
|
494
|
+
with open(source, encoding="utf-8", errors="ignore") as fh:
|
|
215
495
|
head = fh.read(2048)
|
|
216
496
|
except OSError:
|
|
217
497
|
return False
|
|
@@ -246,7 +526,7 @@ def _apply_grounding_policy(notes: list[dict], max_notes: int) -> list[dict]:
|
|
|
246
526
|
"""Quarantine inferred notes (Dreaming output) from grounded context.
|
|
247
527
|
|
|
248
528
|
Policy (PR-3 v4.1): inferred notes are EXCLUDED by default; they are
|
|
249
|
-
only included — explicitly suffixed `(
|
|
529
|
+
only included — explicitly suffixed `(inferida — não autoritativa)`
|
|
250
530
|
by the formatter — when fewer than 2 grounded notes matched.
|
|
251
531
|
"""
|
|
252
532
|
grounded = [n for n in notes if not n.get("inferred")]
|
|
@@ -277,7 +557,7 @@ class KBContextLayer(Layer):
|
|
|
277
557
|
def __init__(
|
|
278
558
|
self,
|
|
279
559
|
vector_store: Any = None,
|
|
280
|
-
vault_path:
|
|
560
|
+
vault_path: str | None = None,
|
|
281
561
|
max_notes: int = 5,
|
|
282
562
|
min_similarity: float = 0.5,
|
|
283
563
|
) -> None:
|
|
@@ -302,6 +582,11 @@ class KBContextLayer(Layer):
|
|
|
302
582
|
def cache_ttl(self) -> int:
|
|
303
583
|
return 0
|
|
304
584
|
|
|
585
|
+
@property
|
|
586
|
+
def emits_block(self) -> bool:
|
|
587
|
+
"""Content is the self-naming [arka:kb-context] / graph block."""
|
|
588
|
+
return True
|
|
589
|
+
|
|
305
590
|
@property
|
|
306
591
|
def priority(self) -> int:
|
|
307
592
|
return 25
|
|
@@ -360,7 +645,7 @@ class KBContextLayer(Layer):
|
|
|
360
645
|
]
|
|
361
646
|
return _apply_grounding_policy(fallback_notes, self._max_notes), False
|
|
362
647
|
|
|
363
|
-
def build(self, prompt: str) ->
|
|
648
|
+
def build(self, prompt: str) -> str | None:
|
|
364
649
|
"""Public entrypoint — returns the formatted block or None."""
|
|
365
650
|
if not prompt or not _l25_feature_flag_on():
|
|
366
651
|
return None
|
|
@@ -369,6 +654,28 @@ class KBContextLayer(Layer):
|
|
|
369
654
|
return None
|
|
370
655
|
return _format_kb_block(notes[: self._max_notes], degraded=degraded)
|
|
371
656
|
|
|
657
|
+
def _safe_graph_block(self, prompt: str) -> str:
|
|
658
|
+
"""Graphify block, or ''. Guarded: Synapse does not wrap compute."""
|
|
659
|
+
try:
|
|
660
|
+
return _graphify_context(prompt)
|
|
661
|
+
except Exception:
|
|
662
|
+
return ""
|
|
663
|
+
|
|
664
|
+
def _render(self, notes: list[dict], degraded: bool, graph_block: str) -> tuple[str, str]:
|
|
665
|
+
"""(content, tag) for the assembled KB + graph context."""
|
|
666
|
+
parts = []
|
|
667
|
+
if notes:
|
|
668
|
+
parts.append(_format_kb_block(notes[: self._max_notes], degraded=degraded))
|
|
669
|
+
if graph_block:
|
|
670
|
+
parts.append(graph_block)
|
|
671
|
+
tag = (
|
|
672
|
+
f"[kb-context:{len(notes)} degraded=keyword]" if degraded
|
|
673
|
+
else f"[kb-context:{len(notes)}]"
|
|
674
|
+
)
|
|
675
|
+
if graph_block:
|
|
676
|
+
tag = f"{tag[:-1]} +graph]"
|
|
677
|
+
return "\n".join(parts), tag
|
|
678
|
+
|
|
372
679
|
def compute(self, ctx: PromptContext) -> LayerResult:
|
|
373
680
|
start = time.time()
|
|
374
681
|
if not ctx.user_input or not _l25_feature_flag_on():
|
|
@@ -378,18 +685,17 @@ class KBContextLayer(Layer):
|
|
|
378
685
|
except Exception:
|
|
379
686
|
return self._empty(start)
|
|
380
687
|
self._record(ctx, len(notes))
|
|
381
|
-
|
|
688
|
+
# Graphify graph-context is opt-in (default OFF) — a cheap no-op when
|
|
689
|
+
# the flag is off, and always fail-open so it never blocks Obsidian.
|
|
690
|
+
graph_block = self._safe_graph_block(ctx.user_input[:2000])
|
|
691
|
+
if not notes and not graph_block:
|
|
382
692
|
return self._empty(start)
|
|
383
|
-
block =
|
|
384
|
-
ms = int((time.time() - start) * 1000)
|
|
385
|
-
tag = f"[kb-context:{len(notes)}]"
|
|
386
|
-
if degraded:
|
|
387
|
-
tag = f"[kb-context:{len(notes)} degraded=keyword]"
|
|
693
|
+
block, tag = self._render(notes, degraded, graph_block)
|
|
388
694
|
return LayerResult(
|
|
389
695
|
layer_id=self.id,
|
|
390
696
|
tag=tag,
|
|
391
697
|
content=block,
|
|
392
698
|
tokens_est=len(block.split()),
|
|
393
|
-
compute_ms=
|
|
699
|
+
compute_ms=int((time.time() - start) * 1000),
|
|
394
700
|
cached=False,
|
|
395
701
|
)
|
|
@@ -80,6 +80,13 @@ const links = [[{
|
|
|
80
80
|
onSelect: () => {
|
|
81
81
|
open.value = false
|
|
82
82
|
}
|
|
83
|
+
}, {
|
|
84
|
+
label: 'Plan Canvas',
|
|
85
|
+
icon: 'i-lucide-drafting-compass',
|
|
86
|
+
to: '/plan-canvas',
|
|
87
|
+
onSelect: () => {
|
|
88
|
+
open.value = false
|
|
89
|
+
}
|
|
83
90
|
}, {
|
|
84
91
|
label: 'Knowledge',
|
|
85
92
|
icon: 'i-lucide-brain',
|