docspack 0.1.0 → 0.2.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/README.md +148 -27
- package/dist/build.js +92 -9
- package/dist/build.js.map +1 -1
- package/dist/cli.js +38 -10
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +5 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +11 -2
- package/dist/db.js.map +1 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +47 -25
- package/dist/discovery.js.map +1 -1
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +7 -3
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +6 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +11 -3
- package/dist/search.js.map +1 -1
- package/dist/spec.d.ts +2 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +3 -1
- package/dist/spec.js.map +1 -1
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +9 -2
- package/dist/style.js.map +1 -1
- package/package.json +5 -4
- package/src/build.ts +100 -9
- package/src/cli.ts +47 -10
- package/src/db.ts +11 -2
- package/src/discovery.ts +44 -22
- package/src/doctor.ts +13 -4
- package/src/index.ts +1 -0
- package/src/search.ts +17 -3
- package/src/spec.ts +3 -1
- package/src/style.ts +12 -2
package/dist/spec.js
CHANGED
|
@@ -5,6 +5,8 @@ export const LLMS_DIR = ".llms";
|
|
|
5
5
|
export const MANIFEST_FILE = "manifest.json";
|
|
6
6
|
export const CHUNKS_DIR = "chunks";
|
|
7
7
|
export const SCHEMA_URL = "https://docspack.dev/schema/v1.json";
|
|
8
|
+
/** Prose specification of the package format. The hint on a validation failure points here. */
|
|
9
|
+
export const SPEC_URL = "https://docspack.dev/spec";
|
|
8
10
|
const CHUNK_ID = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
9
11
|
/** Official vendor packages: `@stripe/docspack`. */
|
|
10
12
|
export function isVendorPackage(name) {
|
|
@@ -47,7 +49,7 @@ export function parseManifest(raw, where) {
|
|
|
47
49
|
// The explicit annotation is what lets TypeScript treat a `fail(...)` call as unreachable-after.
|
|
48
50
|
const fail = (message) => {
|
|
49
51
|
throw new DocspackError(`${where}: ${message}`, {
|
|
50
|
-
hint: `See the package specification: ${
|
|
52
|
+
hint: `See the package specification: ${SPEC_URL}`,
|
|
51
53
|
});
|
|
52
54
|
};
|
|
53
55
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
package/dist/spec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../src/spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,+EAA+E;AAC/E,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAChC,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC;AACnC,MAAM,CAAC,MAAM,UAAU,GAAG,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../src/spec.ts"],"names":[],"mappings":"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"}
|
package/dist/style.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmCH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,CAcpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAsB,GAAG,MAAM,EAAE,CAOxF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOxD;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD;AAKD,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD"}
|
package/dist/style.js
CHANGED
|
@@ -31,6 +31,11 @@ const SENTENCE_SPLIT = /(?<=[.!?])\s+/;
|
|
|
31
31
|
/** A heading or list item starts a new unit, whatever the previous line ended with. */
|
|
32
32
|
const BLOCK_START = /\n(?=\s*(?:[-*+]\s|\d+\.\s|#{1,6}\s|\|))/;
|
|
33
33
|
const LONG_SENTENCE_WORDS = 40;
|
|
34
|
+
/**
|
|
35
|
+
* A word carries a letter or a digit. Stripped code leaves its punctuation behind, so counting
|
|
36
|
+
* everything between spaces reports an enumeration of forty code spans as a forty-word sentence.
|
|
37
|
+
*/
|
|
38
|
+
const WORD = /[\p{L}\p{N}]/u;
|
|
34
39
|
/** Filler phrases found in a document, with how often each occurs. */
|
|
35
40
|
export function findFiller(text) {
|
|
36
41
|
const prose = withoutCode(text);
|
|
@@ -56,7 +61,7 @@ export function findLongSentences(text, maxWords = LONG_SENTENCE_WORDS) {
|
|
|
56
61
|
.flatMap((block) => block.split(BLOCK_START))
|
|
57
62
|
.flatMap((block) => block.split(SENTENCE_SPLIT))
|
|
58
63
|
.map((sentence) => sentence.replace(/\s+/g, " ").trim())
|
|
59
|
-
.filter((sentence) => sentence.split(" ").filter(
|
|
64
|
+
.filter((sentence) => sentence.split(" ").filter((word) => WORD.test(word)).length > maxWords);
|
|
60
65
|
}
|
|
61
66
|
/**
|
|
62
67
|
* True when a chunk contains something concrete: a code block, an inline identifier, a list or a
|
|
@@ -77,11 +82,13 @@ export function contentFingerprint(text) {
|
|
|
77
82
|
.replace(/[^a-z0-9]+/g, " ")
|
|
78
83
|
.trim();
|
|
79
84
|
}
|
|
85
|
+
/** An authored directive, e.g. `<!-- docspack: tags=grid -->`. Metadata, not site boilerplate. */
|
|
86
|
+
const DIRECTIVE_LINE = /^\s*<!--\s*docspack:/i;
|
|
80
87
|
/** Removes lines a documentation site wraps around its content. */
|
|
81
88
|
export function stripBoilerplate(text) {
|
|
82
89
|
return text
|
|
83
90
|
.split("\n")
|
|
84
|
-
.filter((line) => !BOILERPLATE.some((pattern) => pattern.test(line)))
|
|
91
|
+
.filter((line) => DIRECTIVE_LINE.test(line) || !BOILERPLATE.some((pattern) => pattern.test(line)))
|
|
85
92
|
.join("\n");
|
|
86
93
|
}
|
|
87
94
|
/** Text with fenced and inline code removed, so prose rules never fire on a code sample. */
|
package/dist/style.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,MAAM,MAAM,GAAG;IACb,6DAA6D;IAC7D,iGAAiG;IACjG,2DAA2D;IAC3D,sBAAsB;IACtB,mEAAmE;IACnE,4DAA4D;IAC5D,+DAA+D;IAC/D,4HAA4H;IAC5H,0EAA0E;IAC1E,0BAA0B;CAC3B,CAAC;AAEF,kFAAkF;AAClF,MAAM,WAAW,GAAG;IAClB,2CAA2C,EAAE,0BAA0B;IACvE,gCAAgC,EAAE,aAAa;IAC/C,sFAAsF;IACtF,kCAAkC;IAClC,yBAAyB;CAC1B,CAAC;AAEF,MAAM,cAAc,GAAG,eAAe,CAAC;AACvC,uFAAuF;AACvF,MAAM,WAAW,GAAG,0CAA0C,CAAC;AAC/D,MAAM,mBAAmB,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docspack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local, version-locked documentation packages for AI agents, indexed in SQLite and served over MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"context",
|
|
15
15
|
"cli"
|
|
16
16
|
],
|
|
17
|
-
"homepage": "https://
|
|
17
|
+
"homepage": "https://docspack.dev",
|
|
18
18
|
"bugs": "https://github.com/docspack/docspack/issues",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"bin",
|
|
39
39
|
"dist",
|
|
40
|
+
"src",
|
|
40
41
|
"README.md",
|
|
41
|
-
"
|
|
42
|
+
"LICENSE"
|
|
42
43
|
],
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=22.5.0"
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
48
49
|
"turndown": "7.2.4",
|
|
49
50
|
"zod": "4.4.3",
|
|
50
|
-
"@docspack/registry": "^0.1.
|
|
51
|
+
"@docspack/registry": "^0.1.1"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/node": "26.2.0",
|
package/src/build.ts
CHANGED
|
@@ -408,8 +408,10 @@ function chunkDocuments(
|
|
|
408
408
|
: splitMarkdown(document.text, document.title, maxTokens);
|
|
409
409
|
|
|
410
410
|
for (const section of sections) {
|
|
411
|
-
const
|
|
412
|
-
const
|
|
411
|
+
const directives = readDirectives(section.body);
|
|
412
|
+
const body = withoutDuplicateHeading(directives.body, section.heading);
|
|
413
|
+
const id = uniqueId(chunkSlug(document.title, section.heading), taken);
|
|
414
|
+
const contents = `# ${section.heading}\n\n<!-- docspack: from ${document.origin} -->\n\n${body}\n`;
|
|
413
415
|
const file = `${CHUNKS_DIR}/${id}.md`;
|
|
414
416
|
|
|
415
417
|
files.push({ path: file, contents });
|
|
@@ -417,8 +419,20 @@ function chunkDocuments(
|
|
|
417
419
|
id,
|
|
418
420
|
file,
|
|
419
421
|
tokens: estimateTokens(contents),
|
|
420
|
-
tags:
|
|
421
|
-
|
|
422
|
+
// Directive tags first: they are the author aiming one chunk, and `uniqueWords` caps.
|
|
423
|
+
tags: uniqueWords([
|
|
424
|
+
...directives.tags,
|
|
425
|
+
document.title,
|
|
426
|
+
section.heading,
|
|
427
|
+
...(document.tags ?? []),
|
|
428
|
+
]),
|
|
429
|
+
entities: [
|
|
430
|
+
...new Set([
|
|
431
|
+
...(document.entities ?? []),
|
|
432
|
+
...directives.entities,
|
|
433
|
+
...extractEntities(body),
|
|
434
|
+
]),
|
|
435
|
+
],
|
|
422
436
|
});
|
|
423
437
|
}
|
|
424
438
|
}
|
|
@@ -508,13 +522,90 @@ function splitParagraphs(text: string, maxTokens: number): string[] {
|
|
|
508
522
|
return parts;
|
|
509
523
|
}
|
|
510
524
|
|
|
511
|
-
/**
|
|
525
|
+
/** A member access in inline code, e.g. `Stripe.setApiKey`. */
|
|
526
|
+
const DOTTED_ENTITY = /`([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)\(?\)?`/g;
|
|
527
|
+
/**
|
|
528
|
+
* An inline-code token that is an identifier on its own: `TwoColumn`, `useSlide`,
|
|
529
|
+
* `text-accent`. Component libraries name most of their surface without a dot, so requiring
|
|
530
|
+
* one made `verify` blind to them. Two words are required — a single capitalised word in a
|
|
531
|
+
* code span is as often a value as an API name.
|
|
532
|
+
*/
|
|
533
|
+
const BARE_ENTITY =
|
|
534
|
+
/`([A-Za-z][a-z0-9]*(?:[A-Z][A-Za-z0-9]*)+|[a-z][a-z0-9]*(?:-[a-z0-9]+)+)\(?\)?`/g;
|
|
535
|
+
|
|
536
|
+
const MAX_ENTITIES = 20;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Pulls the identifiers a section names out of its inline code.
|
|
540
|
+
*
|
|
541
|
+
* Member accesses and camel or Pascal names come first. A reference page often enumerates
|
|
542
|
+
* dozens of hyphenated variants — `token-0`, `token-1` — and letting those fill the cap would
|
|
543
|
+
* drop the component the page is actually about.
|
|
544
|
+
*/
|
|
512
545
|
function extractEntities(body: string): string[] {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
546
|
+
const dotted = matchAll(body, DOTTED_ENTITY);
|
|
547
|
+
const bare = matchAll(body, BARE_ENTITY);
|
|
548
|
+
const ordered = [
|
|
549
|
+
...dotted,
|
|
550
|
+
...bare.filter((name) => !name.includes("-")),
|
|
551
|
+
...bare.filter((name) => name.includes("-")),
|
|
552
|
+
];
|
|
553
|
+
return [...new Set(ordered)].slice(0, MAX_ENTITIES);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function matchAll(text: string, pattern: RegExp): string[] {
|
|
557
|
+
const found: string[] = [];
|
|
558
|
+
for (const match of text.matchAll(pattern)) {
|
|
559
|
+
if (match[1] !== undefined) found.push(match[1]);
|
|
516
560
|
}
|
|
517
|
-
return
|
|
561
|
+
return found;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** One authored override under a heading: `<!-- docspack: tags=grid,columns -->`. */
|
|
565
|
+
const DIRECTIVE = /^[ \t]*<!--[ \t]*docspack:[ \t]*(tags|entities)[ \t]*=([^>]*?)-->[ \t]*$/gim;
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Reads per-section directives and removes them from the body. Front matter belongs to a whole
|
|
569
|
+
* document, so a page of eighty sections cannot aim any one of them; this is that lever.
|
|
570
|
+
*/
|
|
571
|
+
function readDirectives(body: string): { body: string; tags: string[]; entities: string[] } {
|
|
572
|
+
const tags: string[] = [];
|
|
573
|
+
const entities: string[] = [];
|
|
574
|
+
|
|
575
|
+
const text = body.replace(DIRECTIVE, (_line, key: string, value: string) => {
|
|
576
|
+
const values = value
|
|
577
|
+
.split(",")
|
|
578
|
+
.map((item) => item.trim())
|
|
579
|
+
.filter((item) => item.length > 0);
|
|
580
|
+
(key.toLowerCase() === "tags" ? tags : entities).push(...values);
|
|
581
|
+
return "";
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
return { body: text.replace(/\n{3,}/g, "\n\n").trim(), tags, entities };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Drops a source `# Title` that repeats the heading being written above it. A single-`H1`
|
|
589
|
+
* document has no `##` to split at, so its heading falls back to the title and the body would
|
|
590
|
+
* otherwise open with the same line twice.
|
|
591
|
+
*/
|
|
592
|
+
function withoutDuplicateHeading(body: string, heading: string): string {
|
|
593
|
+
const [first = "", ...rest] = body.split("\n");
|
|
594
|
+
const match = /^#\s+(.+?)\s*$/.exec(first.trim());
|
|
595
|
+
if (match?.[1] === undefined) return body;
|
|
596
|
+
if (match[1].trim().toLowerCase() !== heading.trim().toLowerCase()) return body;
|
|
597
|
+
return rest.join("\n").trim();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The chunk id for a section. When the section heading is the document title — every
|
|
602
|
+
* single-`H1` page — concatenating the two stutters: `colours-colours`.
|
|
603
|
+
*/
|
|
604
|
+
function chunkSlug(title: string, heading: string): string {
|
|
605
|
+
const titleSlug = slugify(title, "chunk");
|
|
606
|
+
return slugify(heading, "chunk") === titleSlug
|
|
607
|
+
? titleSlug
|
|
608
|
+
: slugify(`${title}-${heading}`, "chunk");
|
|
518
609
|
}
|
|
519
610
|
|
|
520
611
|
/** Reduces a heading to a chunk id: lowercase, hyphen-separated, no path separators. */
|
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,13 @@ import { type DoctorReport, runDoctor } from "./doctor.js";
|
|
|
9
9
|
import { DocspackError } from "./errors.js";
|
|
10
10
|
import { renderTree } from "./init/write.js";
|
|
11
11
|
import { previewPackage } from "./preview.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_LIMIT,
|
|
14
|
+
DEFAULT_MAX_TOKENS,
|
|
15
|
+
type QueryResult,
|
|
16
|
+
queryDocs,
|
|
17
|
+
renderAnswer,
|
|
18
|
+
} from "./search.js";
|
|
13
19
|
import { AGENTS_SNIPPET, FEEDBACK_SNIPPET } from "./snippet.js";
|
|
14
20
|
import { syncProject } from "./sync.js";
|
|
15
21
|
import { verifyProject } from "./verify.js";
|
|
@@ -46,6 +52,7 @@ const OPTIONS = {
|
|
|
46
52
|
yes: { type: "boolean", short: "y" },
|
|
47
53
|
"dry-run": { type: "boolean" },
|
|
48
54
|
strict: { type: "boolean" },
|
|
55
|
+
pedantic: { type: "boolean" },
|
|
49
56
|
"package-dir": { type: "string" },
|
|
50
57
|
chunk: { type: "string" },
|
|
51
58
|
kind: { type: "string" },
|
|
@@ -55,6 +62,14 @@ const OPTIONS = {
|
|
|
55
62
|
repro: { type: "string" },
|
|
56
63
|
} as const;
|
|
57
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A query that returned nothing is not a failure, but "you have not indexed anything yet" and
|
|
67
|
+
* "the documentation does not cover this" are different answers and a wrapper has to tell them
|
|
68
|
+
* apart. Both are non-zero; neither collides with 1 (failed) or 2 (used wrongly).
|
|
69
|
+
*/
|
|
70
|
+
const EXIT_NOT_INDEXED = 3;
|
|
71
|
+
const EXIT_NO_MATCH = 4;
|
|
72
|
+
|
|
58
73
|
const HELP = `docspack — local, version-locked documentation for AI agents
|
|
59
74
|
|
|
60
75
|
Usage
|
|
@@ -134,7 +149,8 @@ Options
|
|
|
134
149
|
--no-workflow init: skip the release workflow
|
|
135
150
|
--no-build init: scaffold only
|
|
136
151
|
--template <t> init: full (default) or minimal
|
|
137
|
-
--strict doctor: treat warnings as failures
|
|
152
|
+
--strict doctor: treat structural warnings as failures
|
|
153
|
+
--pedantic doctor: --strict, and fail on prose style as well
|
|
138
154
|
--package-dir <d> doctor, preview, verify: the package to inspect (default: .)
|
|
139
155
|
--chunk <id> feedback add: the chunk the problem is in
|
|
140
156
|
--kind <k> feedback add: drift, incorrect or missing
|
|
@@ -143,10 +159,11 @@ Options
|
|
|
143
159
|
--actual <text> feedback add: what happened instead
|
|
144
160
|
--repro <code> feedback add: code that demonstrates it
|
|
145
161
|
--force sync: re-index packages already in the store; init: overwrite files
|
|
146
|
-
-p, --package <s> search: only packages whose name contains this text
|
|
147
|
-
--limit <n> search: maximum chunks to return (default ${DEFAULT_LIMIT})
|
|
148
|
-
--max-tokens <n> search: token ceiling for the result set
|
|
149
|
-
|
|
162
|
+
-p, --package <s> ask, search: only packages whose name contains this text
|
|
163
|
+
--limit <n> ask, search, preview: maximum chunks to return (default ${DEFAULT_LIMIT})
|
|
164
|
+
--max-tokens <n> ask, search, preview: token ceiling for the result set
|
|
165
|
+
(default ${DEFAULT_MAX_TOKENS})
|
|
166
|
+
--all ask, search: the whole store, not just this project;
|
|
150
167
|
feedback remove: every recorded finding
|
|
151
168
|
--from <dir> build: directory of Markdown to package
|
|
152
169
|
--openapi <file> build: OpenAPI JSON to package, one chunk per operation
|
|
@@ -161,6 +178,15 @@ Options
|
|
|
161
178
|
-h, --help Show this help
|
|
162
179
|
-v, --version Show the version
|
|
163
180
|
|
|
181
|
+
Exit codes
|
|
182
|
+
\`ask\` and \`search\` answer with an exit code a script can branch on:
|
|
183
|
+
|
|
184
|
+
0 chunks were returned
|
|
185
|
+
${EXIT_NOT_INDEXED} nothing matched, and a docs package is installed but not indexed —
|
|
186
|
+
run \`docspack sync\`
|
|
187
|
+
${EXIT_NO_MATCH} nothing matched, and everything installed is already indexed
|
|
188
|
+
1 the command failed; 2 the command was used wrongly
|
|
189
|
+
|
|
164
190
|
Examples
|
|
165
191
|
npx docspack sync
|
|
166
192
|
npx docspack ask "how do I verify a webhook signature"
|
|
@@ -239,6 +265,12 @@ function reportDoctor(report: DoctorReport, quiet: boolean): void {
|
|
|
239
265
|
}
|
|
240
266
|
}
|
|
241
267
|
|
|
268
|
+
/** 0 when the query was answered; otherwise which of the two empty answers this was. */
|
|
269
|
+
function queryExit(result: QueryResult): number {
|
|
270
|
+
if (result.hits.length > 0) return 0;
|
|
271
|
+
return result.unindexed.length > 0 ? EXIT_NOT_INDEXED : EXIT_NO_MATCH;
|
|
272
|
+
}
|
|
273
|
+
|
|
242
274
|
function openStore(values: Values): Store {
|
|
243
275
|
return Store.open(values.store ?? defaultStorePath());
|
|
244
276
|
}
|
|
@@ -358,8 +390,7 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
358
390
|
// Exactly what the MCP tool returns, so both interfaces answer identically.
|
|
359
391
|
process.stdout.write(`${renderAnswer(result, question)}\n`);
|
|
360
392
|
}
|
|
361
|
-
|
|
362
|
-
return 0;
|
|
393
|
+
return queryExit(result);
|
|
363
394
|
} finally {
|
|
364
395
|
store.close();
|
|
365
396
|
}
|
|
@@ -389,11 +420,16 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
389
420
|
|
|
390
421
|
if (json) {
|
|
391
422
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
392
|
-
return
|
|
423
|
+
return queryExit(result);
|
|
393
424
|
}
|
|
394
425
|
if (result.hits.length === 0) {
|
|
395
426
|
process.stdout.write(`No local documentation matched "${query}".\n`);
|
|
396
|
-
|
|
427
|
+
if (result.unindexed.length > 0) {
|
|
428
|
+
process.stderr.write(
|
|
429
|
+
`${yellow("!")} ${result.unindexed.join(", ")} installed but not indexed. Run \`docspack sync\`.\n`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return queryExit(result);
|
|
397
433
|
}
|
|
398
434
|
|
|
399
435
|
for (const hit of result.hits) {
|
|
@@ -793,6 +829,7 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
793
829
|
const report = await runDoctor({
|
|
794
830
|
dir,
|
|
795
831
|
...(values.strict === true ? { strict: true } : {}),
|
|
832
|
+
...(values.pedantic === true ? { pedantic: true } : {}),
|
|
796
833
|
});
|
|
797
834
|
|
|
798
835
|
if (json) {
|
package/src/db.ts
CHANGED
|
@@ -101,6 +101,11 @@ export function defaultStorePath(env: NodeJS.ProcessEnv = process.env): string {
|
|
|
101
101
|
/**
|
|
102
102
|
* Turns free text into an FTS5 MATCH expression. Every term is quoted, so punctuation in a user
|
|
103
103
|
* query can never be interpreted as FTS syntax.
|
|
104
|
+
*
|
|
105
|
+
* Terms of one or two characters are dropped. Questions are mostly `how`, `do`, `i`, `in`, and
|
|
106
|
+
* every chunk containing one becomes a candidate — which is how a chunk that merely quotes an
|
|
107
|
+
* example question outranks the chunk that answers it. Two characters is the cut because it
|
|
108
|
+
* leaves real short names (`id`, `db`) out and keeps three-letter ones (`api`, `key`) in.
|
|
104
109
|
*/
|
|
105
110
|
export function toFtsQuery(raw: string): string {
|
|
106
111
|
const terms = raw.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
@@ -109,7 +114,9 @@ export function toFtsQuery(raw: string): string {
|
|
|
109
114
|
hint: "Search for words, for example: docspack search webhook signature",
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
|
-
|
|
117
|
+
const meaningful = terms.filter((term) => term.length > 2);
|
|
118
|
+
// A query made only of short terms still has to search for something.
|
|
119
|
+
return (meaningful.length > 0 ? meaningful : terms).map((term) => `"${term}"`).join(" OR ");
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
/** The global chunk index: one SQLite database shared by every project on the machine. */
|
|
@@ -223,7 +230,9 @@ export class Store {
|
|
|
223
230
|
FROM chunks_fts f
|
|
224
231
|
JOIN chunks c ON c.chunk_id = f.chunk_id
|
|
225
232
|
WHERE ${conditions.join(" AND ")}
|
|
226
|
-
|
|
233
|
+
-- Tags are what an author writes to aim a chunk, and front matter did nothing at all
|
|
234
|
+
-- while they scored the same as prose. Weights are (content, tags).
|
|
235
|
+
ORDER BY bm25(chunks_fts, 1.0, 3.0)
|
|
227
236
|
LIMIT ?`,
|
|
228
237
|
)
|
|
229
238
|
.all(...parameters, limit)
|
package/src/discovery.ts
CHANGED
|
@@ -96,33 +96,55 @@ export async function projectPackageIds(cwd: string): Promise<string[]> {
|
|
|
96
96
|
return packages.map((pkg) => pkg.id);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Every docs package declared by this directory or by an ancestor of it. A workspace declares
|
|
101
|
+
* shared tooling in the repository root and its members inherit the install, so reading only
|
|
102
|
+
* `cwd/package.json` answers "this project has no documentation" for most monorepos — the same
|
|
103
|
+
* upward walk `resolvePackageDir` already does for the install.
|
|
104
|
+
*/
|
|
99
105
|
async function declaredDocsPackages(cwd: string): Promise<string[]> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const names = new Set<string>();
|
|
107
|
+
let found = false;
|
|
108
|
+
let dir = cwd;
|
|
109
|
+
|
|
110
|
+
while (true) {
|
|
111
|
+
const path = join(dir, "package.json");
|
|
112
|
+
let raw: string;
|
|
113
|
+
try {
|
|
114
|
+
raw = await readFile(path, "utf8");
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
117
|
+
const parent = dirname(dir);
|
|
118
|
+
if (parent === dir) break;
|
|
119
|
+
dir = parent;
|
|
120
|
+
continue;
|
|
108
121
|
}
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
found = true;
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(raw);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new DocspackError(`${path} is not valid JSON`, { cause: error });
|
|
129
|
+
}
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
const manifest = parsed as { dependencies?: unknown; devDependencies?: unknown };
|
|
132
|
+
for (const field of [manifest.dependencies, manifest.devDependencies]) {
|
|
133
|
+
if (typeof field !== "object" || field === null) continue;
|
|
134
|
+
for (const name of Object.keys(field as Record<string, unknown>)) {
|
|
135
|
+
if (isDocsPackage(name)) names.add(name);
|
|
136
|
+
}
|
|
125
137
|
}
|
|
138
|
+
|
|
139
|
+
const parent = dirname(dir);
|
|
140
|
+
if (parent === dir) break;
|
|
141
|
+
dir = parent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!found) {
|
|
145
|
+
throw new DocspackError(`No package.json found in ${cwd}`, {
|
|
146
|
+
hint: "Run docspack from a project directory, or pass --cwd <dir>.",
|
|
147
|
+
});
|
|
126
148
|
}
|
|
127
149
|
return [...names].sort();
|
|
128
150
|
}
|
package/src/doctor.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface DoctorOptions {
|
|
|
40
40
|
readonly dir: string;
|
|
41
41
|
/** Treat warnings as failures. Used by prepublishOnly and CI. */
|
|
42
42
|
readonly strict?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Also fail on prose style — filler and long sentences. Implies `strict`. Off by default
|
|
45
|
+
* because `--strict` is the gate `init` scaffolds, and prose written by a human is not a
|
|
46
|
+
* reason to block a first publish.
|
|
47
|
+
*/
|
|
48
|
+
readonly pedantic?: boolean;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/** Over this, a chunk crowds out the response budget; under it, a chunk answers nothing. */
|
|
@@ -53,6 +59,9 @@ const MIN_CHUNK_TOKENS = 30;
|
|
|
53
59
|
export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
54
60
|
const findings: Finding[] = [];
|
|
55
61
|
const llmsDir = join(options.dir, LLMS_DIR);
|
|
62
|
+
// Structural checks describe a package that will not retrieve properly; the two prose checks
|
|
63
|
+
// are opinions about writing. Only the first kind may block a publish by default.
|
|
64
|
+
const prose: Severity = options.pedantic === true ? "warn" : "info";
|
|
56
65
|
|
|
57
66
|
const pkg = await readJson(join(options.dir, "package.json"));
|
|
58
67
|
const manifest = await loadManifest(llmsDir, findings);
|
|
@@ -145,7 +154,7 @@ export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
|
145
154
|
.join(", ");
|
|
146
155
|
findings.push({
|
|
147
156
|
check: "filler",
|
|
148
|
-
severity:
|
|
157
|
+
severity: prose,
|
|
149
158
|
message: `chunk "${chunk.id}" contains narration a model does not need: ${listed}`,
|
|
150
159
|
where: chunk.file,
|
|
151
160
|
fix: "Delete it. Nobody searches for these words and they carry no fact.",
|
|
@@ -156,7 +165,7 @@ export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
|
156
165
|
if (long.length > 0) {
|
|
157
166
|
findings.push({
|
|
158
167
|
check: "long-sentence",
|
|
159
|
-
severity:
|
|
168
|
+
severity: prose,
|
|
160
169
|
message: `chunk "${chunk.id}" has ${plural(long.length, "sentence")} over 40 words`,
|
|
161
170
|
where: chunk.file,
|
|
162
171
|
fix: "Split them. One claim per sentence retrieves and reads better.",
|
|
@@ -209,9 +218,9 @@ export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
|
209
218
|
});
|
|
210
219
|
}
|
|
211
220
|
|
|
221
|
+
const strict = options.strict === true || options.pedantic === true;
|
|
212
222
|
const failed = findings.some(
|
|
213
|
-
(finding) =>
|
|
214
|
-
finding.severity === "error" || (options.strict === true && finding.severity === "warn"),
|
|
223
|
+
(finding) => finding.severity === "error" || (strict && finding.severity === "warn"),
|
|
215
224
|
);
|
|
216
225
|
|
|
217
226
|
return { ok: !failed, findings, chunks: manifest.chunks.length, tokens };
|
package/src/index.ts
CHANGED
package/src/search.ts
CHANGED
|
@@ -32,13 +32,21 @@ export interface QueryResult {
|
|
|
32
32
|
readonly tokens: number;
|
|
33
33
|
/** True when any hit came from an unvetted community package. */
|
|
34
34
|
readonly untrusted: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Packages this project depends on that are installed but absent from the store. Nothing they
|
|
37
|
+
* document can match until `docspack sync` runs, which is a different answer from "nothing
|
|
38
|
+
* covers this question".
|
|
39
|
+
*/
|
|
40
|
+
readonly unindexed: readonly string[];
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
38
44
|
const scoped = options.scoped !== false;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const installed = scoped ? (await discoverPackages(options.cwd)).packages : undefined;
|
|
46
|
+
const packageIds = installed?.map((pkg) => pkg.id);
|
|
47
|
+
const unindexed = (installed ?? [])
|
|
48
|
+
.filter((pkg) => !options.store.hasPackage(pkg.id))
|
|
49
|
+
.map((pkg) => pkg.id);
|
|
42
50
|
|
|
43
51
|
const hits = options.store
|
|
44
52
|
.search(options.query, {
|
|
@@ -58,6 +66,7 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
58
66
|
hits,
|
|
59
67
|
tokens: hits.reduce((total, hit) => total + hit.tokens, 0),
|
|
60
68
|
untrusted: hits.some((hit) => !hit.trusted),
|
|
69
|
+
unindexed,
|
|
61
70
|
};
|
|
62
71
|
}
|
|
63
72
|
|
|
@@ -71,6 +80,11 @@ const UNTRUSTED_NOTICE =
|
|
|
71
80
|
*/
|
|
72
81
|
export function renderAnswer(result: QueryResult, query: string): string {
|
|
73
82
|
if (result.hits.length === 0) {
|
|
83
|
+
// Forgetting `sync` is the likeliest first mistake, and the tool can tell it apart from a
|
|
84
|
+
// question nothing covers: it can see what is installed and what is in the store.
|
|
85
|
+
if (result.unindexed.length > 0) {
|
|
86
|
+
return `No local documentation matched "${query}", and ${result.unindexed.join(", ")} ${result.unindexed.length === 1 ? "is" : "are"} installed but not indexed. Run \`docspack sync\`, then ask again.`;
|
|
87
|
+
}
|
|
74
88
|
return `No local documentation matched "${query}". The project may not depend on a docspack package covering it.`;
|
|
75
89
|
}
|
|
76
90
|
|
package/src/spec.ts
CHANGED
|
@@ -6,6 +6,8 @@ export const LLMS_DIR = ".llms";
|
|
|
6
6
|
export const MANIFEST_FILE = "manifest.json";
|
|
7
7
|
export const CHUNKS_DIR = "chunks";
|
|
8
8
|
export const SCHEMA_URL = "https://docspack.dev/schema/v1.json";
|
|
9
|
+
/** Prose specification of the package format. The hint on a validation failure points here. */
|
|
10
|
+
export const SPEC_URL = "https://docspack.dev/spec";
|
|
9
11
|
|
|
10
12
|
/** One retrievable unit of documentation. */
|
|
11
13
|
export interface ChunkSpec {
|
|
@@ -73,7 +75,7 @@ export function parseManifest(raw: unknown, where: string): PackageManifest {
|
|
|
73
75
|
// The explicit annotation is what lets TypeScript treat a `fail(...)` call as unreachable-after.
|
|
74
76
|
const fail: (message: string) => never = (message: string): never => {
|
|
75
77
|
throw new DocspackError(`${where}: ${message}`, {
|
|
76
|
-
hint: `See the package specification: ${
|
|
78
|
+
hint: `See the package specification: ${SPEC_URL}`,
|
|
77
79
|
});
|
|
78
80
|
};
|
|
79
81
|
|