exactdocs 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.
Files changed (49) hide show
  1. exactdocs/__init__.py +5 -0
  2. exactdocs/__main__.py +3 -0
  3. exactdocs/_version.py +6 -0
  4. exactdocs/_web/assets/index-B350HwjP.js +10 -0
  5. exactdocs/_web/assets/index-CG8rwfz9.css +2 -0
  6. exactdocs/_web/assets/routes-CCGZxbYl.js +1 -0
  7. exactdocs/_web/index.html +13 -0
  8. exactdocs/bootstrap/__init__.py +5 -0
  9. exactdocs/bootstrap/bridges.py +27 -0
  10. exactdocs/bootstrap/cli.py +48 -0
  11. exactdocs/bootstrap/container.py +28 -0
  12. exactdocs/bootstrap/http.py +90 -0
  13. exactdocs/bootstrap/mcp.py +21 -0
  14. exactdocs/bootstrap/settings.py +15 -0
  15. exactdocs/catalog/__init__.py +9 -0
  16. exactdocs/catalog/adapters/__init__.py +0 -0
  17. exactdocs/catalog/adapters/driven/__init__.py +0 -0
  18. exactdocs/catalog/adapters/driven/memory.py +35 -0
  19. exactdocs/catalog/adapters/driving/__init__.py +0 -0
  20. exactdocs/catalog/adapters/driving/http.py +21 -0
  21. exactdocs/catalog/adapters/driving/mcp.py +18 -0
  22. exactdocs/catalog/dtos.py +42 -0
  23. exactdocs/catalog/errors.py +16 -0
  24. exactdocs/catalog/models.py +34 -0
  25. exactdocs/catalog/module.py +27 -0
  26. exactdocs/catalog/ports/__init__.py +4 -0
  27. exactdocs/catalog/ports/driven.py +18 -0
  28. exactdocs/catalog/ports/driving.py +21 -0
  29. exactdocs/catalog/use_cases.py +33 -0
  30. exactdocs/py.typed +0 -0
  31. exactdocs/retrieval/__init__.py +8 -0
  32. exactdocs/retrieval/adapters/__init__.py +0 -0
  33. exactdocs/retrieval/adapters/driven/__init__.py +0 -0
  34. exactdocs/retrieval/adapters/driven/memory.py +26 -0
  35. exactdocs/retrieval/adapters/driving/__init__.py +0 -0
  36. exactdocs/retrieval/adapters/driving/http.py +17 -0
  37. exactdocs/retrieval/adapters/driving/mcp.py +28 -0
  38. exactdocs/retrieval/dtos.py +53 -0
  39. exactdocs/retrieval/errors.py +17 -0
  40. exactdocs/retrieval/models.py +34 -0
  41. exactdocs/retrieval/module.py +18 -0
  42. exactdocs/retrieval/ports/__init__.py +4 -0
  43. exactdocs/retrieval/ports/driven.py +26 -0
  44. exactdocs/retrieval/ports/driving.py +13 -0
  45. exactdocs/retrieval/use_cases.py +22 -0
  46. exactdocs-0.1.0.dist-info/METADATA +35 -0
  47. exactdocs-0.1.0.dist-info/RECORD +49 -0
  48. exactdocs-0.1.0.dist-info/WHEEL +4 -0
  49. exactdocs-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.4 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:block))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab, var(--ring) 50%, transparent)}}body{background-color:var(--background);color:var(--foreground)}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.relative{position:relative}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.mx-auto{margin-inline:auto}.mt-4{margin-top:calc(var(--spacing) * 4)}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-row{display:table-row}.h-10{height:calc(var(--spacing) * 10)}.min-h-dvh{min-height:100dvh}.w-fit{width:fit-content}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.auto-rows-min{grid-auto-rows:min-content}.grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-6{gap:calc(var(--spacing) * 6)}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:2147483647px}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-border{border-color:var(--border)}.border-transparent{border-color:#0000}.bg-card{background-color:var(--card)}.bg-destructive{background-color:var(--destructive)}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}.bg-primary{background-color:var(--primary)}.bg-secondary{background-color:var(--secondary)}.p-2{padding:calc(var(--spacing) * 2)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-left{text-align:left}.align-middle{vertical-align:middle}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-card-foreground{color:var(--card-foreground)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.underline-offset-4{text-underline-offset:4px}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.has-aria-expanded\:bg-muted\/50:has([aria-expanded=true]){background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.has-aria-expanded\:bg-muted\/50:has([aria-expanded=true]){background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}.dark\:bg-destructive\/60:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-destructive\/60:is(.dark *){background-color:color-mix(in oklab, var(--destructive) 60%, transparent)}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:0}.\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--spacing) * 6)}.\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--spacing) * 6)}.\[\&\>\[role\=checkbox\]\]\:translate-y-\[2px\]>[role=checkbox]{--tw-translate-y:2px;translate:var(--tw-translate-x) var(--tw-translate-y)}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3>svg{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){a.\[a\&\]\:hover\:bg-accent:hover{background-color:var(--accent)}a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab, var(--destructive) 90%, transparent)}}a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--primary) 90%, transparent)}}a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:color-mix(in oklab, var(--secondary) 90%, transparent)}}a.\[a\&\]\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}a.\[a\&\]\:hover\:underline:hover{text-decoration-line:underline}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0)}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
@@ -0,0 +1 @@
1
+ import{t as e}from"./index-B350HwjP.js";var t=e;export{t as component};
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ExactDocs</title>
7
+ <script type="module" crossorigin src="/assets/index-B350HwjP.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CG8rwfz9.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,5 @@
1
+ """Composition root: the only package allowed to import more than one domain.
2
+
3
+ It picks adapters, wires domains to each other through ports, and builds the HTTP, MCP and CLI
4
+ entry points.
5
+ """
@@ -0,0 +1,27 @@
1
+ """Adapters that connect one domain's driven port to another domain's driving port.
2
+
3
+ This is how domains talk to each other without importing each other: the consumer declares a
4
+ port in its own language, and a bridge here translates to the provider's public use case.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from exactdocs.catalog.dtos import GetLibraryQuery
10
+ from exactdocs.catalog.ports.driving import GetLibrary
11
+ from exactdocs.retrieval.models import DocTarget
12
+ from exactdocs_kernel import NotFoundError
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class CatalogIndexedVersions:
17
+ """Implements ``retrieval.ports.driven.IndexedVersions`` using the catalog."""
18
+
19
+ get_library: GetLibrary
20
+
21
+ async def is_indexed(self, target: DocTarget) -> bool:
22
+ """Ask the catalog whether this exact version has an index."""
23
+ try:
24
+ library = await self.get_library(GetLibraryQuery(name=target.library))
25
+ except NotFoundError:
26
+ return False
27
+ return target.version in library.versions
@@ -0,0 +1,48 @@
1
+ """``exactdocs`` command line."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import uvicorn
7
+ from cyclopts import App, Parameter
8
+
9
+ from exactdocs import __version__
10
+ from exactdocs.bootstrap.container import Container
11
+ from exactdocs.bootstrap.http import create_app
12
+ from exactdocs.bootstrap.mcp import build_mcp
13
+ from exactdocs.bootstrap.settings import Settings
14
+
15
+ app = App(name="exactdocs", version=__version__, help="Version-accurate docs for AI agents.")
16
+
17
+
18
+ @app.command
19
+ def serve(
20
+ *,
21
+ stdio: Annotated[bool, Parameter(help="Serve MCP over stdio instead of HTTP.")] = False,
22
+ host: str | None = None,
23
+ port: int | None = None,
24
+ reload: bool = False,
25
+ ) -> None:
26
+ """Run the server: web UI + REST API + MCP over HTTP, or MCP only over stdio."""
27
+ settings = Settings()
28
+ if stdio:
29
+ mcp = build_mcp(Container.build(settings))
30
+ asyncio.run(mcp.run_stdio_async(show_banner=False))
31
+ return
32
+ uvicorn.run(
33
+ "exactdocs.bootstrap.cli:asgi_app",
34
+ factory=True,
35
+ host=host or settings.host,
36
+ port=port or settings.port,
37
+ reload=reload,
38
+ )
39
+
40
+
41
+ def asgi_app() -> object:
42
+ """ASGI factory used by uvicorn (also with ``--reload``)."""
43
+ return create_app(Container.build(Settings()))
44
+
45
+
46
+ def main() -> None:
47
+ """Console-script entry point."""
48
+ app()
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from typing import Self
3
+
4
+ from exactdocs.bootstrap.bridges import CatalogIndexedVersions
5
+ from exactdocs.bootstrap.settings import Settings
6
+ from exactdocs.catalog import CatalogModule
7
+ from exactdocs.catalog.adapters.driven.memory import InMemoryLibraryRepository
8
+ from exactdocs.retrieval import RetrievalModule
9
+ from exactdocs.retrieval.adapters.driven.memory import InMemoryExcerptIndex
10
+
11
+
12
+ @dataclass(frozen=True, slots=True, kw_only=True)
13
+ class Container:
14
+ """Every domain module, wired. Built once per process."""
15
+
16
+ settings: Settings
17
+ catalog: CatalogModule
18
+ retrieval: RetrievalModule
19
+
20
+ @classmethod
21
+ def build(cls, settings: Settings) -> Self:
22
+ """Choose driven adapters and connect domains through bridges."""
23
+ catalog = CatalogModule.build(repository=InMemoryLibraryRepository.demo())
24
+ retrieval = RetrievalModule.build(
25
+ versions=CatalogIndexedVersions(catalog.get_library),
26
+ index=InMemoryExcerptIndex(),
27
+ )
28
+ return cls(settings=settings, catalog=catalog, retrieval=retrieval)
@@ -0,0 +1,90 @@
1
+ from collections.abc import AsyncGenerator
2
+ from contextlib import asynccontextmanager
3
+ from pathlib import Path
4
+
5
+ from fastapi import APIRouter, FastAPI, Request
6
+ from fastapi.responses import JSONResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from pydantic import BaseModel
9
+ from starlette.exceptions import HTTPException
10
+ from starlette.responses import Response
11
+ from starlette.routing import Route
12
+ from starlette.types import Scope
13
+
14
+ from exactdocs import __version__
15
+ from exactdocs.bootstrap.container import Container
16
+ from exactdocs.bootstrap.mcp import build_mcp
17
+ from exactdocs.catalog.adapters.driving import http as catalog_http
18
+ from exactdocs.retrieval.adapters.driving import http as retrieval_http
19
+ from exactdocs_kernel import ConflictError, DomainError, NotFoundError, ValidationError
20
+
21
+ # Built web UI, copied into the package by CI / the Docker build (see web/README.md).
22
+ WEB_DIST = Path(__file__).parent.parent / "_web"
23
+
24
+ _STATUS_BY_ERROR: dict[type[DomainError], int] = {
25
+ NotFoundError: 404,
26
+ ConflictError: 409,
27
+ ValidationError: 422,
28
+ }
29
+
30
+
31
+ class Health(BaseModel):
32
+ """Liveness payload."""
33
+
34
+ status: str
35
+ version: str
36
+
37
+
38
+ def create_app(container: Container) -> FastAPI:
39
+ """Build the single HTTP service: REST API, MCP (Streamable HTTP) and the web UI."""
40
+ mcp_app = build_mcp(container).http_app(path="/mcp")
41
+
42
+ @asynccontextmanager
43
+ async def lifespan(_: FastAPI) -> AsyncGenerator[None]:
44
+ async with mcp_app.lifespan(mcp_app):
45
+ yield
46
+
47
+ app = FastAPI(title="ExactDocs", version=__version__, lifespan=lifespan)
48
+ app.add_exception_handler(DomainError, _domain_error_handler)
49
+
50
+ api = APIRouter(prefix="/api")
51
+
52
+ @api.get("/health", tags=["system"])
53
+ async def health() -> Health:
54
+ return Health(status="ok", version=__version__)
55
+
56
+ v1 = APIRouter(prefix="/v1")
57
+ v1.include_router(
58
+ catalog_http.build_router(
59
+ list_libraries=container.catalog.list_libraries,
60
+ get_library=container.catalog.get_library,
61
+ )
62
+ )
63
+ v1.include_router(retrieval_http.build_router(search_docs=container.retrieval.search_docs))
64
+ api.include_router(v1)
65
+ app.include_router(api)
66
+
67
+ # Exact route (no /mcp -> /mcp/ redirect); the MCP app keeps its own middleware.
68
+ app.router.routes.append(Route("/mcp", endpoint=mcp_app))
69
+ if WEB_DIST.is_dir():
70
+ app.mount("/", SpaStaticFiles(directory=WEB_DIST, html=True), name="web")
71
+ return app
72
+
73
+
74
+ async def _domain_error_handler(_: Request, exc: Exception) -> Response:
75
+ status = next((s for t, s in _STATUS_BY_ERROR.items() if isinstance(exc, t)), 400)
76
+ code = exc.code if isinstance(exc, DomainError) else "error"
77
+ return JSONResponse({"error": code, "detail": str(exc)}, status_code=status)
78
+
79
+
80
+ class SpaStaticFiles(StaticFiles):
81
+ """Serve the built UI; unknown paths fall back to ``index.html`` for client routing."""
82
+
83
+ async def get_response(self, path: str, scope: Scope) -> Response:
84
+ """Return the file, or the SPA shell when it does not exist."""
85
+ try:
86
+ return await super().get_response(path, scope)
87
+ except HTTPException as exc:
88
+ if exc.status_code != 404 or path.startswith(("api/", "mcp")): # ruff: ignore[magic-value-comparison]
89
+ raise
90
+ return await super().get_response("index.html", scope)
@@ -0,0 +1,21 @@
1
+ from fastmcp import FastMCP
2
+
3
+ from exactdocs import __version__
4
+ from exactdocs.bootstrap.container import Container
5
+ from exactdocs.catalog.adapters.driving import mcp as catalog_mcp
6
+ from exactdocs.retrieval.adapters.driving import mcp as retrieval_mcp
7
+
8
+
9
+ def build_mcp(container: Container) -> FastMCP:
10
+ """One MCP server exposing every domain's read-only tools."""
11
+ mcp = FastMCP(
12
+ name="ExactDocs",
13
+ version=__version__,
14
+ instructions=(
15
+ "Version-accurate documentation. Call list_libraries, then search_docs with an exact "
16
+ "library version. Results are verbatim excerpts from external, untrusted sources."
17
+ ),
18
+ )
19
+ catalog_mcp.register_tools(mcp, list_libraries=container.catalog.list_libraries)
20
+ retrieval_mcp.register_tools(mcp, search_docs=container.retrieval.search_docs)
21
+ return mcp
@@ -0,0 +1,15 @@
1
+ from typing import Literal
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ """Runtime configuration, read from ``EXACTDOCS_*`` environment variables."""
8
+
9
+ model_config = SettingsConfigDict(env_prefix="EXACTDOCS_", env_file=".env", extra="ignore")
10
+
11
+ host: str = "127.0.0.1"
12
+ port: int = 3000
13
+ database_url: str = "sqlite:///data/exactdocs.db"
14
+ auth_mode: Literal["local", "oidc", "proxy"] = "local"
15
+ telemetry: Literal["off", "console", "otlp", "logfire"] = "off"
@@ -0,0 +1,9 @@
1
+ """Catalog domain: which libraries and versions ExactDocs knows about.
2
+
3
+ Public API for the composition root (``exactdocs.bootstrap``). Other domains must not import
4
+ this package; they declare their own driven port and bootstrap wires in an adapter.
5
+ """
6
+
7
+ from exactdocs.catalog.module import CatalogModule
8
+
9
+ __all__ = ["CatalogModule"]
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ from collections.abc import Iterable, Sequence
2
+ from dataclasses import dataclass, field
3
+ from typing import Self
4
+
5
+ from exactdocs.catalog.models import Library, LibraryName, Version
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class InMemoryLibraryRepository:
10
+ """Implements ``ports.driven.LibraryRepository``. For tests and the zero-config demo."""
11
+
12
+ libraries: dict[LibraryName, Library] = field(default_factory=dict)
13
+
14
+ @classmethod
15
+ def of(cls, libraries: Iterable[Library]) -> Self:
16
+ """Create a repository pre-filled with ``libraries``."""
17
+ return cls({lib.name: lib for lib in libraries})
18
+
19
+ @classmethod
20
+ def demo(cls) -> Self:
21
+ """Create a small fixed catalog, so a fresh install has something to show."""
22
+ return cls.of(
23
+ [
24
+ Library(name=LibraryName("fastapi"), versions=frozenset({Version("0.115")})),
25
+ Library(name=LibraryName("react"), versions=frozenset({Version("19.1")})),
26
+ ]
27
+ )
28
+
29
+ async def list_all(self) -> Sequence[Library]:
30
+ """Return every library."""
31
+ return tuple(self.libraries.values())
32
+
33
+ async def get(self, name: LibraryName) -> Library | None:
34
+ """Return the library with this exact name, or ``None``."""
35
+ return self.libraries.get(name)
File without changes
@@ -0,0 +1,21 @@
1
+ """REST adapter for the catalog (consumed by the web UI)."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from exactdocs.catalog.dtos import GetLibraryQuery, LibraryList, LibrarySummary
6
+ from exactdocs.catalog.ports.driving import GetLibrary, ListLibraries
7
+
8
+
9
+ def build_router(*, list_libraries: ListLibraries, get_library: GetLibrary) -> APIRouter:
10
+ """Expose the catalog's driving ports under ``/libraries``."""
11
+ router = APIRouter(prefix="/libraries", tags=["catalog"])
12
+
13
+ @router.get("")
14
+ async def list_all() -> LibraryList:
15
+ return await list_libraries()
16
+
17
+ @router.get("/{name}")
18
+ async def get_one(name: str) -> LibrarySummary:
19
+ return await get_library(GetLibraryQuery(name=name))
20
+
21
+ return router
@@ -0,0 +1,18 @@
1
+ """MCP adapter for the catalog. All tools are read-only."""
2
+
3
+ from fastmcp import FastMCP
4
+ from mcp.types import ToolAnnotations
5
+
6
+ from exactdocs.catalog.dtos import LibraryList
7
+ from exactdocs.catalog.ports.driving import ListLibraries
8
+
9
+ READ_ONLY = ToolAnnotations(read_only_hint=True)
10
+
11
+
12
+ def register_tools(mcp: FastMCP, *, list_libraries: ListLibraries) -> None:
13
+ """Register the catalog's MCP tools on ``mcp``."""
14
+
15
+ @mcp.tool(name="list_libraries", annotations=READ_ONLY)
16
+ async def list_libraries_tool() -> LibraryList:
17
+ """List indexed libraries and their versions."""
18
+ return await list_libraries()
@@ -0,0 +1,42 @@
1
+ """Data crossing the catalog boundary (into and out of driving ports). Immutable, validated."""
2
+
3
+ from collections.abc import Iterable
4
+ from typing import Self
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from exactdocs.catalog.models import Library
9
+
10
+
11
+ class _Dto(BaseModel):
12
+ model_config = ConfigDict(frozen=True, extra="forbid")
13
+
14
+
15
+ class LibrarySummary(_Dto):
16
+ """A library and its indexed versions, sorted ascending."""
17
+
18
+ name: str
19
+ versions: list[str]
20
+
21
+ @classmethod
22
+ def from_model(cls, library: Library) -> Self:
23
+ """Map a domain entity to its boundary representation."""
24
+ return cls(name=library.name, versions=[str(v) for v in sorted(library.versions)])
25
+
26
+
27
+ class LibraryList(_Dto):
28
+ """All libraries visible to the caller, sorted by name."""
29
+
30
+ libraries: list[LibrarySummary]
31
+
32
+ @classmethod
33
+ def from_models(cls, libraries: Iterable[Library]) -> Self:
34
+ """Map domain entities."""
35
+ ordered = sorted(libraries, key=lambda lib: lib.name)
36
+ return cls(libraries=[LibrarySummary.from_model(lib) for lib in ordered])
37
+
38
+
39
+ class GetLibraryQuery(_Dto):
40
+ """Look up one library by its exact name."""
41
+
42
+ name: str = Field(min_length=1)
@@ -0,0 +1,16 @@
1
+ from exactdocs_kernel import NotFoundError, ValidationError
2
+
3
+
4
+ class EmptyVersionError(ValidationError):
5
+ """A version label is blank."""
6
+
7
+ def __init__(self) -> None:
8
+ super().__init__("version must not be empty")
9
+
10
+
11
+ class LibraryNotFoundError(NotFoundError):
12
+ """No library with this name is in the catalog."""
13
+
14
+ def __init__(self, name: str) -> None:
15
+ super().__init__(f"library {name!r} is not in the catalog")
16
+ self.name = name
@@ -0,0 +1,34 @@
1
+ """Catalog entities and value objects. Pure Python: no frameworks, no I/O."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import NewType
5
+
6
+ from exactdocs.catalog.errors import EmptyVersionError
7
+
8
+ LibraryName = NewType("LibraryName", str)
9
+
10
+
11
+ @dataclass(frozen=True, slots=True, order=True)
12
+ class Version:
13
+ """A documentation version label exactly as published by the library (e.g. ``19.1``)."""
14
+
15
+ value: str
16
+
17
+ def __post_init__(self) -> None:
18
+ if not self.value.strip():
19
+ raise EmptyVersionError
20
+
21
+ def __str__(self) -> str:
22
+ return self.value
23
+
24
+
25
+ @dataclass(frozen=True, slots=True, kw_only=True)
26
+ class Library:
27
+ """A documented library and the versions that have an index."""
28
+
29
+ name: LibraryName
30
+ versions: frozenset[Version] = field(default_factory=frozenset)
31
+
32
+ def has_version(self, version: Version) -> bool:
33
+ """Version is a hard filter: only an exact match counts."""
34
+ return version in self.versions
@@ -0,0 +1,27 @@
1
+ """Wiring of the catalog domain: driven adapters in, driving ports out.
2
+
3
+ It does not pick adapters itself; the composition root (``exactdocs.bootstrap``) passes them in.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Self
8
+
9
+ from exactdocs.catalog.ports.driven import LibraryRepository
10
+ from exactdocs.catalog.ports.driving import GetLibrary, ListLibraries
11
+ from exactdocs.catalog.use_cases import GetLibraryUseCase, ListLibrariesUseCase
12
+
13
+
14
+ @dataclass(frozen=True, slots=True, kw_only=True)
15
+ class CatalogModule:
16
+ """The catalog's driving ports, ready to be exposed by driving adapters."""
17
+
18
+ list_libraries: ListLibraries
19
+ get_library: GetLibrary
20
+
21
+ @classmethod
22
+ def build(cls, *, repository: LibraryRepository) -> Self:
23
+ """Assemble the use cases around the given driven adapters."""
24
+ return cls(
25
+ list_libraries=ListLibrariesUseCase(repository),
26
+ get_library=GetLibraryUseCase(repository),
27
+ )
@@ -0,0 +1,4 @@
1
+ from exactdocs.catalog.ports.driven import LibraryRepository
2
+ from exactdocs.catalog.ports.driving import GetLibrary, ListLibraries
3
+
4
+ __all__ = ["GetLibrary", "LibraryRepository", "ListLibraries"]
@@ -0,0 +1,18 @@
1
+ """Driven (secondary) ports: what the catalog needs from infrastructure."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Protocol
5
+
6
+ from exactdocs.catalog.models import Library, LibraryName
7
+
8
+
9
+ class LibraryRepository(Protocol):
10
+ """Persistence for catalog entries."""
11
+
12
+ async def list_all(self) -> Sequence[Library]:
13
+ """Return every library."""
14
+ ...
15
+
16
+ async def get(self, name: LibraryName) -> Library | None:
17
+ """Return the library with this exact name, or ``None``."""
18
+ ...
@@ -0,0 +1,21 @@
1
+ """Driving (primary) ports: what the outside world may ask the catalog to do."""
2
+
3
+ from typing import Protocol
4
+
5
+ from exactdocs.catalog.dtos import GetLibraryQuery, LibraryList, LibrarySummary
6
+
7
+
8
+ class ListLibraries(Protocol):
9
+ """List every library and its indexed versions."""
10
+
11
+ async def __call__(self) -> LibraryList:
12
+ """Run the use case."""
13
+ ...
14
+
15
+
16
+ class GetLibrary(Protocol):
17
+ """Fetch one library; raises ``LibraryNotFoundError`` when it is missing."""
18
+
19
+ async def __call__(self, query: GetLibraryQuery) -> LibrarySummary:
20
+ """Run the use case."""
21
+ ...
@@ -0,0 +1,33 @@
1
+ """Catalog use cases. Each one satisfies a driving port structurally, without inheriting it."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from exactdocs.catalog.dtos import GetLibraryQuery, LibraryList, LibrarySummary
6
+ from exactdocs.catalog.errors import LibraryNotFoundError
7
+ from exactdocs.catalog.models import LibraryName
8
+ from exactdocs.catalog.ports.driven import LibraryRepository
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ListLibrariesUseCase:
13
+ """Implements ``ports.driving.ListLibraries``."""
14
+
15
+ repository: LibraryRepository
16
+
17
+ async def __call__(self) -> LibraryList:
18
+ """List every library."""
19
+ return LibraryList.from_models(await self.repository.list_all())
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class GetLibraryUseCase:
24
+ """Implements ``ports.driving.GetLibrary``."""
25
+
26
+ repository: LibraryRepository
27
+
28
+ async def __call__(self, query: GetLibraryQuery) -> LibrarySummary:
29
+ """Fetch one library by its exact name."""
30
+ library = await self.repository.get(LibraryName(query.name))
31
+ if library is None:
32
+ raise LibraryNotFoundError(query.name)
33
+ return LibrarySummary.from_model(library)
exactdocs/py.typed ADDED
File without changes
@@ -0,0 +1,8 @@
1
+ """Retrieval domain: version-filtered search that returns verbatim excerpts with sources.
2
+
3
+ Public API for the composition root (``exactdocs.bootstrap``) only.
4
+ """
5
+
6
+ from exactdocs.retrieval.module import RetrievalModule
7
+
8
+ __all__ = ["RetrievalModule"]
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ from collections.abc import Iterable, Sequence
2
+ from dataclasses import dataclass, field
3
+ from typing import Self
4
+
5
+ from exactdocs.retrieval.models import DocTarget, Excerpt, SearchText
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class InMemoryExcerptIndex:
10
+ """Implements ``ports.driven.ExcerptIndex`` with case-insensitive substring matching.
11
+
12
+ Placeholder until the SQLite (FTS5 + sqlite-vec) and pgvector adapters land.
13
+ """
14
+
15
+ excerpts: list[Excerpt] = field(default_factory=list)
16
+
17
+ @classmethod
18
+ def of(cls, excerpts: Iterable[Excerpt]) -> Self:
19
+ """Create an index pre-filled with ``excerpts``."""
20
+ return cls(list(excerpts))
21
+
22
+ async def search(self, target: DocTarget, text: SearchText, limit: int) -> Sequence[Excerpt]:
23
+ """Return excerpts of ``target`` that contain ``text``."""
24
+ needle = text.value.casefold()
25
+ hits = (e for e in self.excerpts if e.target == target and needle in e.text.casefold())
26
+ return [hit for _, hit in zip(range(limit), hits, strict=False)]
File without changes