@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 +36 -0
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/config.d.ts +39 -0
- package/dist/config.js +186 -0
- package/dist/config.js.map +1 -0
- package/dist/engine.d.ts +33 -0
- package/dist/engine.js +43 -0
- package/dist/engine.js.map +1 -0
- package/dist/expand.d.ts +33 -0
- package/dist/expand.js +61 -0
- package/dist/expand.js.map +1 -0
- package/dist/facade.d.ts +93 -0
- package/dist/facade.js +385 -0
- package/dist/facade.js.map +1 -0
- package/dist/formats/agents-md.d.ts +15 -0
- package/dist/formats/agents-md.js +30 -0
- package/dist/formats/agents-md.js.map +1 -0
- package/dist/formats/generic-markdown.d.ts +9 -0
- package/dist/formats/generic-markdown.js +18 -0
- package/dist/formats/generic-markdown.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +6 -0
- package/dist/json.js +48 -0
- package/dist/json.js.map +1 -0
- package/dist/query.d.ts +96 -0
- package/dist/query.js +405 -0
- package/dist/query.js.map +1 -0
- package/dist/render.d.ts +105 -0
- package/dist/render.js +209 -0
- package/dist/render.js.map +1 -0
- package/dist/search-engine.d.ts +50 -0
- package/dist/search-engine.js +103 -0
- package/dist/search-engine.js.map +1 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -3
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
|
-
#
|
|
1
|
+
# @any-nav/core
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/config.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAiClC,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,WAAW,CAAC,sBAAuB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,6BAA6B,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CACxC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,UAAU,CAC3E,CAAC;IACF,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,WAAW,CAAC,2BAA2B,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtF,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACjD,IAAI,IAAI,KAAK,SAAS;QAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;IAC3C,IAAI,QAAQ,KAAK,SAAS;QAAE,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACvD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,UAAkB;IACrD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAChD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,KAAa;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,sCAAsC,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvF,IAAI,UAAU,KAAK,EAAE,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,mEAAmE,KAAK,EAAE,CAAC,CAAC;IAC5G,CAAC;IACD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,kCAAkC,KAAK,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,qCAAqC,KAAK,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,gDAAgD,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,UAAU,CAAC,GAAY;IAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,iFAAiF,CAAC,CAAC;IAC3G,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QACvB,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAe,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QACtE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,WAAW,CAAC,oCAAoC,UAAU,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACrB,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAAC,GAAY;IAC7B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,KAAK,GAAG;QACZ,aAAa;QACb,cAAc;QACd,mBAAmB;QACnB,kBAAkB;QAClB,gBAAgB;QAChB,UAAU;KACX,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,WAAW,CAAC,yBAAyB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,IAAI,GAAe,EAAE,CAAC;IAC5B,KAAK,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,cAAc,EAAE,gBAAgB,CAAU,EAAE,CAAC;QAC7E,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;YAC3F,MAAM,IAAI,WAAW,CAAC,QAAQ,GAAG,wCAAwC,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,WAAW,CACnB,GAAG,KAAK,gBAAgB;gBACtB,CAAC,CAAC,uGAAuG;gBACzG,CAAC,CAAC,QAAQ,GAAG,mGAAmG,CACnH,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,KAAiB,CAAC;IAChC,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,CAAC,mBAAmB,EAAE,kBAAkB,CAAU,EAAE,CAAC;QACrE,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,WAAW,CAAC,QAAQ,GAAG,oBAAoB,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACpB,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC/B,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;YAC7F,MAAM,IAAI,WAAW,CAAC,0CAA0C,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,GAAY;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,iFAAiF,CAAC,CAAC;IAC3G,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACrD,MAAM,IAAI,WAAW,CAAC,YAAY,CAAC,2CAA2C,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,WAAW,CAAC,0CAA0C,KAAK,EAAE,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IACH,OAAO,GAAe,CAAC;AACzB,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,wBAAwB,CAAC,CAAC;IAClD,CAAC;IACD,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,WAAW,CAAC,CAAC;IACnF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,WAAW,CAAC,2BAA2B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC;YACxE,MAAM,IAAI,WAAW,CAAC,yCAAyC,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IAC3B,CAAC;IACD,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;YAChG,MAAM,IAAI,WAAW,CAAC,6CAA6C,CAAC,CAAC;QACvE,CAAC;QACD,MAAM,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/engine.d.ts
ADDED
|
@@ -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"}
|
package/dist/expand.d.ts
ADDED
|
@@ -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"}
|
package/dist/facade.d.ts
ADDED
|
@@ -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>;
|