@any-nav/core 0.0.0-stage → 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/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-10-31
4
+
5
+ - fast-bm25 loads lazily on first engine construction: importing the
6
+ package no longer touches the search engine at module-load time, so
7
+ every non-search path (CLI `list` / `list --json`, dsh nav) skips it.
8
+ Public API and search results are unchanged.
9
+ - Routing and curated rows take a folder's `markdown` locator from the
10
+ scan's representative-README field on `NavNode`; the view-level README
11
+ pairing is retired.
12
+ - The re-exported scan face tracks `@any-nav/scan` 0.2.0: the walk engine
13
+ delegates to `@catheadowl/pathscope`, folder descriptions read the
14
+ directory's `package.json` / `manifest.yml` carriers, and the former
15
+ traversal-boundary export family is removed with the in-package engine
16
+ it belonged to — see the scan changelog for the full face.
17
+
18
+ ## 0.1.0 — 2026-10-21
19
+
20
+ First registry release.
21
+
22
+ - Public SDK surface: the `createAnyNav` / `listFolder` facade, config
23
+ (`loadConfigFile` / `parseConfig` / `ConfigError`), the budget-guard
24
+ engine, render / `materialize` with built-in `agents-md` and
25
+ `generic-markdown` formats, and the query faces (`routingView`,
26
+ `curatedView`, `candidateFeed`, the search kernel, `readSource`).
27
+ - `routingViewJson` — the routing projection serialized as pure JSON, the
28
+ machine face for CLI `--json` and library consumers.
29
+ - Scanning now composes the split-out zero-dependency `@any-nav/scan`
30
+ package and re-exports its full face; embedders who only need the engine
31
+ can depend on `@any-nav/scan` directly.
32
+ - Listings compose the structure-only walk — depth-bounded views hydrate
33
+ only the rows they render, not every file under the root.
34
+ - Budget cuts surface as lower bounds: the routing face carries
35
+ `countIncompletePaths`; rendered rows show ≥N floors with the
36
+ `count-truncated-at-limit` diagnostic.
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,70 @@
1
- # Temporary Holding Version
1
+ # @any-nav/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > The Any-Nav SDK: scan a Markdown folder, guard it against a context budget, render navigation fragments, and answer list / search / read.
4
+
5
+ `@any-nav/core` is the read-only SDK behind Any-Nav. It composes the
6
+ zero-dependency scan engine (`@any-nav/scan` — its full face is re-exported
7
+ here) with: strict `any-nav.toml` config parsing, curation of default
8
+ expansion via the config's `local` list, the budget-guard engine, rendering
9
+ of the canonical navigation fragment for registered formats, and the query
10
+ faces — `list(path)` with its rows / markdown renderings and JSON
11
+ serialization, the search kernel, `read`, and the candidate feed.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install @any-nav/core
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ The one-call face — list a folder under its nearest governing config (pure
22
+ defaults when there is none):
23
+
24
+ ```ts
25
+ import { listFolder, routingViewJson } from '@any-nav/core';
26
+
27
+ const listing = await listFolder('docs'); // optional query: { configPath, depth, commandRunner }
28
+ const machine = routingViewJson(listing); // the routing projection as a pure JSON string
29
+ ```
30
+
31
+ The full facade:
32
+
33
+ ```ts
34
+ import { createAnyNav } from '@any-nav/core';
35
+
36
+ const app = await createAnyNav({ configPath: 'any-nav.toml' }); // or { config, cwd }
37
+
38
+ const sources = app.sources(); // the config root as the single implicit source
39
+ const exposure = app.decideExposure(sources[0]); // budget-guard reading — warns, never collapses
40
+ const fragment = app.render(exposure, 'agents-md');
41
+ const next = app.materialize(current, fragment); // pure splice — the caller writes the file
42
+ ```
43
+
44
+ ## Config
45
+
46
+ `any-nav.toml` states only exceptions; every key defaults:
47
+
48
+ - `local` — curation list: paths expanded into the default listing.
49
+ - `budget` — the context-budget guard (over budget warns, never silently
50
+ collapses).
51
+ - `scan` — scan boundary policy (`excludeDirs` / `excludeFiles` /
52
+ `excludeDotEntries` / `respectGitignore` / `maxFiles`), additive over the
53
+ engine's built-in floor.
54
+
55
+ ## Boundaries this SDK keeps
56
+
57
+ - Read-only over the filesystem: no writes, no processes, no network, no
58
+ transport or LLM-client imports. `materialize()` returns new text — every
59
+ write belongs to the adapter.
60
+ - The unified list model: `list(path)` is the product face; governance and
61
+ expansion live here, adapters only transport.
62
+ - `NavItem { name, description, locator }` (from `@any-nav/scan`) is the only
63
+ knowledge model.
64
+ - Embedders that only need the scan engine can depend on `@any-nav/scan`
65
+ directly and carry no SDK dependencies (smol-toml, fast-bm25 live on this
66
+ package's config and search paths, not the engine's).
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,39 @@
1
+ import type { ScanConfig } from '@any-nav/scan';
2
+ export interface BudgetConfig {
3
+ /** Share of the context budget navigation may occupy, 0 < share ≤ 1. */
4
+ share?: number;
5
+ /** Hard cap in estimated tokens. */
6
+ maxTokens?: number;
7
+ }
8
+ export interface AnyNavConfig {
9
+ /**
10
+ * Curated default-expansion list (the `local` kind family; external /
11
+ * command members are parked in a trigger unit). Entries are
12
+ * config-root-relative folder paths; a listed folder's row expands to
13
+ * its direct children. Membership says expansion, not existence —
14
+ * folders off the list stay addressable by path. Optional; default = no
15
+ * expansion (depth-0 everywhere).
16
+ */
17
+ local?: string[];
18
+ budget?: BudgetConfig;
19
+ scan?: ScanConfig;
20
+ /**
21
+ * Declared entry commands (2026-10-16): full CLI command lines an agent
22
+ * runs once when starting here, e.g. "mytool instructions". Pure
23
+ * declarations — any-nav never executes them and never reads their
24
+ * output; the list faces render one leading procedure line. Optional;
25
+ * default = none.
26
+ */
27
+ commands?: string[];
28
+ }
29
+ export declare class ConfigError extends Error {
30
+ constructor(message: string);
31
+ }
32
+ export declare function parseConfig(text: string): AnyNavConfig;
33
+ export declare function loadConfigFile(configPath: string): Promise<AnyNavConfig>;
34
+ /**
35
+ * Normalize one local entry to a clean config-root-relative POSIX path.
36
+ * The unit is the path — no globs, no numeric depths (Any principle: an
37
+ * entry states "expand this folder", containment merges trees for free).
38
+ */
39
+ export declare function normalizeLocalPath(entry: string, where: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Config: parse and strictly validate any-nav.toml.
3
+ *
4
+ * The file only states exceptions — scan / local / budget / commands are
5
+ * the whole key set (framework decision 9, unified list model; commands
6
+ * joined 2026-10-16, entry-procedures argumentation): scan bounds the
7
+ * subtree, local curates default expansion, budget guards size, commands
8
+ * declares entry procedure. Unknown keys are rejected rather than ignored.
9
+ */
10
+ import { readFile } from 'node:fs/promises';
11
+ import { parse } from 'smol-toml';
12
+ export class ConfigError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'ConfigError';
16
+ }
17
+ }
18
+ export function parseConfig(text) {
19
+ let raw;
20
+ try {
21
+ raw = parse(text);
22
+ }
23
+ catch (err) {
24
+ throw new ConfigError(`TOML syntax error: ${err.message}`);
25
+ }
26
+ if (typeof raw !== 'object' || raw === null) {
27
+ throw new ConfigError('config must be a TOML table');
28
+ }
29
+ const obj = raw;
30
+ const unknownTop = Object.keys(obj).filter((k) => k !== 'local' && k !== 'budget' && k !== 'scan' && k !== 'commands');
31
+ if (unknownTop.length > 0) {
32
+ throw new ConfigError(`unknown top-level keys: ${unknownTop.join(', ')}`);
33
+ }
34
+ const local = obj.local === undefined ? undefined : parseLocal(obj.local);
35
+ const budget = obj.budget === undefined ? undefined : parseBudget(obj.budget);
36
+ const scan = obj.scan === undefined ? undefined : parseScan(obj.scan);
37
+ const commands = obj.commands === undefined ? undefined : parseCommands(obj.commands);
38
+ const config = {};
39
+ if (local !== undefined)
40
+ config.local = local;
41
+ if (budget !== undefined)
42
+ config.budget = budget;
43
+ if (scan !== undefined)
44
+ config.scan = scan;
45
+ if (commands !== undefined)
46
+ config.commands = commands;
47
+ return config;
48
+ }
49
+ export async function loadConfigFile(configPath) {
50
+ const text = await readFile(configPath, 'utf8');
51
+ return parseConfig(text);
52
+ }
53
+ /**
54
+ * Normalize one local entry to a clean config-root-relative POSIX path.
55
+ * The unit is the path — no globs, no numeric depths (Any principle: an
56
+ * entry states "expand this folder", containment merges trees for free).
57
+ */
58
+ export function normalizeLocalPath(entry, where) {
59
+ if (typeof entry !== 'string' || entry.trim() === '') {
60
+ throw new ConfigError(`${where} must be a non-empty path string`);
61
+ }
62
+ if (/[*?[\]]/.test(entry)) {
63
+ throw new ConfigError(`${where} must be a plain path, not a glob: ${entry}`);
64
+ }
65
+ const normalized = entry.replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
66
+ if (normalized === '' || normalized === '.') {
67
+ throw new ConfigError(`${where} must name a folder under the config root, not the root itself: ${entry}`);
68
+ }
69
+ const segments = normalized.split('/');
70
+ if (segments.some((s) => s === '' || s === '.')) {
71
+ throw new ConfigError(`${where} is not a clean relative path: ${entry}`);
72
+ }
73
+ if (segments.includes('..')) {
74
+ throw new ConfigError(`${where} must stay under the config root: ${entry}`);
75
+ }
76
+ if (/^[a-zA-Z]:/.test(normalized) || normalized.startsWith('/')) {
77
+ throw new ConfigError(`${where} must be config-root-relative, not absolute: ${entry}`);
78
+ }
79
+ return segments.join('/');
80
+ }
81
+ function parseLocal(raw) {
82
+ if (!Array.isArray(raw) || raw.length === 0) {
83
+ throw new ConfigError('local must be a non-empty array of folder paths (omit the key for no expansion)');
84
+ }
85
+ const seen = new Set();
86
+ const out = [];
87
+ raw.forEach((entry, i) => {
88
+ const normalized = normalizeLocalPath(entry, `local[${i}]`);
89
+ if (seen.has(normalized)) {
90
+ throw new ConfigError(`local lists the same path twice: ${normalized}`);
91
+ }
92
+ seen.add(normalized);
93
+ out.push(normalized);
94
+ });
95
+ return out;
96
+ }
97
+ function parseScan(raw) {
98
+ if (typeof raw !== 'object' || raw === null) {
99
+ throw new ConfigError('scan must be a table');
100
+ }
101
+ const obj = raw;
102
+ const known = [
103
+ 'excludeDirs',
104
+ 'excludeFiles',
105
+ 'excludeDotEntries',
106
+ 'respectGitignore',
107
+ 'boundaryExempt',
108
+ 'maxFiles',
109
+ ];
110
+ const unknown = Object.keys(obj).filter((k) => !known.includes(k));
111
+ if (unknown.length > 0) {
112
+ throw new ConfigError(`unknown keys in scan: ${unknown.join(', ')}`);
113
+ }
114
+ const scan = {};
115
+ for (const key of ['excludeDirs', 'excludeFiles', 'boundaryExempt']) {
116
+ const value = obj[key];
117
+ if (value === undefined)
118
+ continue;
119
+ if (!Array.isArray(value) || !value.every((s) => typeof s === 'string' && s.trim() !== '')) {
120
+ throw new ConfigError(`scan.${key} must be an array of non-empty strings`);
121
+ }
122
+ if (value.length === 0) {
123
+ throw new ConfigError(key === 'boundaryExempt'
124
+ ? 'scan.boundaryExempt must not be empty — omit the key for no exemptions (an empty list states nothing)'
125
+ : `scan.${key} must not be empty — omit the key to keep just the built-in defaults (it appends, never replaces)`);
126
+ }
127
+ scan[key] = value;
128
+ }
129
+ for (const key of ['excludeDotEntries', 'respectGitignore']) {
130
+ const value = obj[key];
131
+ if (value === undefined)
132
+ continue;
133
+ if (typeof value !== 'boolean') {
134
+ throw new ConfigError(`scan.${key} must be a boolean`);
135
+ }
136
+ scan[key] = value;
137
+ }
138
+ if (obj.maxFiles !== undefined) {
139
+ if (typeof obj.maxFiles !== 'number' || !Number.isInteger(obj.maxFiles) || obj.maxFiles <= 0) {
140
+ throw new ConfigError('scan.maxFiles must be a positive integer');
141
+ }
142
+ scan.maxFiles = obj.maxFiles;
143
+ }
144
+ return scan;
145
+ }
146
+ function parseCommands(raw) {
147
+ if (!Array.isArray(raw) || raw.length === 0) {
148
+ throw new ConfigError('commands must be a non-empty array of CLI command lines (omit the key for none)');
149
+ }
150
+ const seen = new Set();
151
+ raw.forEach((entry, i) => {
152
+ if (typeof entry !== 'string' || entry.trim() === '') {
153
+ throw new ConfigError(`commands[${i}] must be a non-empty command line string`);
154
+ }
155
+ if (seen.has(entry)) {
156
+ throw new ConfigError(`commands lists the same command twice: ${entry}`);
157
+ }
158
+ seen.add(entry);
159
+ });
160
+ return raw;
161
+ }
162
+ function parseBudget(raw) {
163
+ if (typeof raw !== 'object' || raw === null) {
164
+ throw new ConfigError('budget must be a table');
165
+ }
166
+ const obj = raw;
167
+ const unknown = Object.keys(obj).filter((k) => k !== 'share' && k !== 'maxTokens');
168
+ if (unknown.length > 0) {
169
+ throw new ConfigError(`unknown keys in budget: ${unknown.join(', ')}`);
170
+ }
171
+ const budget = {};
172
+ if (obj.share !== undefined) {
173
+ if (typeof obj.share !== 'number' || !(obj.share > 0 && obj.share <= 1)) {
174
+ throw new ConfigError('budget.share must be a number in (0, 1]');
175
+ }
176
+ budget.share = obj.share;
177
+ }
178
+ if (obj.maxTokens !== undefined) {
179
+ if (typeof obj.maxTokens !== 'number' || !Number.isInteger(obj.maxTokens) || obj.maxTokens <= 0) {
180
+ throw new ConfigError('budget.maxTokens must be a positive integer');
181
+ }
182
+ budget.maxTokens = obj.maxTokens;
183
+ }
184
+ return budget;
185
+ }
186
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Engine: token estimation and the budget guard.
3
+ *
4
+ * Mode forms are retired (framework decision 9, unified list model): the
5
+ * fragment's shape is the curated `local` view, and budget is a fuse —
6
+ * over budget warns loudly, never silently. The write-face guard that
7
+ * acts on it (collapse marker branches, then trim lines) lives in
8
+ * render's fragmentBodyGuarded; this module only sizes and warns.
9
+ */
10
+ import type { BudgetConfig } from './config.ts';
11
+ import type { FragmentNode } from './types.ts';
12
+ export interface Exposure {
13
+ source: {
14
+ path: string;
15
+ };
16
+ estimatedTokens: number;
17
+ budgetTokens: number;
18
+ /** True when the fragment exceeds the budget — warned, never silently altered. */
19
+ overBudget: boolean;
20
+ /** Human-readable guard reading (declarer-facing; the fragment itself carries no aggregate note). */
21
+ reason: string;
22
+ }
23
+ export declare const NON_CJK_CHARS_PER_TOKEN = 4;
24
+ export declare const FORMAT_OVERHEAD = 1.25;
25
+ export declare const DEFAULT_SHARE = 0.02;
26
+ export declare const DEFAULT_CONTEXT_TOKENS = 100000;
27
+ export declare function estimateTokens(text: string): number;
28
+ export declare function resolveBudget(budget?: BudgetConfig): number;
29
+ /** Format-agnostic listing used for size estimation: the curated view walked as rows. */
30
+ export declare function canonicalListing(nodes: FragmentNode[]): string;
31
+ export declare function decideExposure(source: {
32
+ path: string;
33
+ }, nodes: FragmentNode[], budgetTokens: number): Exposure;
package/dist/engine.js ADDED
@@ -0,0 +1,43 @@
1
+ // Heuristic constants — thresholds are engineering means, the constraint
2
+ // is the context budget. Calibrate with measurements during M1 usage.
3
+ export const NON_CJK_CHARS_PER_TOKEN = 4;
4
+ export const FORMAT_OVERHEAD = 1.25;
5
+ export const DEFAULT_SHARE = 0.02;
6
+ export const DEFAULT_CONTEXT_TOKENS = 100_000;
7
+ const CJK = /[\u{4E00}-\u{9FFF}\u{3040}-\u{30FF}\u{AC00}-\u{D7AF}]/gu;
8
+ export function estimateTokens(text) {
9
+ const cjk = [...text.matchAll(CJK)].length;
10
+ const rest = text.length - cjk;
11
+ return Math.ceil(cjk + rest / NON_CJK_CHARS_PER_TOKEN);
12
+ }
13
+ export function resolveBudget(budget) {
14
+ if (budget?.maxTokens !== undefined)
15
+ return budget.maxTokens;
16
+ const share = budget?.share ?? DEFAULT_SHARE;
17
+ return Math.max(1, Math.round(share * DEFAULT_CONTEXT_TOKENS));
18
+ }
19
+ /** Format-agnostic listing used for size estimation: the curated view walked as rows. */
20
+ export function canonicalListing(nodes) {
21
+ const lines = [];
22
+ const visit = (nodes) => {
23
+ for (const node of nodes) {
24
+ lines.push(`${node.name}${node.kind === 'folder' ? '/' : ''} — ${node.description ?? ''} (${node.path})`);
25
+ if (node.children !== undefined)
26
+ visit(node.children);
27
+ }
28
+ };
29
+ visit(nodes);
30
+ return lines.join('\n');
31
+ }
32
+ function sized(text) {
33
+ return Math.ceil(estimateTokens(text) * FORMAT_OVERHEAD);
34
+ }
35
+ export function decideExposure(source, nodes, budgetTokens) {
36
+ const estimatedTokens = sized(canonicalListing(nodes));
37
+ const overBudget = estimatedTokens > budgetTokens;
38
+ const reason = overBudget
39
+ ? `fragment exceeds the budget (${estimatedTokens} > ${budgetTokens} tokens) — the rows answer stays whole; see 'any-nav help config'`
40
+ : `fragment fits the budget (${estimatedTokens} ≤ ${budgetTokens} tokens)`;
41
+ return { source, estimatedTokens, budgetTokens, overBudget, reason };
42
+ }
43
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAsBA,yEAAyE;AACzE,sEAAsE;AACtE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AACzC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;AAClC,MAAM,CAAC,MAAM,sBAAsB,GAAG,OAAO,CAAC;AAE9C,MAAM,GAAG,GAAG,yDAAyD,CAAC;AAEtE,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;IAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,uBAAuB,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,IAAI,MAAM,EAAE,SAAS,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC;IAC7D,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,aAAa,CAAC;IAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,sBAAsB,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,KAAqB;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,KAAqB,EAAQ,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,WAAW,IAAI,EAAE,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAC3G,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxD,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAwB,EACxB,KAAqB,EACrB,YAAoB;IAEpB,MAAM,eAAe,GAAG,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,eAAe,GAAG,YAAY,CAAC;IAClD,MAAM,MAAM,GAAG,UAAU;QACvB,CAAC,CAAC,gCAAgC,eAAe,MAAM,YAAY,mEAAmE;QACtI,CAAC,CAAC,6BAA6B,eAAe,MAAM,YAAY,UAAU,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Expand markers: resolve the config's `local` list into the member set
3
+ * for the config's OWN view (framework decision 9; strict locality
4
+ * 2026-10-07: a toml serves only its own folder's direct listing —
5
+ * re-rooting into a descendant's view is abolished).
6
+ *
7
+ * Membership says expansion, not existence: a folder on the list expands
8
+ * to its direct children wherever its row is rendered within this view;
9
+ * every folder stays path-addressable regardless. Entries are
10
+ * config-root-relative and apply only when the toml's own folder is the
11
+ * listed folder. Deep entries stand alone (transparent corridors,
12
+ * 2026-10-08): an ancestor that is not itself a member passes only the
13
+ * member path through — see hasMemberDescendant.
14
+ */
15
+ import type { NavNode, ScanDiagnostic } from './types.ts';
16
+ /** Member paths for the config's own view — the list as written. */
17
+ export declare function expandMemberSet(local: readonly string[] | undefined): ReadonlySet<string>;
18
+ /**
19
+ * Whether a non-member folder carries a member descendant — the corridor
20
+ * test (transparent-corridor ruling, 2026-10-08): such a folder renders
21
+ * nothing of its own; only the member path passes through it, so a deep
22
+ * entry never needs its ancestors on the list.
23
+ */
24
+ export declare function hasMemberDescendant(node: NavNode, members: ReadonlySet<string>): boolean;
25
+ /**
26
+ * Guard diagnostics for the whole local list, checked once per instance
27
+ * against the governing config-root scan: a marker naming a path with no
28
+ * folder in the scanned tree fires a warning — never silent. Scan policy
29
+ * (excluded subtrees) counts as absent: such a marker can never fire.
30
+ */
31
+ export declare function expandDiagnostics(local: readonly string[] | undefined, tree: NavNode): ScanDiagnostic[];
32
+ /** Folder lookup by source-relative path; null when absent or not a directory. */
33
+ export declare function findDir(root: NavNode, path: string): NavNode | null;
package/dist/expand.js ADDED
@@ -0,0 +1,61 @@
1
+ /** Member paths for the config's own view — the list as written. */
2
+ export function expandMemberSet(local) {
3
+ const members = new Set();
4
+ if (local === undefined)
5
+ return members;
6
+ for (const entry of local)
7
+ members.add(entry);
8
+ return members;
9
+ }
10
+ /**
11
+ * Whether a non-member folder carries a member descendant — the corridor
12
+ * test (transparent-corridor ruling, 2026-10-08): such a folder renders
13
+ * nothing of its own; only the member path passes through it, so a deep
14
+ * entry never needs its ancestors on the list.
15
+ */
16
+ export function hasMemberDescendant(node, members) {
17
+ for (const child of node.children ?? []) {
18
+ if (child.kind !== 'dir')
19
+ continue;
20
+ if (members.has(child.path) || hasMemberDescendant(child, members))
21
+ return true;
22
+ }
23
+ return false;
24
+ }
25
+ /**
26
+ * Guard diagnostics for the whole local list, checked once per instance
27
+ * against the governing config-root scan: a marker naming a path with no
28
+ * folder in the scanned tree fires a warning — never silent. Scan policy
29
+ * (excluded subtrees) counts as absent: such a marker can never fire.
30
+ */
31
+ export function expandDiagnostics(local, tree) {
32
+ if (local === undefined)
33
+ return [];
34
+ const diagnostics = [];
35
+ for (const entry of local) {
36
+ if (findDir(tree, entry) === null) {
37
+ diagnostics.push({
38
+ code: 'expand-path-missing',
39
+ severity: 'warning',
40
+ message: `local entry '${entry}' has no folder in the scanned tree (missing or excluded by scan policy); it will never expand.`,
41
+ path: entry,
42
+ audience: 'maintainer',
43
+ });
44
+ }
45
+ }
46
+ return diagnostics;
47
+ }
48
+ /** Folder lookup by source-relative path; null when absent or not a directory. */
49
+ export function findDir(root, path) {
50
+ if (path === '')
51
+ return root;
52
+ let node = root;
53
+ for (const segment of path.split('/')) {
54
+ const next = node.children?.find((c) => c.kind === 'dir' && c.name === segment);
55
+ if (!next)
56
+ return null;
57
+ node = next;
58
+ }
59
+ return node;
60
+ }
61
+ //# sourceMappingURL=expand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expand.js","sourceRoot":"","sources":["../src/expand.ts"],"names":[],"mappings":"AAgBA,oEAAoE;AACpE,MAAM,UAAU,eAAe,CAAC,KAAoC;IAClE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,KAAK;QAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa,EAAE,OAA4B;IAC7E,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QACxC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK;YAAE,SAAS;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;IAClF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAoC,EAAE,IAAa;IACnF,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,WAAW,GAAqB,EAAE,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;YAClC,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,qBAAqB;gBAC3B,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,gBAAgB,KAAK,iGAAiG;gBAC/H,IAAI,EAAE,KAAK;gBACX,QAAQ,EAAE,YAAY;aACvB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,OAAO,CAAC,IAAa,EAAE,IAAY;IACjD,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,IAAI,GAAY,IAAI,CAAC;IACzB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;QAChF,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { AnyNavConfig } from './config.ts';
2
+ import type { DescriptionProvider } from '@any-nav/scan';
3
+ import { type Exposure } from './engine.ts';
4
+ import { type NavFormat } from './render.ts';
5
+ import type { ListView, NavCandidateFeed, NavLocator, NavSource, RenderedFragment, SearchHit } from './types.ts';
6
+ /** Reserved (v1.x) so index/description caches never make core write. */
7
+ export interface CacheStore {
8
+ get(key: string): Promise<string | undefined>;
9
+ set(key: string, value: string): Promise<void>;
10
+ }
11
+ /**
12
+ * Entry-command execution port (commands key, full-render ruling
13
+ * 2026-10-16): core never spawns — the adapter injects the runner, core
14
+ * consumes the captured stdout and renders it canonically. cwd is the
15
+ * governing toml's directory (the tool's project context).
16
+ */
17
+ export type CommandRunner = (command: string, ctx: {
18
+ cwd: string;
19
+ }) => Promise<{
20
+ ok: true;
21
+ stdout: string;
22
+ } | {
23
+ ok: false;
24
+ reason: string;
25
+ }>;
26
+ export interface CreateAnyNavInput {
27
+ /** Config object, or a path to any-nav.toml (adapters decide discovery). */
28
+ config: AnyNavConfig | {
29
+ configPath: string;
30
+ };
31
+ /** Base directory for resolving relative source paths when passing an
32
+ * object; a config file resolves against its own directory. */
33
+ cwd: string;
34
+ /** Extra axis-B formats; built-ins are always registered. */
35
+ formats?: NavFormat[];
36
+ descriptionProvider?: DescriptionProvider;
37
+ cache?: CacheStore;
38
+ }
39
+ export interface AnyNav {
40
+ /** The scanned sources — the config root itself (implicit single source). */
41
+ sources(): NavSource[];
42
+ /** The budget-guard reading for one source (warns, never collapses here). */
43
+ decideExposure(source: NavSource): Exposure;
44
+ render(exposure: Exposure, formatId: string): RenderedFragment;
45
+ /** Pure splice of a fragment into existing content; the adapter writes it. */
46
+ materialize(currentContent: string, fragment: RenderedFragment): string;
47
+ /**
48
+ * The product face, pinned form: `dir` defaults to '.', any folder
49
+ * under this instance's config root (the boundary survives for
50
+ * --config-style pinning). The discovery form — nearest toml at or
51
+ * above the listed folder, pure defaults when none — is `listFolder`;
52
+ * adapters that don't pin should call that instead.
53
+ */
54
+ list(dir?: string, query?: {
55
+ depth?: number;
56
+ commandRunner?: CommandRunner;
57
+ }): Promise<ListView>;
58
+ search(query: {
59
+ text: string;
60
+ limit?: number;
61
+ }): SearchHit[];
62
+ read(locator: NavLocator): Promise<{
63
+ content: string;
64
+ mediaType: 'text/markdown';
65
+ }>;
66
+ /**
67
+ * Candidate feed (mode ④ supply face): the curated tree
68
+ * flattened into word-picker candidates, every path re-rooted onto
69
+ * baseDir. Snapshot semantics — freshness is the consumer's rebuild
70
+ * policy. maxFiles caps the feed loudly (truncated + omittedCount).
71
+ */
72
+ candidates(query: {
73
+ baseDir: string;
74
+ maxFiles?: number;
75
+ }): NavCandidateFeed;
76
+ }
77
+ export declare function createAnyNav(input: CreateAnyNavInput): Promise<AnyNav>;
78
+ /** A listing plus the config that actually governed it (null = defaults). */
79
+ export interface FolderListing extends ListView {
80
+ governingConfig: string | null;
81
+ }
82
+ /**
83
+ * The product face, discovery form (2026-10-07 optionality ruling):
84
+ * governance = the nearest any-nav.toml at or above `dir` — the folder's
85
+ * own toml if it carries one, else the closest ancestor's, else pure
86
+ * defaults. `configPath` pins a specific config instead (paths outside
87
+ * its root then error — the pinned boundary). cwd never participates.
88
+ */
89
+ export declare function listFolder(dir: string, query?: {
90
+ configPath?: string;
91
+ depth?: number;
92
+ commandRunner?: CommandRunner;
93
+ }): Promise<FolderListing>;