awkno 0.1.0__py3-none-any.whl
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.
- awkno/__init__.py +25 -0
- awkno/_doctor.py +139 -0
- awkno/cli.py +355 -0
- awkno/corpus.py +216 -0
- awkno/generate.py +549 -0
- awkno/pages/agent-senses.json +16 -0
- awkno/pages/agent-vm.json +14 -0
- awkno/pages/aitherconnect.json +11 -0
- awkno/pages/aitherkvcache.json +11 -0
- awkno/pages/aitherzero.json +11 -0
- awkno/pages/awask.json +15 -0
- awkno/pages/awbac.json +11 -0
- awkno/pages/awbeads.json +13 -0
- awkno/pages/awbonsai.json +13 -0
- awkno/pages/awbrowse.json +13 -0
- awkno/pages/awdit.json +12 -0
- awkno/pages/awdk.json +17 -0
- awkno/pages/awevolve.json +15 -0
- awkno/pages/awfind.json +13 -0
- awkno/pages/awgit.json +12 -0
- awkno/pages/awgraph.json +13 -0
- awkno/pages/awiam.json +12 -0
- awkno/pages/awkit.json +13 -0
- awkno/pages/awkno.json +13 -0
- awkno/pages/awknowledge.json +12 -0
- awkno/pages/awm.json +13 -0
- awkno/pages/awmail.json +18 -0
- awkno/pages/awnboard.json +20 -0
- awkno/pages/awnest.json +19 -0
- awkno/pages/awnet.json +14 -0
- awkno/pages/awnix.json +20 -0
- awkno/pages/awnode.json +12 -0
- awkno/pages/awpack.json +14 -0
- awkno/pages/awpredict.json +11 -0
- awkno/pages/awprism.json +14 -0
- awkno/pages/awreason.json +14 -0
- awkno/pages/awrecover.json +13 -0
- awkno/pages/awrecurse.json +13 -0
- awkno/pages/awrelay.json +12 -0
- awkno/pages/awrena.json +14 -0
- awkno/pages/awrepl.json +13 -0
- awkno/pages/awresearch.json +14 -0
- awkno/pages/awrise.json +15 -0
- awkno/pages/awrun.json +14 -0
- awkno/pages/awscreen.json +13 -0
- awkno/pages/awseal.json +13 -0
- awkno/pages/awsh.json +13 -0
- awkno/pages/awshare.json +12 -0
- awkno/pages/awskills.json +12 -0
- awkno/pages/awsprite.json +14 -0
- awkno/pages/awsync.json +16 -0
- awkno/pages/awtoll.json +13 -0
- awkno/pages/awtunnel.json +12 -0
- awkno/pages/awvision.json +13 -0
- awkno/pages/awvoice.json +13 -0
- awkno/pages/awwall.json +19 -0
- awkno/pages/bead-space.json +12 -0
- awkno/pages/browser-companion-graduation.json +15 -0
- awkno/pages/cited-research.json +14 -0
- awkno/pages/gobbonet-agentic.json +12 -0
- awkno/pages/gobbonet.json +13 -0
- awkno/pages/guide-00.json +16 -0
- awkno/pages/guide-01.json +14 -0
- awkno/pages/guide-02.json +14 -0
- awkno/pages/guide-03.json +14 -0
- awkno/pages/guide-04.json +15 -0
- awkno/pages/guide-05.json +16 -0
- awkno/pages/guide-06.json +15 -0
- awkno/pages/guide-07.json +15 -0
- awkno/pages/guide-08.json +17 -0
- awkno/pages/guide-09.json +13 -0
- awkno/pages/guide.json +22 -0
- awkno/pages/law-01.json +11 -0
- awkno/pages/law-02.json +11 -0
- awkno/pages/law-03.json +11 -0
- awkno/pages/law-04.json +11 -0
- awkno/pages/law-05.json +11 -0
- awkno/pages/law-06.json +11 -0
- awkno/pages/law-07.json +11 -0
- awkno/pages/law-08.json +11 -0
- awkno/pages/law-09.json +11 -0
- awkno/pages/law-10.json +11 -0
- awkno/pages/law-11.json +11 -0
- awkno/pages/law-12.json +11 -0
- awkno/pages/law-13.json +11 -0
- awkno/pages/law-14.json +11 -0
- awkno/pages/law-15.json +11 -0
- awkno/pages/law-16.json +11 -0
- awkno/pages/law-17.json +11 -0
- awkno/pages/law-18.json +11 -0
- awkno/pages/law-19.json +11 -0
- awkno/pages/law-20.json +11 -0
- awkno/pages/one-surface.json +15 -0
- awkno/pages/persona.json +12 -0
- awkno/pages/personal-mesh-pool.json +13 -0
- awkno/pages/provenance.json +14 -0
- awkno/pages/shared-worktree.json +14 -0
- awkno/pages/the-front-door.json +15 -0
- awkno/pages/the-reasoning-loop.json +14 -0
- awkno/pages/who-what-did.json +13 -0
- awkno-0.1.0.dist-info/METADATA +336 -0
- awkno-0.1.0.dist-info/RECORD +106 -0
- awkno-0.1.0.dist-info/WHEEL +5 -0
- awkno-0.1.0.dist-info/entry_points.txt +2 -0
- awkno-0.1.0.dist-info/licenses/LICENSE +118 -0
- awkno-0.1.0.dist-info/top_level.txt +1 -0
awkno/corpus.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Load and query the offline corpus.
|
|
2
|
+
|
|
3
|
+
Pages are stored as JSON files under awkno/pages/. This module handles the
|
|
4
|
+
loading, searching, and formatting of those pages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from difflib import SequenceMatcher
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotFoundError(Exception):
|
|
17
|
+
"""Raised when a topic is not found."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AwknoPage:
|
|
24
|
+
"""A single man page."""
|
|
25
|
+
|
|
26
|
+
topic: str
|
|
27
|
+
category: str
|
|
28
|
+
synopsis: str
|
|
29
|
+
description: str
|
|
30
|
+
adopt: Optional[str] = None
|
|
31
|
+
status: Optional[str] = None
|
|
32
|
+
see_also: Optional[list[str]] = None
|
|
33
|
+
body: Optional[str] = None
|
|
34
|
+
# The law filename's own slug (`design-for-the-silence`). Carried rather
|
|
35
|
+
# than re-derived from the synopsis: the generator already computes it, and
|
|
36
|
+
# a slug inferred from a title silently stops matching the moment a law is
|
|
37
|
+
# retitled.
|
|
38
|
+
slug: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
def render(self, plain: bool = False) -> str:
|
|
41
|
+
"""Render the page as text.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
plain: If True, no ANSI codes. If False, use formatting (when TTY).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The formatted page text.
|
|
48
|
+
"""
|
|
49
|
+
lines = []
|
|
50
|
+
lines.append("NAME")
|
|
51
|
+
lines.append(f" {self.topic} — {self.synopsis}")
|
|
52
|
+
lines.append("")
|
|
53
|
+
|
|
54
|
+
if self.adopt:
|
|
55
|
+
lines.append("ADOPT")
|
|
56
|
+
for line in self.adopt.split("\n"):
|
|
57
|
+
lines.append(f" {line}")
|
|
58
|
+
lines.append("")
|
|
59
|
+
|
|
60
|
+
if self.description:
|
|
61
|
+
lines.append("DESCRIPTION")
|
|
62
|
+
for line in self.description.split("\n"):
|
|
63
|
+
lines.append(f" {line}")
|
|
64
|
+
lines.append("")
|
|
65
|
+
|
|
66
|
+
if self.body:
|
|
67
|
+
lines.append("DETAILS")
|
|
68
|
+
for line in self.body.split("\n"):
|
|
69
|
+
lines.append(f" {line}")
|
|
70
|
+
lines.append("")
|
|
71
|
+
|
|
72
|
+
if self.status:
|
|
73
|
+
lines.append("STATUS")
|
|
74
|
+
lines.append(f" {self.status}")
|
|
75
|
+
lines.append("")
|
|
76
|
+
|
|
77
|
+
if self.see_also:
|
|
78
|
+
lines.append("SEE ALSO")
|
|
79
|
+
for item in self.see_also:
|
|
80
|
+
lines.append(f" {item}")
|
|
81
|
+
|
|
82
|
+
text = "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
if not plain:
|
|
85
|
+
# Check if stdout is a TTY for formatting
|
|
86
|
+
import sys
|
|
87
|
+
|
|
88
|
+
if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
|
|
89
|
+
# Apply ANSI formatting
|
|
90
|
+
text = text.replace("NAME\n", "\033[1mNAME\033[0m\n")
|
|
91
|
+
text = text.replace("ADOPT\n", "\033[1mADOPT\033[0m\n")
|
|
92
|
+
text = text.replace("DESCRIPTION\n", "\033[1mDESCRIPTION\033[0m\n")
|
|
93
|
+
text = text.replace("DETAILS\n", "\033[1mDETAILS\033[0m\n")
|
|
94
|
+
text = text.replace("STATUS\n", "\033[1mSTATUS\033[0m\n")
|
|
95
|
+
text = text.replace("SEE ALSO\n", "\033[1mSEE ALSO\033[0m\n")
|
|
96
|
+
|
|
97
|
+
return text
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict:
|
|
100
|
+
"""Convert to dict for JSON serialization."""
|
|
101
|
+
return {
|
|
102
|
+
"topic": self.topic,
|
|
103
|
+
"category": self.category,
|
|
104
|
+
"synopsis": self.synopsis,
|
|
105
|
+
"description": self.description,
|
|
106
|
+
"adopt": self.adopt,
|
|
107
|
+
"status": self.status,
|
|
108
|
+
"see_also": self.see_also,
|
|
109
|
+
"body": self.body,
|
|
110
|
+
"slug": self.slug,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_dict(cls, data: dict) -> AwknoPage:
|
|
115
|
+
"""Create from dict (JSON deserialization)."""
|
|
116
|
+
return cls(**data)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AwknoRegistry:
|
|
120
|
+
"""Load and query all pages."""
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
"""Load all pages from the corpus directory."""
|
|
124
|
+
self.pages: dict[str, AwknoPage] = {}
|
|
125
|
+
self._load_pages()
|
|
126
|
+
|
|
127
|
+
def _load_pages(self) -> None:
|
|
128
|
+
"""Load all JSON files from awkno/pages/."""
|
|
129
|
+
pages_dir = Path(__file__).parent / "pages"
|
|
130
|
+
|
|
131
|
+
if not pages_dir.exists():
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
for json_file in pages_dir.glob("*.json"):
|
|
135
|
+
try:
|
|
136
|
+
with open(json_file) as f:
|
|
137
|
+
data = json.load(f)
|
|
138
|
+
page = AwknoPage.from_dict(data)
|
|
139
|
+
self.pages[page.topic.lower()] = page
|
|
140
|
+
except (json.JSONDecodeError, ValueError):
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def get(self, topic: str) -> AwknoPage:
|
|
144
|
+
"""Get a page by topic name.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
topic: The topic name (case-insensitive).
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The AwknoPage.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
NotFoundError: If the topic is not found.
|
|
154
|
+
"""
|
|
155
|
+
key = topic.lower()
|
|
156
|
+
if key in self.pages:
|
|
157
|
+
return self.pages[key]
|
|
158
|
+
raise NotFoundError(f"Topic '{topic}' not found")
|
|
159
|
+
|
|
160
|
+
def list_topics(self) -> list[str]:
|
|
161
|
+
"""List all available topics."""
|
|
162
|
+
return sorted(self.pages.keys())
|
|
163
|
+
|
|
164
|
+
def list_by_category(self, category: str) -> list[AwknoPage]:
|
|
165
|
+
"""List pages by category.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
category: One of "brick", "stack", "law", "topic".
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of pages in that category.
|
|
172
|
+
"""
|
|
173
|
+
return [
|
|
174
|
+
page for page in self.pages.values() if page.category == category
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
def search(self, term: str) -> list[tuple[AwknoPage, float]]:
|
|
178
|
+
"""Search pages by keyword.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
term: Search term (case-insensitive).
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of (page, score) tuples sorted by score (highest first).
|
|
185
|
+
"""
|
|
186
|
+
term_lower = term.lower()
|
|
187
|
+
results = []
|
|
188
|
+
|
|
189
|
+
for page in self.pages.values():
|
|
190
|
+
# Score based on where the term appears
|
|
191
|
+
score = 0.0
|
|
192
|
+
|
|
193
|
+
if term_lower in page.topic.lower():
|
|
194
|
+
score += 1.0
|
|
195
|
+
|
|
196
|
+
if page.synopsis and term_lower in page.synopsis.lower():
|
|
197
|
+
score += 0.8
|
|
198
|
+
|
|
199
|
+
if page.description and term_lower in page.description.lower():
|
|
200
|
+
score += 0.5
|
|
201
|
+
|
|
202
|
+
if page.adopt and term_lower in page.adopt.lower():
|
|
203
|
+
score += 0.3
|
|
204
|
+
|
|
205
|
+
if page.body and term_lower in page.body.lower():
|
|
206
|
+
score += 0.2
|
|
207
|
+
|
|
208
|
+
# Use SequenceMatcher for fuzzy matching on topic
|
|
209
|
+
ratio = SequenceMatcher(None, term_lower, page.topic.lower()).ratio()
|
|
210
|
+
score += ratio * 0.5
|
|
211
|
+
|
|
212
|
+
if score > 0:
|
|
213
|
+
results.append((page, score))
|
|
214
|
+
|
|
215
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
216
|
+
return results
|