@any-nav/scan 0.0.0-stage → 0.1.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/description.d.ts +33 -0
- package/dist/description.js +273 -0
- package/dist/description.js.map +1 -0
- package/dist/gitignore.d.ts +9 -0
- package/dist/gitignore.js +128 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/scan.d.ts +82 -0
- package/dist/scan.js +603 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +24 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-21
|
|
4
|
+
|
|
5
|
+
First registry release: the zero-dependency scan engine, split out of
|
|
6
|
+
`@any-nav/core` (2026-10-19) as the lean consumption face for embedders.
|
|
7
|
+
|
|
8
|
+
- `scanSource()` walks a Markdown folder into a `NavNode` tree under the
|
|
9
|
+
routes traversal semantics: additive exclusion lists over an always-on
|
|
10
|
+
built-in floor, `.gitignore` inheritance, a loud `maxFiles` budget
|
|
11
|
+
(diagnostic, never a silent cut), empty-directory pruning, and folder
|
|
12
|
+
descriptions via the `_category_` sidecar → README chain.
|
|
13
|
+
- Budget cuts are disclosed as lower bounds: folders cut by the cap carry
|
|
14
|
+
floor counts (`countIncompletePaths` on the output; views render ≥N
|
|
15
|
+
with a per-row `count-truncated-at-limit` diagnostic) — surviving rows
|
|
16
|
+
never overstate completeness.
|
|
17
|
+
- `scanStructure()` runs the identical walk with zero content reads: file
|
|
18
|
+
nodes carry no `NavItem` until `describe(paths)` hydrates exactly the
|
|
19
|
+
rendered rows in place; bounded-head reads keep extraction identical to
|
|
20
|
+
a full read.
|
|
21
|
+
- Walk throughput: overlapped directory reads and memoized `.gitignore`
|
|
22
|
+
matching.
|
|
23
|
+
- Description extraction: frontmatter `description:` → document-head
|
|
24
|
+
`description:` line → first substantive body line → placeholder +
|
|
25
|
+
`no-description` diagnostic.
|
|
26
|
+
- Terminal `ResolvedScanOptions` with `resolveScanOptions` as the additive
|
|
27
|
+
default floor; `builtinDescriptionProvider` as the default
|
|
28
|
+
`DescriptionProvider` port implementation.
|
|
29
|
+
- The knowledge model: `NavItem { name, description, locator }` plus
|
|
30
|
+
`NavNode`, `ScanConfig`, and audience-tagged `ScanDiagnostic`s.
|
|
31
|
+
- Zero npm dependencies — the isolated install closure is this package alone.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CatheadOwl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @any-nav/scan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Walk any Markdown folder into a navigation tree. Zero npm dependencies.
|
|
4
|
+
|
|
5
|
+
`@any-nav/scan` is the lean scan engine behind Any-Nav: it reads a folder of
|
|
6
|
+
Markdown files and produces a navigation tree — names, one-line descriptions,
|
|
7
|
+
and file locators — ready for listings, indexes, and agent navigation
|
|
8
|
+
surfaces. This package exists so embedders who only need the engine carry
|
|
9
|
+
nothing else: no TOML parser, no search index, no SDK. The full SDK (config,
|
|
10
|
+
budget guard, rendering, query) lives in `@any-nav/core`, which composes and
|
|
11
|
+
re-exports this package's full face.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @any-nav/scan
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { builtinDescriptionProvider, resolveScanOptions, scanSource } from '@any-nav/scan';
|
|
23
|
+
|
|
24
|
+
const output = await scanSource(
|
|
25
|
+
'docs', // the folder to walk (absolute or relative)
|
|
26
|
+
builtinDescriptionProvider, // description chain; this is the default, shown for clarity
|
|
27
|
+
resolveScanOptions({ maxFiles: 2000, respectGitignore: true }),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
output.tree; // NavNode tree — directories are nodes, files carry a NavItem
|
|
31
|
+
output.itemCount; // number of Markdown items read
|
|
32
|
+
output.diagnostics; // loud notes (budget hit, no-description files, …) — never a silent cut
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The knowledge model is one shape: `NavItem { name, description, locator }`
|
|
36
|
+
(`name` defaults to the file stem; `locator.path` is POSIX-relative to the
|
|
37
|
+
scan root). There is no second model — the SDK's view types build on it, never
|
|
38
|
+
beside it.
|
|
39
|
+
|
|
40
|
+
### Structure-only walks (lazy descriptions)
|
|
41
|
+
|
|
42
|
+
When a consumer renders a slice of the tree (a depth-0 listing shows only
|
|
43
|
+
its own rows), reading every file up front is wasted work. `scanStructure`
|
|
44
|
+
runs the identical walk — exclusions, gitignore, budget, pruning — with
|
|
45
|
+
zero content reads; file nodes carry no `NavItem` until `describe(paths)`
|
|
46
|
+
hydrates exactly the ones a view renders, in place:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { resolveScanOptions, scanStructure } from '@any-nav/scan';
|
|
50
|
+
|
|
51
|
+
const structure = await scanStructure('docs', undefined, resolveScanOptions());
|
|
52
|
+
structure.tree; // same shape a full scan produces; file items absent
|
|
53
|
+
structure.itemCount; // identical to a full scan (the budget counts entries)
|
|
54
|
+
structure.diagnostics; // walk findings; per-file diagnostics ride describe()
|
|
55
|
+
const perFile = await structure.describe(new Set(['README.md', 'guides/README.md']));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Reads fetch a bounded head that provably covers the description chain's
|
|
59
|
+
input (frontmatter plus the body-head window, extended adaptively when a
|
|
60
|
+
head cannot prove stability) — extraction results are identical to a full
|
|
61
|
+
read, at a fraction of the bytes.
|
|
62
|
+
|
|
63
|
+
## Scan semantics
|
|
64
|
+
|
|
65
|
+
- **Additive exclusions** — `excludeDirs` / `excludeFiles` append to an
|
|
66
|
+
always-on built-in floor (e.g. `AGENTS.md`, `CLAUDE.md`, `node_modules`,
|
|
67
|
+
`dist`); stating one never drops the defaults. Dot entries are skipped by
|
|
68
|
+
default. Empty directories are pruned from the tree.
|
|
69
|
+
- **gitignore inheritance** — `respectGitignore` (default on): traversal
|
|
70
|
+
honors `.gitignore` rules from the scan root down.
|
|
71
|
+
- **A loud budget** — `maxFiles` (default 2000) caps markdown entries per
|
|
72
|
+
source walk; hitting the cap fires diagnostics, never a silent truncation:
|
|
73
|
+
folders whose descent was cut carry floor counts (`countIncompletePaths`
|
|
74
|
+
on the output; views render ≥N with a per-row `count-truncated-at-limit`
|
|
75
|
+
diagnostic) and later folders are absent. The budget counts tree entries
|
|
76
|
+
(identical cut for full and structure walks).
|
|
77
|
+
- **Description source chain** — per file, in priority order: frontmatter
|
|
78
|
+
`description:`, else an explicit `description:` line in the document head,
|
|
79
|
+
else the first substantive body line; none matching falls to a placeholder
|
|
80
|
+
plus a `no-description` diagnostic (full scans emit one per file; structure
|
|
81
|
+
walks emit them per hydrated file, so a lazy view warns about the rows it
|
|
82
|
+
renders).
|
|
83
|
+
- **Folder descriptions** — a directory describes itself via its
|
|
84
|
+
`_category_.yml` / `.yaml` / `.json` sidecar's `description` key, else via
|
|
85
|
+
the same extraction over its own `README.md`.
|
|
86
|
+
- **Read-only** — scanning never modifies source files. The map is not the
|
|
87
|
+
territory.
|
|
88
|
+
|
|
89
|
+
## Boundaries this package keeps
|
|
90
|
+
|
|
91
|
+
- Zero runtime dependencies beyond Node builtins — the install closure is
|
|
92
|
+
this package alone.
|
|
93
|
+
- `ResolvedScanOptions` is terminal: callers hand the scanner final values;
|
|
94
|
+
`resolveScanOptions` is an additive default floor, not an obligation.
|
|
95
|
+
- `DescriptionProvider` is a port — the built-in chain is the default; a
|
|
96
|
+
custom provider (e.g. an LLM-backed one) plugs into the same interface.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-line description extraction, ported verbatim from the routes module
|
|
3
|
+
* (dsh-plugin-dev/extras/modules/routes/src/description.ts, ADR-0010:
|
|
4
|
+
* absorption is porting — every boundary handling here is tuition already
|
|
5
|
+
* paid on production corpora; do not "simplify" it).
|
|
6
|
+
*
|
|
7
|
+
* Priority: ① frontmatter `description:`, ② an explicit `description:` line
|
|
8
|
+
* in the document head, ③ the first substantive body line as a fallback.
|
|
9
|
+
* Returns null when none match.
|
|
10
|
+
*/
|
|
11
|
+
/** Body-line description lookup only trusts the document head, so a `description:` inside a code block or deep in the prose cannot hijack the route. Exported for the scan-side head-read sufficiency check (scan.ts), which must stay in lockstep with this window. */
|
|
12
|
+
export declare const BODY_HEAD_LINE_LIMIT = 15;
|
|
13
|
+
/**
|
|
14
|
+
* Extracts a one-line route description from Markdown, in priority order:
|
|
15
|
+
* ① frontmatter `description:`, ② an explicit `description:` line in the document
|
|
16
|
+
* head, ③ the first substantive body line as a fallback. Returns `null` when none match.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractDescription(content: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Directory sidecar read (2026-10-07): `_category_.yml/.yaml/.json` carry a
|
|
21
|
+
* folder's description so release-face READMEs stay untouched. YAML reuses
|
|
22
|
+
* the frontmatter ① key reader — the sidecar is frontmatter without the
|
|
23
|
+
* document (an indented `link.description:` therefore reads too, covering
|
|
24
|
+
* Docusaurus-shaped files for free); JSON parses properly and accepts the
|
|
25
|
+
* top-level `description` or the Docusaurus `link.description` shape.
|
|
26
|
+
* `{ description: null }` means the file carries no description key.
|
|
27
|
+
*/
|
|
28
|
+
export type SidecarRead = {
|
|
29
|
+
description: string | null;
|
|
30
|
+
} | {
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function extractSidecarDescription(fileName: string, content: string): SidecarRead;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-line description extraction, ported verbatim from the routes module
|
|
3
|
+
* (dsh-plugin-dev/extras/modules/routes/src/description.ts, ADR-0010:
|
|
4
|
+
* absorption is porting — every boundary handling here is tuition already
|
|
5
|
+
* paid on production corpora; do not "simplify" it).
|
|
6
|
+
*
|
|
7
|
+
* Priority: ① frontmatter `description:`, ② an explicit `description:` line
|
|
8
|
+
* in the document head, ③ the first substantive body line as a fallback.
|
|
9
|
+
* Returns null when none match.
|
|
10
|
+
*/
|
|
11
|
+
/** Body-line description lookup only trusts the document head, so a `description:` inside a code block or deep in the prose cannot hijack the route. Exported for the scan-side head-read sufficiency check (scan.ts), which must stay in lockstep with this window. */
|
|
12
|
+
export const BODY_HEAD_LINE_LIMIT = 15;
|
|
13
|
+
/** Minimum code-point length for a body line to count as a substantive first sentence (fallback ③). Word count is avoided because CJK text has no space tokenization. */
|
|
14
|
+
const MIN_SUBSTANTIVE_LENGTH = 20;
|
|
15
|
+
/** Approximate cap (code points) for a fallback description; longer lines are cut at the last sentence boundary inside the cap. */
|
|
16
|
+
const MAX_DESCRIPTION_CHARS = 120;
|
|
17
|
+
const SENTENCE_ENDERS = ['.', '。', '!', '!', '?', '?', ';', ';'];
|
|
18
|
+
/** Link or image, tolerating one level of nesting so a badge (`[](href)`) counts as a single token. */
|
|
19
|
+
const LINK_OR_IMAGE = /!?\[(?:[^[\]]|\[[^\]]*\])*\]\([^)]*\)/gu;
|
|
20
|
+
/**
|
|
21
|
+
* Extracts a one-line route description from Markdown, in priority order:
|
|
22
|
+
* ① frontmatter `description:`, ② an explicit `description:` line in the document
|
|
23
|
+
* head, ③ the first substantive body line as a fallback. Returns `null` when none match.
|
|
24
|
+
*/
|
|
25
|
+
export function extractDescription(content) {
|
|
26
|
+
const frontmatter = parseYamlLikeFrontmatter(content);
|
|
27
|
+
const frontmatterDescription = frontmatter
|
|
28
|
+
? readDescriptionKey(frontmatter.split(/\r?\n/u))
|
|
29
|
+
: null;
|
|
30
|
+
if (frontmatterDescription)
|
|
31
|
+
return frontmatterDescription;
|
|
32
|
+
const body = frontmatter ? contentAfterFrontmatter(content) : content;
|
|
33
|
+
const openBodyLines = stripHtmlComments(stripCodeFences(body.split(/\r?\n/u))).slice(0, BODY_HEAD_LINE_LIMIT);
|
|
34
|
+
const bodyDescription = readDescriptionKey(openBodyLines);
|
|
35
|
+
if (bodyDescription)
|
|
36
|
+
return bodyDescription;
|
|
37
|
+
return firstSubstantiveLine(openBodyLines);
|
|
38
|
+
}
|
|
39
|
+
/** Fallback ③: the first body line that is prose (not Markdown structure) and long enough, cleaned of inline Markdown and capped. */
|
|
40
|
+
function firstSubstantiveLine(lines) {
|
|
41
|
+
for (let index = 0; index < lines.length; index++) {
|
|
42
|
+
const line = lines[index];
|
|
43
|
+
if (isStructuralLine(line) || isSetextTitle(lines, index))
|
|
44
|
+
continue;
|
|
45
|
+
const cleaned = cleanInlineMarkdown(line);
|
|
46
|
+
if (codePointLength(cleaned) < MIN_SUBSTANTIVE_LENGTH)
|
|
47
|
+
continue;
|
|
48
|
+
return truncateDescription(cleaned);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** A setext heading's title line sits directly above its `===`/`---` underline; skip the title, not just the underline. */
|
|
53
|
+
function isSetextTitle(lines, index) {
|
|
54
|
+
if (index + 1 >= lines.length)
|
|
55
|
+
return false;
|
|
56
|
+
const trimmed = lines[index + 1].trim();
|
|
57
|
+
return trimmed.length > 0 && /^(={3,}|-{3,})\s*$/u.test(trimmed);
|
|
58
|
+
}
|
|
59
|
+
/** Structural Markdown lines never carry a description; they are skipped before the length test. */
|
|
60
|
+
function isStructuralLine(line) {
|
|
61
|
+
const text = line.trim();
|
|
62
|
+
if (text.length === 0)
|
|
63
|
+
return true;
|
|
64
|
+
if (/^#{1,6}(\s|$)/u.test(text))
|
|
65
|
+
return true; // ATX heading
|
|
66
|
+
if (/^(={3,}|-{3,}|\*{3,}|_{3,})$/u.test(text))
|
|
67
|
+
return true; // setext underline / horizontal rule
|
|
68
|
+
if (/^>/u.test(text))
|
|
69
|
+
return true;
|
|
70
|
+
if (/^[-+*](\s|$)/u.test(text))
|
|
71
|
+
return true; // unordered list
|
|
72
|
+
if (/^\d{1,3}\.\s/u.test(text))
|
|
73
|
+
return true; // ordered list
|
|
74
|
+
// table row / language-switch ("English | [中文]"); inline code spans are
|
|
75
|
+
// masked first — marked-up code is never table structure (ADR-0010 U8)
|
|
76
|
+
const outsideCodeSpans = text.replace(/`[^`]*`/gu, '');
|
|
77
|
+
if (outsideCodeSpans.startsWith('|') || /\s\|\s/u.test(outsideCodeSpans))
|
|
78
|
+
return true;
|
|
79
|
+
if (/^</u.test(text))
|
|
80
|
+
return true;
|
|
81
|
+
if (isLinkOnlyLine(text))
|
|
82
|
+
return true; // badge row / nav chrome
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
/** A line whose visible text is only links/images (badges, nav) carries no prose description. */
|
|
86
|
+
function isLinkOnlyLine(text) {
|
|
87
|
+
return text.length > 0 && text.replace(LINK_OR_IMAGE, '').trim().length === 0;
|
|
88
|
+
}
|
|
89
|
+
/** Light inline-Markdown cleanup so a fallback line reads as plain prose. */
|
|
90
|
+
function cleanInlineMarkdown(line) {
|
|
91
|
+
return line
|
|
92
|
+
.trim()
|
|
93
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/gu, '$1')
|
|
94
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/gu, '$1')
|
|
95
|
+
.replace(/`([^`]*)`/gu, '$1')
|
|
96
|
+
.replace(/\*\*([^*]+)\*\*/gu, '$1')
|
|
97
|
+
.replace(/__([^_]+)__/gu, '$1')
|
|
98
|
+
.replace(/\s+/g, ' ')
|
|
99
|
+
.replace(/[::]\s*$/u, '')
|
|
100
|
+
.trim();
|
|
101
|
+
}
|
|
102
|
+
/** Cut long fallback text at the last sentence boundary inside the cap; hard-cut (with a trailing `...`) when no boundary is deep enough. */
|
|
103
|
+
function truncateDescription(text) {
|
|
104
|
+
if (codePointLength(text) <= MAX_DESCRIPTION_CHARS)
|
|
105
|
+
return text;
|
|
106
|
+
const head = [...text].slice(0, MAX_DESCRIPTION_CHARS).join('');
|
|
107
|
+
const boundary = lastSentenceBoundary(head);
|
|
108
|
+
if (boundary >= 0) {
|
|
109
|
+
const cut = head.slice(0, boundary + 1).trimEnd();
|
|
110
|
+
if (codePointLength(cut) >= MIN_SUBSTANTIVE_LENGTH)
|
|
111
|
+
return cut;
|
|
112
|
+
}
|
|
113
|
+
return head.trimEnd() + '...';
|
|
114
|
+
}
|
|
115
|
+
/** Last sentence ender inside `text`, skipping abbreviation dots (`ctx.`, `e.g.`). */
|
|
116
|
+
function lastSentenceBoundary(text) {
|
|
117
|
+
for (let index = text.length - 1; index >= 0; index--) {
|
|
118
|
+
const char = text[index];
|
|
119
|
+
if (char === '.') {
|
|
120
|
+
if (isAbbreviationDot(text, index))
|
|
121
|
+
continue;
|
|
122
|
+
return index;
|
|
123
|
+
}
|
|
124
|
+
if (SENTENCE_ENDERS.includes(char))
|
|
125
|
+
return index;
|
|
126
|
+
}
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
function isAbbreviationDot(text, index) {
|
|
130
|
+
let letters = 0;
|
|
131
|
+
for (let prev = index - 1; prev >= 0 && prev >= index - 3; prev--) {
|
|
132
|
+
if (/[a-z]/u.test(text[prev]))
|
|
133
|
+
letters++;
|
|
134
|
+
else
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (letters === 0)
|
|
138
|
+
return false;
|
|
139
|
+
const next = index + 1 < text.length ? text[index + 1] : '';
|
|
140
|
+
return /[a-z,;):]/u.test(next);
|
|
141
|
+
}
|
|
142
|
+
function codePointLength(text) {
|
|
143
|
+
return [...text].length;
|
|
144
|
+
}
|
|
145
|
+
/** Drops fenced code blocks (```…``` and ~~~…~~~) so tool/program examples cannot leak `description:` matches. */
|
|
146
|
+
function stripCodeFences(lines) {
|
|
147
|
+
const openLines = [];
|
|
148
|
+
let insideFence = false;
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
if (isFenceLine(line)) {
|
|
151
|
+
insideFence = !insideFence;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!insideFence)
|
|
155
|
+
openLines.push(line);
|
|
156
|
+
}
|
|
157
|
+
return openLines;
|
|
158
|
+
}
|
|
159
|
+
function isFenceLine(line) {
|
|
160
|
+
return /^\s*(`{3,}|~{3,})/u.test(line);
|
|
161
|
+
}
|
|
162
|
+
/** Drops HTML comments (`<!--`…`-->`, possibly multi-line) so generated/boilerplate comment text cannot become the description. */
|
|
163
|
+
function stripHtmlComments(lines) {
|
|
164
|
+
const openLines = [];
|
|
165
|
+
let insideComment = false;
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
let rest = line;
|
|
168
|
+
let openPart = '';
|
|
169
|
+
while (rest.length > 0) {
|
|
170
|
+
if (!insideComment) {
|
|
171
|
+
const start = rest.indexOf('<!--');
|
|
172
|
+
if (start === -1) {
|
|
173
|
+
openPart += rest;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
openPart += rest.slice(0, start);
|
|
177
|
+
rest = rest.slice(start + 4);
|
|
178
|
+
insideComment = true;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const end = rest.indexOf('-->');
|
|
182
|
+
if (end === -1) {
|
|
183
|
+
rest = '';
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
rest = rest.slice(end + 3);
|
|
187
|
+
insideComment = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
openLines.push(openPart);
|
|
191
|
+
}
|
|
192
|
+
return openLines;
|
|
193
|
+
}
|
|
194
|
+
export function extractSidecarDescription(fileName, content) {
|
|
195
|
+
if (fileName.toLowerCase().endsWith('.json')) {
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(content);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return { error: `invalid JSON: ${err.message}` };
|
|
202
|
+
}
|
|
203
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
204
|
+
return { error: 'sidecar JSON must be an object' };
|
|
205
|
+
}
|
|
206
|
+
const obj = parsed;
|
|
207
|
+
const fromKey = (value) => typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
208
|
+
const top = fromKey(obj.description);
|
|
209
|
+
if (top !== null)
|
|
210
|
+
return { description: top };
|
|
211
|
+
const link = typeof obj.link === 'object' && obj.link !== null && !Array.isArray(obj.link)
|
|
212
|
+
? obj.link
|
|
213
|
+
: undefined;
|
|
214
|
+
const nested = link === undefined ? null : fromKey(link.description);
|
|
215
|
+
return { description: nested };
|
|
216
|
+
}
|
|
217
|
+
return { description: readDescriptionKey(content.split(/\r?\n/u)) };
|
|
218
|
+
}
|
|
219
|
+
function parseYamlLikeFrontmatter(content) {
|
|
220
|
+
const lines = content.split(/\r?\n/u);
|
|
221
|
+
if (lines[0]?.trim() !== '---')
|
|
222
|
+
return null;
|
|
223
|
+
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
224
|
+
if (endIndex === -1)
|
|
225
|
+
return null;
|
|
226
|
+
return lines.slice(1, endIndex).join('\n');
|
|
227
|
+
}
|
|
228
|
+
function contentAfterFrontmatter(content) {
|
|
229
|
+
const lines = content.split(/\r?\n/u);
|
|
230
|
+
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
231
|
+
return endIndex === -1 ? content : lines.slice(endIndex + 1).join('\n');
|
|
232
|
+
}
|
|
233
|
+
function readDescriptionKey(lines) {
|
|
234
|
+
for (let index = 0; index < lines.length; index++) {
|
|
235
|
+
const line = lines[index];
|
|
236
|
+
const match = line.match(/^\s*(description|decription)\s*:\s*(.*)$/iu);
|
|
237
|
+
if (!match)
|
|
238
|
+
continue;
|
|
239
|
+
const inline = stripYamlString(match[2].trim());
|
|
240
|
+
if (inline)
|
|
241
|
+
return inline;
|
|
242
|
+
const block = readIndentedBlock(lines, index + 1);
|
|
243
|
+
if (block)
|
|
244
|
+
return block;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function readIndentedBlock(lines, start) {
|
|
249
|
+
const collected = [];
|
|
250
|
+
for (let index = start; index < lines.length; index++) {
|
|
251
|
+
const line = lines[index];
|
|
252
|
+
if (line.trim().length === 0) {
|
|
253
|
+
if (collected.length > 0)
|
|
254
|
+
break;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (!/^\s+/.test(line))
|
|
258
|
+
break;
|
|
259
|
+
collected.push(line.trim());
|
|
260
|
+
}
|
|
261
|
+
return collected.length > 0 ? collected.join(' ') : null;
|
|
262
|
+
}
|
|
263
|
+
function stripYamlString(value) {
|
|
264
|
+
const stripped = value
|
|
265
|
+
.replace(/^\|[+-]?$/u, '')
|
|
266
|
+
.replace(/^>[+-]?$/u, '')
|
|
267
|
+
.trim();
|
|
268
|
+
if (!stripped)
|
|
269
|
+
return null;
|
|
270
|
+
const quoted = stripped.match(/^(['"])(.*)\1$/u);
|
|
271
|
+
return (quoted ? quoted[2] : stripped).trim() || null;
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=description.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"description.js","sourceRoot":"","sources":["../src/description.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface GitignoreRule {
|
|
2
|
+
baseDir: string;
|
|
3
|
+
pattern: string;
|
|
4
|
+
negated: boolean;
|
|
5
|
+
directoryOnly: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function readInheritedGitignoreRules(root: string, scanRoot: string): Promise<GitignoreRule[]>;
|
|
8
|
+
export declare function readGitignoreRules(dir: string): Promise<GitignoreRule[]>;
|
|
9
|
+
export declare function isGitignored(filePath: string, isDirectory: boolean, rules: readonly GitignoreRule[]): boolean;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitignore-aware traversal support, ported verbatim from the routes module
|
|
3
|
+
* (dsh-plugin-dev/extras/modules/routes/src/gitignore.ts, ADR-0010: port,
|
|
4
|
+
* don't rewrite). Known subset boundary (routes-documented): `!` re-includes
|
|
5
|
+
* inside one file are honored; cross-file re-include of an ignored path is
|
|
6
|
+
* not — revisit via a port if real corpora hit it (ADR-0010 reversal).
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { join, relative, resolve } from 'node:path';
|
|
10
|
+
export async function readInheritedGitignoreRules(root, scanRoot) {
|
|
11
|
+
const rootPath = resolve(root);
|
|
12
|
+
const scanRootPath = resolve(scanRoot);
|
|
13
|
+
const relativeScanRoot = normalizeSlash(relative(rootPath, scanRootPath));
|
|
14
|
+
const segments = relativeScanRoot && !relativeScanRoot.startsWith('..')
|
|
15
|
+
? relativeScanRoot.split('/').filter(Boolean)
|
|
16
|
+
: [];
|
|
17
|
+
const dirs = [rootPath];
|
|
18
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
19
|
+
dirs.push(join(rootPath, ...segments.slice(0, index + 1)));
|
|
20
|
+
}
|
|
21
|
+
const rules = await Promise.all(dirs.map((dir) => readGitignoreRules(dir)));
|
|
22
|
+
return rules.flat();
|
|
23
|
+
}
|
|
24
|
+
export async function readGitignoreRules(dir) {
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await readFile(join(dir, '.gitignore'), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return content
|
|
33
|
+
.split(/\r?\n/u)
|
|
34
|
+
.map((line) => parseGitignoreLine(dir, line))
|
|
35
|
+
.filter((rule) => rule !== null);
|
|
36
|
+
}
|
|
37
|
+
export function isGitignored(filePath, isDirectory, rules) {
|
|
38
|
+
let ignored = false;
|
|
39
|
+
// Perf (2026-10-20, semantics identical): one relative() per distinct
|
|
40
|
+
// baseDir per call — a typical tree carries many rules from the same
|
|
41
|
+
// ancestor .gitignore, and the path result is rule-invariant.
|
|
42
|
+
const relativeByBaseDir = new Map();
|
|
43
|
+
for (const rule of rules) {
|
|
44
|
+
if (rule.directoryOnly && !isDirectory)
|
|
45
|
+
continue;
|
|
46
|
+
if (!matchesRule(filePath, rule, relativeByBaseDir))
|
|
47
|
+
continue;
|
|
48
|
+
ignored = !rule.negated;
|
|
49
|
+
}
|
|
50
|
+
return ignored;
|
|
51
|
+
}
|
|
52
|
+
function parseGitignoreLine(baseDir, rawLine) {
|
|
53
|
+
let line = rawLine.trim();
|
|
54
|
+
if (!line || line.startsWith('#'))
|
|
55
|
+
return null;
|
|
56
|
+
let negated = false;
|
|
57
|
+
if (line.startsWith('!')) {
|
|
58
|
+
negated = true;
|
|
59
|
+
line = line.slice(1).trim();
|
|
60
|
+
}
|
|
61
|
+
if (!line)
|
|
62
|
+
return null;
|
|
63
|
+
const directoryOnly = line.endsWith('/');
|
|
64
|
+
line = line.replace(/^\/+/u, '').replace(/\/+$/u, '');
|
|
65
|
+
if (!line)
|
|
66
|
+
return null;
|
|
67
|
+
return {
|
|
68
|
+
baseDir,
|
|
69
|
+
pattern: normalizeSlash(line),
|
|
70
|
+
negated,
|
|
71
|
+
directoryOnly,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function matchesRule(filePath, rule, relativeByBaseDir) {
|
|
75
|
+
let relativePath = relativeByBaseDir.get(rule.baseDir);
|
|
76
|
+
if (relativePath === undefined) {
|
|
77
|
+
relativePath = normalizeSlash(relative(rule.baseDir, filePath));
|
|
78
|
+
relativeByBaseDir.set(rule.baseDir, relativePath);
|
|
79
|
+
}
|
|
80
|
+
if (!relativePath || relativePath.startsWith('..'))
|
|
81
|
+
return false;
|
|
82
|
+
if (!rule.pattern.includes('/')) {
|
|
83
|
+
return relativePath.split('/').some((part) => matchesGlob(part, rule.pattern));
|
|
84
|
+
}
|
|
85
|
+
return matchesGlob(relativePath, rule.pattern);
|
|
86
|
+
}
|
|
87
|
+
/** Compiled-glob memo (perf 2026-10-20, semantics identical): the regex
|
|
88
|
+
* depends only on the pattern, and a scan checks the same handful of
|
|
89
|
+
* patterns once per entry — compiling per check was ~70% of large-tree
|
|
90
|
+
* scan CPU. Patterns are few and bounded by the corpus's .gitignore lines. */
|
|
91
|
+
const globRegexCache = new Map();
|
|
92
|
+
function matchesGlob(value, pattern) {
|
|
93
|
+
let regex = globRegexCache.get(pattern);
|
|
94
|
+
if (regex === undefined) {
|
|
95
|
+
regex = new RegExp(`^${globToRegex(pattern)}$`, 'u');
|
|
96
|
+
globRegexCache.set(pattern, regex);
|
|
97
|
+
}
|
|
98
|
+
return regex.test(value);
|
|
99
|
+
}
|
|
100
|
+
function globToRegex(pattern) {
|
|
101
|
+
let regex = '';
|
|
102
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
103
|
+
const char = pattern[index];
|
|
104
|
+
const next = pattern[index + 1];
|
|
105
|
+
if (char === '*' && next === '*') {
|
|
106
|
+
regex += '.*';
|
|
107
|
+
index += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (char === '*') {
|
|
111
|
+
regex += '[^/]*';
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === '?') {
|
|
115
|
+
regex += '[^/]';
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
regex += escapeRegex(char);
|
|
119
|
+
}
|
|
120
|
+
return regex;
|
|
121
|
+
}
|
|
122
|
+
function escapeRegex(value) {
|
|
123
|
+
return value.replace(/[|\\{}()[\]^$+*?.]/gu, '\\$&');
|
|
124
|
+
}
|
|
125
|
+
function normalizeSlash(value) {
|
|
126
|
+
return value.replace(/\\/gu, '/');
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=gitignore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitignore.js","sourceRoot":"","sources":["../src/gitignore.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @any-nav/scan public API — the lean engine face.
|
|
3
|
+
*
|
|
4
|
+
* Boundary rules this surface must preserve:
|
|
5
|
+
* 1. zero runtime dependencies beyond node builtins — embedders install
|
|
6
|
+
* nothing else (this package exists so they never carry the SDK's
|
|
7
|
+
* smol-toml / fast-bm25);
|
|
8
|
+
* 2. read-only over the filesystem — scanning never modifies sources
|
|
9
|
+
* (map is not territory);
|
|
10
|
+
* 3. terminal options: ResolvedScanOptions is what it says — callers
|
|
11
|
+
* hand the scanner final values; the additive default floor is a
|
|
12
|
+
* convenience (resolveScanOptions), not an obligation.
|
|
13
|
+
*/
|
|
14
|
+
export type { DiagnosticAudience, NavItem, NavLocator, NavLocatorKind, NavNode, ScanConfig, ScanDiagnostic, } from './types.ts';
|
|
15
|
+
export type { DescriptionProvider, ResolvedScanOptions, ScanOutput, ScannedFile, StructureScanOutput, } from './scan.ts';
|
|
16
|
+
export { builtinDescriptionProvider, DEFAULT_EXCLUDE_DIRS, DEFAULT_EXCLUDE_FILES, DIR_SIDECAR_NAMES, isDirSidecarName, isExcludedFileName, resolveScanOptions, scanSource, scanStructure, } from './scan.ts';
|
|
17
|
+
export { extractDescription } from './description.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { builtinDescriptionProvider, DEFAULT_EXCLUDE_DIRS, DEFAULT_EXCLUDE_FILES, DIR_SIDECAR_NAMES, isDirSidecarName, isExcludedFileName, resolveScanOptions, scanSource, scanStructure, } from './scan.js';
|
|
2
|
+
export { extractDescription } from './description.js';
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
|