@better-fs-tools/read 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,7 +107,7 @@ read({ path, offset, limit }, ctx)
107
107
  │ ├─ on a miss: suggest one bounded listing, names only
108
108
  │ └─ on a directory .... a directory converter lists it, else NOT_A_FILE
109
109
  │ authorize ............... host policy on the open file, before the core reads content
110
- │ sample + classifiers .... text, image, PDF, notebook, binary, ...
110
+ │ sample + classifiers .... text, image, PDF, Office, binary, ...
111
111
  │ converters .............. other formats to text or media parts
112
112
  │ scan .................... lines, clamping, view bytes, token budget, scan limit
113
113
  │ verify .................. size check and change detection
@@ -343,7 +343,9 @@ While a prompt waits, the file stays open. If the file changes during the wait,
343
343
 
344
344
  `classifiers` is an ordered list. Each classifier sees a bounded sample of the file (`limits.sampleBytes`). The first classifier with an opinion wins.
345
345
 
346
- The default chain detects images, PDFs, Office documents, notebooks, binary content, and text encodings. It returns these `unsupported` codes: `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `NOTEBOOK`, `BINARY`, and `UNKNOWN_ENCODING`. Each refusal has a note with code `unsupported-<code>`, for example `unsupported-pdf`.
346
+ The default chain detects images, PDFs, Office documents, binary content, and text encodings. It returns these `unsupported` codes: `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `BINARY`, and `UNKNOWN_ENCODING`. Each refusal has a note with code `unsupported-<code>`, for example `unsupported-pdf`.
347
+
348
+ A Jupyter notebook is JSON, so the default chain reads it as text. `notebookClassifier()` is exported but not in the chain. Put it first, as `classifiers: [notebookClassifier(), ...defaultClassifiers()]`, to refuse notebooks with code `NOTEBOOK`, or to convert them with `notebookConverter()`. The write tools use the same classifiers, so the same list makes them refuse a notebook too.
347
349
 
348
350
  To detect by extension, or to change a refusal message:
349
351
 
@@ -370,13 +372,15 @@ A classifier only decides what a file is. A converter changes the content.
370
372
 
371
373
  ## File conversion, images, and directories
372
374
 
373
- `converters` is a list. It runs after classification. The first file converter whose `accepts` returns `true` handles the file. The first directory converter handles directories. With no converters, text is read, other formats are `unsupported`, and a directory gives `NOT_A_FILE`.
375
+ `converters` is a list. It runs after classification. The first file converter whose `accepts` returns `true` handles the file. The first directory converter handles directories. With no converters, text is read, other formats are `unsupported`, and a directory gives `NOT_A_FILE`. The core has no converters by default. The Pi and AI SDK hosts add `imageConverter()` when you pass no `converters`.
374
376
 
375
377
  ```ts
376
378
  import { createNodeReadTool } from "@better-fs-tools/node";
377
379
  import {
380
+ defaultClassifiers,
378
381
  directoryListing,
379
382
  imageConverter,
383
+ notebookClassifier,
380
384
  notebookConverter,
381
385
  textConverter,
382
386
  } from "@better-fs-tools/read";
@@ -385,6 +389,9 @@ import {
385
389
  declare function pdfToText(source: AsyncIterable<Uint8Array>): AsyncIterable<string>;
386
390
 
387
391
  export const read = createNodeReadTool({
392
+ // The default chain reads a notebook as JSON text. notebookClassifier()
393
+ // marks it NOTEBOOK, so notebookConverter() can take it.
394
+ classifiers: [notebookClassifier(), ...defaultClassifiers()],
388
395
  converters: [
389
396
  imageConverter(),
390
397
  notebookConverter({ outputs: false }),
@@ -400,12 +407,12 @@ export const read = createNodeReadTool({
400
407
  });
401
408
  ```
402
409
 
403
- | Converter | Result |
404
- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
405
- | `imageConverter({ transform })` | Accepts code `IMAGE`. Gives a `media` result with the image as one content part. `transform` can resize it. |
406
- | `notebookConverter({ outputs })` | Accepts code `NOTEBOOK`. Gives the cells as text. `outputs` defaults to `true`. Broken notebook JSON is refused as `INVALID_NOTEBOOK`. |
407
- | `textConverter({ id, accepts, mimeType, run })` | Builds a text converter from a stream function, for example `pdftotext`, `pandoc`, or `markitdown` in your host. |
408
- | `directoryListing({ trailingSlash, sort })` | A directory becomes text, one entry for each line. `sort` is `"name"` (the default) or `"type-then-name"`. |
410
+ | Converter | Result |
411
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
412
+ | `imageConverter({ transform })` | Accepts code `IMAGE`. Gives a `media` result with the image as one content part. `transform` can resize it. |
413
+ | `notebookConverter({ outputs })` | Accepts code `NOTEBOOK`, so it needs `notebookClassifier()`. Gives the cells as text. `outputs` defaults to `true`. Broken notebook JSON is refused as `INVALID_NOTEBOOK`. |
414
+ | `textConverter({ id, accepts, mimeType, run })` | Builds a text converter from a stream function, for example `pdftotext`, `pandoc`, or `markitdown` in your host. |
415
+ | `directoryListing({ trailingSlash, sort })` | A directory becomes text, one entry for each line. `sort` is `"name"` (the default) or `"type-then-name"`. |
409
416
 
410
417
  A converter returns one of these outcomes:
411
418
 
@@ -428,7 +435,7 @@ Conversion runs again on each continuation read. Cache inside your converter if
428
435
  | Limit | Default | Meaning |
429
436
  | --------------------- | ------- | ----------------------------------------------------- |
430
437
  | `maxLines` | 2,000 | Lines in the view. Also the ceiling for `limit`. |
431
- | `maxViewBytes` | 128 KiB | UTF-8 bytes of source text in the view |
438
+ | `maxViewBytes` | 50 KiB | UTF-8 bytes of source text in the view |
432
439
  | `maxCharsPerLine` | 2,000 | Characters in one line before it is clamped |
433
440
  | `maxScanBytes` | 64 MiB | Bytes scanned before totals and content identity stop |
434
441
  | `sampleBytes` | 8 KiB | Bytes given to classifiers. At most `maxScanBytes`. |
@@ -548,7 +555,7 @@ export const read = createNodeReadTool({
548
555
  gutter: hashlineGutter({ width: 2 }), // "12:a3|text"
549
556
  header: fileHashHeader(), // "file-hash: sha256:..."
550
557
  footer: eofFooter((n) => `(End of file - total ${n} lines)`),
551
- notes: (note) => (note.code === "weak-identity" ? null : note),
558
+ notes: (note) => (note.severity === "info" ? null : note),
552
559
  }),
553
560
  });
554
561
  ```
@@ -568,7 +575,7 @@ import { opencodeFormat } from "@better-fs-tools/read/formats";
568
575
  export const read = createNodeReadTool({ formatter: opencodeFormat() });
569
576
  ```
570
577
 
571
- A custom formatter implements `format(outcome, ctx)`. It is synchronous and pure over the outcome. `ctx` has the `digest`, the `limits`, the `call`, and a `mode`. The mode is `"model"` for model output and `"view"` for the body only, with no notes, header, or footer. The Pi adapter uses `"view"` for its truncation details. If your formatter returns parts in `"view"` mode, Pi leaves those details out. A formatter sees only the finished outcome. It cannot read the file. If the formatter throws or returns neither a string nor an array, the call still succeeds: the core adds an `extension-failed` warning and formats the result with `lineNumberFormatter()`.
578
+ A custom formatter implements `format(outcome, ctx)`. It is synchronous and pure over the outcome. `ctx` has the `digest`, the `limits`, the `call`, and a `mode`. The mode is `"model"` for model output and `"view"` for the body only, with no notes, header, or footer. The Pi adapter uses `"view"` for its truncation details. If your formatter returns parts in `"view"` mode, Pi leaves those details out. A formatter sees only the finished outcome. It cannot read the file. If the formatter throws or returns neither a string nor an array, the call still succeeds: the core formats the result with `lineNumberFormatter()` and adds no note.
572
579
 
573
580
  ## Messages and notes
574
581
 
@@ -683,7 +690,7 @@ Your dependencies can narrow access. They cannot widen it.
683
690
  - The filesystem is the only place that grants access. It checks roots, deny roots, the realpath, and the type in one `open()` call. `resolve` runs before it and `authorize` runs after it.
684
691
  - Each read opens at most one file. A resolver only changes the path that goes into that open.
685
692
  - `nodeFileSystem` opens files with `O_NOFOLLOW | O_NONBLOCK` and checks the type first. It refuses directories, FIFOs, sockets, and devices before any content read. It refuses `/dev`, `/proc`, and `/sys` before it touches the filesystem.
686
- - The core reads no content byte before `authorize` allows it, so no content reaches a classifier, a converter, or the model. `@better-fs-tools/cloudflare-shell` and `@better-fs-tools/just-bash` (a result with a `buffered-backend` note) have already fetched the whole file in `open()`. `@better-fs-tools/cloudflare-computer` has started a `readFile` stream but read no chunk. If a denied read must not reach the backend, use a deny root, or leave the path out of the filesystem's allowed roots.
693
+ - The core reads no content byte before `authorize` allows it, so no content reaches a classifier, a converter, or the model. `@better-fs-tools/cloudflare-shell` and `@better-fs-tools/just-bash` have already fetched the whole file in `open()`. `@better-fs-tools/cloudflare-computer` has started a `readFile` stream but read no chunk. If a denied read must not reach the backend, use a deny root, or leave the path out of the filesystem's allowed roots.
687
694
  - The scan is bounded. Converters get a capped stream from the open file, never a path.
688
695
  - Change detection runs after the scan and after conversion. A file that changed returns `CHANGED_DURING_READ`.
689
696
  - A suggested name is never opened.
@@ -694,11 +701,11 @@ Your dependencies can narrow access. They cannot widen it.
694
701
 
695
702
  Each adapter owns the signature, abort forwarding, and the mapping from `result.content` to the framework's content types. The adapters accept every core dependency and pass it through.
696
703
 
697
- | Package | Factory | Notes |
698
- | ---------------------------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
699
- | [`@better-fs-tools/ai-sdk`](https://www.npmjs.com/package/@better-fs-tools/ai-sdk) | `createAiSdkReadTool(options)` | `execute` returns the whole `ReadResult`. `toModelOutput` maps media parts to AI SDK `file` parts. You pass `fs`. |
700
- | [`@better-fs-tools/pi`](https://www.npmjs.com/package/@better-fs-tools/pi) | `createPiReadTool(options)` | Each call is confined to Pi's `ctx.cwd`. `fs`, `cwd`, and `allowedRoots` are refused. The `pi.extensions` entry registers `read`, `edit`, `write`, and `apply_patch`. |
701
- | [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node) | `createNodeReadTool(options?)` | The core with Node defaults, for your own tool. |
704
+ | Package | Factory | Notes |
705
+ | ---------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
706
+ | [`@better-fs-tools/ai-sdk`](https://www.npmjs.com/package/@better-fs-tools/ai-sdk) | `createAiSdkReadTool(options)` | `execute` returns the whole `ReadResult`. `toModelOutput` maps media parts to AI SDK `file` parts. You pass `fs`. |
707
+ | [`@better-fs-tools/pi`](https://www.npmjs.com/package/@better-fs-tools/pi) | `createPiReadTool(options)` | Each call is confined to Pi's `ctx.cwd`. `fs`, `cwd`, and `allowedRoots` are refused. The `pi.extensions` entry registers `read`, `edit`, and `write`. |
708
+ | [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node) | `createNodeReadTool(options?)` | The core with Node defaults, for your own tool. |
702
709
 
703
710
  Cloudflare Agents hosts use `@better-fs-tools/ai-sdk` with `cloudflareShellFileSystem` from [`@better-fs-tools/cloudflare-shell`](https://www.npmjs.com/package/@better-fs-tools/cloudflare-shell). [`@better-fs-tools/cloudflare-computer`](https://www.npmjs.com/package/@better-fs-tools/cloudflare-computer) and [`@better-fs-tools/just-bash`](https://www.npmjs.com/package/@better-fs-tools/just-bash) are filesystems. Use them with `createReadTool()` or with an adapter.
704
711
 
@@ -8,8 +8,9 @@ export { officeClassifier } from "./office.ts";
8
8
  export { pdfClassifier } from "./pdf.ts";
9
9
  export { utf8Classifier } from "./utf8.ts";
10
10
  /**
11
- * The built-in chain in evaluation order: image, pdf, office, notebook,
12
- * binary, utf8. The first classifier with an opinion wins. utf8Classifier
13
- * runs last and is the one that returns text.
11
+ * The built-in chain in evaluation order: image, pdf, office, binary, utf8.
12
+ * The first classifier with an opinion wins. utf8Classifier runs last and is
13
+ * the one that returns text. A notebook is JSON text here. Put
14
+ * notebookClassifier() before the chain to refuse or convert notebooks.
14
15
  */
15
16
  export declare function defaultClassifiers(options?: DefaultClassifierOptions): Classifier[];
@@ -1,6 +1,5 @@
1
1
  import { binaryClassifier } from "./binary.js";
2
2
  import { imageClassifier } from "./image.js";
3
- import { notebookClassifier } from "./notebook.js";
4
3
  import { officeClassifier } from "./office.js";
5
4
  import { pdfClassifier } from "./pdf.js";
6
5
  import { utf8Classifier } from "./utf8.js";
@@ -12,9 +11,10 @@ export { officeClassifier } from "./office.js";
12
11
  export { pdfClassifier } from "./pdf.js";
13
12
  export { utf8Classifier } from "./utf8.js";
14
13
  /**
15
- * The built-in chain in evaluation order: image, pdf, office, notebook,
16
- * binary, utf8. The first classifier with an opinion wins. utf8Classifier
17
- * runs last and is the one that returns text.
14
+ * The built-in chain in evaluation order: image, pdf, office, binary, utf8.
15
+ * The first classifier with an opinion wins. utf8Classifier runs last and is
16
+ * the one that returns text. A notebook is JSON text here. Put
17
+ * notebookClassifier() before the chain to refuse or convert notebooks.
18
18
  */
19
19
  export function defaultClassifiers(options = {}) {
20
20
  const notes = options.notes ?? {};
@@ -22,7 +22,6 @@ export function defaultClassifiers(options = {}) {
22
22
  imageClassifier(withNote(notes.IMAGE)),
23
23
  pdfClassifier(withNote(notes.PDF)),
24
24
  officeClassifier(withNote(notes.OFFICE_DOCUMENT)),
25
- notebookClassifier(withNote(notes.NOTEBOOK)),
26
25
  binaryClassifier(withNote(notes.BINARY)),
27
26
  utf8Classifier(withNote(notes.UNKNOWN_ENCODING)),
28
27
  ];
@@ -1,7 +1,7 @@
1
1
  export interface ReadLimits {
2
2
  /** Lines in the view. Default 2_000. */
3
3
  readonly maxLines: number;
4
- /** UTF-8 bytes of source text in the view. Default 128 KiB. */
4
+ /** UTF-8 bytes of source text in the view. Default 50 KiB (51 200). */
5
5
  readonly maxViewBytes: number;
6
6
  /** Characters per line before clamping. Default 2_000. */
7
7
  readonly maxCharsPerLine: number;
@@ -107,14 +107,4 @@ export interface ReadMessageCatalog extends ToolMessages {
107
107
  viewModified(c: {
108
108
  hook: string;
109
109
  }): string;
110
- weakIdentity(c: {
111
- backend: string;
112
- }): string;
113
- bufferedBackend(c: {
114
- backend: string;
115
- }): string;
116
- /** The formatter threw or returned neither a string nor an array. */
117
- formatterFailed(c: {
118
- formatter: string;
119
- }): string;
120
110
  }
@@ -3,7 +3,6 @@ import { ConvertSource } from "./convert-source.js";
3
3
  import { checkConvertOutcome, firstMediaType, mediaBytes, refusedOutcome, scanConvertedText, tooLarge, } from "./converted.js";
4
4
  import { AbortReadError, raceAbort } from "./cursor.js";
5
5
  import { extensionId } from "./extension-error.js";
6
- import { capabilityNotes } from "./notes.js";
7
6
  import { buildObservation } from "./observation.js";
8
7
  import { ReadStop } from "./outcomes.js";
9
8
  import { textOutcome } from "./text-outcome.js";
@@ -111,7 +110,7 @@ async function drainAndVerify(source, handle, input, throwIfSourceFailed) {
111
110
  await verifyHandle(handle, deps.messages, request, file, scope);
112
111
  }
113
112
  function mediaOutcome(input, classification, parts, notes, source) {
114
- const { deps, fs, request, file, converter } = input;
113
+ const { deps, request, file, converter } = input;
115
114
  return {
116
115
  tool: "read",
117
116
  status: "media",
@@ -128,6 +127,6 @@ function mediaOutcome(input, classification, parts, notes, source) {
128
127
  // Media is not source text, so it cannot back a text edit.
129
128
  wholeFileVisible: false,
130
129
  }),
131
- notes: [...notes, ...capabilityNotes(fs, deps.messages)],
130
+ notes,
132
131
  };
133
132
  }
@@ -1,5 +1,4 @@
1
1
  import { lineNumberFormatter } from "../formatters/index.js";
2
- import { extensionId } from "./extension-error.js";
3
2
  export function formatResult(deps, call, outcome) {
4
3
  const ctx = {
5
4
  digest: deps.digest,
@@ -14,20 +13,7 @@ export function formatResult(deps, call, outcome) {
14
13
  catch { }
15
14
  if (content !== null)
16
15
  return { ...outcome, content };
17
- const id = extensionId(deps.formatter);
18
- const failed = {
19
- ...outcome,
20
- notes: [
21
- ...outcome.notes,
22
- {
23
- code: "extension-failed",
24
- severity: "warning",
25
- message: deps.messages.formatterFailed({ formatter: id ?? "formatter" }),
26
- data: id === null ? { extension: "formatter" } : { extension: "formatter", id },
27
- },
28
- ],
29
- };
30
- return { ...failed, content: toContent(lineNumberFormatter().format(failed, ctx)) ?? [] };
16
+ return { ...outcome, content: toContent(lineNumberFormatter().format(outcome, ctx)) ?? [] };
31
17
  }
32
18
  function toContent(output) {
33
19
  if (typeof output === "string")
@@ -1,7 +1,7 @@
1
1
  /** The default read limits. They are not ceilings: a host may raise any of them. */
2
2
  export const defaultReadLimits = Object.freeze({
3
3
  maxLines: 2_000,
4
- maxViewBytes: 128 * 1_024,
4
+ maxViewBytes: 50 * 1_024,
5
5
  maxCharsPerLine: 2_000,
6
6
  maxScanBytes: 64 * 1_024 * 1_024,
7
7
  sampleBytes: 8_192,
@@ -6,14 +6,14 @@ const catalog = {
6
6
  offsetUnreached: ({ request, reachedLine, retry }) => `Scanning stopped before line ${request.offset} was reached. Retry from line ${reachedLine} with ${retry}.`,
7
7
  lineClamped: ({ lines, total, maxChars }) => `Source line${total === 1 ? "" : "s"} ${lines.join(", ")}${total > lines.length ? ", …" : ""} exceeded ${maxChars} characters and ${total === 1 ? "was" : "were"} clamped.`,
8
8
  limitClamped: ({ requested, max }) => `The requested line count of ${requested} is over the maximum, so at most ${max} lines are shown.`,
9
- scanLimit: ({ maxScanBytes }) => `Scanning stopped at ${maxScanBytes} bytes; total lines and raw content identity are unknown.`,
9
+ scanLimit: ({ maxScanBytes }) => `Scanning stopped at ${maxScanBytes} bytes, so the total line count is unknown.`,
10
10
  empty: ({ path }) => `${path} is empty (0 bytes); no retry is needed.`,
11
11
  notFound: ({ request, suggestions }) => suggestions.length === 0
12
12
  ? `${request.path} was not found.`
13
13
  : `${request.path} was not found. Nearby names: ${quoted(suggestions)}.`,
14
14
  pathRepaired: ({ from, to }) => `The requested filename ${JSON.stringify(from)} was repaired to the unique Unicode-equivalent path ${JSON.stringify(to)}.`,
15
- notAFile: ({ request, kind }) => `${request.path} is a ${kindLabel(kind)}; directories, FIFOs, sockets, and devices are refused before any content read.`,
16
- dangerousPath: ({ request, detail }) => `${request.path} belongs to a refused policy class${detail === null ? "" : ` (${detail})`}.`,
15
+ notAFile: ({ request, kind }) => `${request.path} is a ${kindLabel(kind)}.`,
16
+ dangerousPath: ({ request, detail }) => `${request.path} is refused by policy${detail === null ? "" : ` (${detail})`}. Choose another path.`,
17
17
  outsideAllowedRoots: ({ request }) => `${request.path} is outside every configured allowed root.`,
18
18
  permissionDenied: ({ request }) => `Permission was denied for ${request.path}.`,
19
19
  denied: ({ path, detail }) => `${path} was refused by policy${detail === null ? "" : ` (${detail})`}.`,
@@ -26,15 +26,12 @@ const catalog = {
26
26
  : `The media converted from ${request.path} is larger than the ${limit}-byte media limit, so it was not returned.`;
27
27
  },
28
28
  changedDuringRead: ({ request, retry }) => `${request.path} changed while it was being read. Read it again with ${retry} before relying on this result.`,
29
- aborted: ({ phase }) => `The read was aborted during the ${phase} phase; no observation was recorded.`,
29
+ aborted: ({ phase }) => `The read was aborted during the ${phase} phase, so nothing was read.`,
30
30
  invalidInput: ({ detail }) => `The read input was rejected: ${detail}`,
31
31
  extensionFailed: ({ request, extension, phase }) => `The ${extension} extension failed during the ${phase} phase while reading ${request.path}; this is a host problem rather than a property of the file.`,
32
32
  ioError: ({ request }) => `The backend failed while reading ${request === null ? "the requested path" : request.path}.`,
33
- unsupportedBackend: ({ request, detail }) => `No classifier had an opinion about ${request.path}${detail === null ? "" : ` (${detail})`}; this is a configuration problem rather than a property of the file.`,
33
+ unsupportedBackend: ({ request, detail }) => `This host cannot read ${request.path}${detail === null ? "" : ` (${detail})`}. Do not retry.`,
34
34
  viewModified: ({ hook }) => `The ${hook} hook changed this view, so it is not the exact file text.`,
35
- weakIdentity: ({ backend }) => `The ${backend} backend has no stable identity, so this observation cannot back a write precondition.`,
36
- bufferedBackend: ({ backend }) => `The ${backend} backend buffers whole objects instead of streaming them.`,
37
- formatterFailed: ({ formatter }) => `The ${formatter} formatter failed, so the default formatter formatted this result.`,
38
35
  };
39
36
  /** The default wording of every read message. */
40
37
  export const defaultReadMessages = Object.freeze(catalog);
@@ -1,4 +1,3 @@
1
- import type { FileSystem } from "@better-fs-tools/fs";
2
1
  import type { ReadRequest } from "../contract/input.ts";
3
2
  import type { ReadLimits } from "../contract/limits.ts";
4
3
  import type { ReadMessageCatalog } from "../contract/messages.ts";
@@ -13,4 +12,3 @@ export interface ViewNoteContext {
13
12
  }
14
13
  export declare function buildViewNotes(context: ViewNoteContext): ReadNote[];
15
14
  export declare function emptyNote(messages: Readonly<ReadMessageCatalog>, path: string): ReadNote;
16
- export declare function capabilityNotes(fs: FileSystem, messages: Readonly<ReadMessageCatalog>): ReadNote[];
@@ -89,26 +89,6 @@ export function buildViewNotes(context) {
89
89
  export function emptyNote(messages, path) {
90
90
  return { code: "empty", severity: "info", message: messages.empty({ path }) };
91
91
  }
92
- export function capabilityNotes(fs, messages) {
93
- const notes = [];
94
- if (!fs.capabilities.identity) {
95
- notes.push({
96
- code: "weak-identity",
97
- severity: "info",
98
- message: messages.weakIdentity({ backend: fs.id }),
99
- data: { backend: fs.id },
100
- });
101
- }
102
- if (!fs.capabilities.streaming) {
103
- notes.push({
104
- code: "buffered-backend",
105
- severity: "info",
106
- message: messages.bufferedBackend({ backend: fs.id }),
107
- data: { backend: fs.id },
108
- });
109
- }
110
- return notes;
111
- }
112
92
  function retryFrom(request, offset) {
113
93
  return { path: request.path, offset, limit: request.limit };
114
94
  }
@@ -1,4 +1,4 @@
1
- import { buildViewNotes, capabilityNotes, emptyNote } from "./notes.js";
1
+ import { buildViewNotes, emptyNote } from "./notes.js";
2
2
  import { buildObservation } from "./observation.js";
3
3
  export function textOutcome(input) {
4
4
  const { deps, fs, request, file, classification, conversion, scan } = input;
@@ -67,7 +67,6 @@ export function textOutcome(input) {
67
67
  ...(conversion === null && reachedEof && scannedBytes === 0
68
68
  ? [emptyNote(deps.messages, file.displayPath)]
69
69
  : []),
70
- ...(fs === null ? [] : capabilityNotes(fs, deps.messages)),
71
70
  ],
72
71
  };
73
72
  }
@@ -113,10 +113,11 @@ handle.verify() did the opened object change?
113
113
  handle.close() always, from a finally block
114
114
  ```
115
115
 
116
- Two capabilities remain, and each one decides one thing:
116
+ One capability remains. It adds no note to the result, because the model cannot act on it:
117
117
 
118
- - `streaming: false` adds a `buffered-backend` note.
119
- - `identity: false` adds a `weak-identity` note, because the observation cannot back a write precondition.
118
+ - `identity: false` sets `file.identity` to `null`. The write tools then judge freshness by the content hash only.
119
+
120
+ The read core reads `bytes()` the same way whether it yields one chunk or many, so a backend does not report which.
120
121
 
121
122
  An optional `list()` is the list capability. Without it, there are no suggestions and no directory listings.
122
123
 
@@ -214,7 +215,7 @@ The core makes the other failures itself:
214
215
  - `IO_ERROR` when a filesystem method or the byte stream throws.
215
216
  - `EXTENSION_FAILED` when host code throws or breaks a rule. `data.extension` names the dependency (`fs`, `state`, `resolve`, `suggest`, `authorize`, `converters`, `budget`, or `hooks`), `data.phase` names the stage, and `data.id` names the extension object when it has one.
216
217
 
217
- A formatter that throws, or returns neither a string nor an array, does not reject the call. The core adds an `extension-failed` warning with `data: { extension: "formatter", id? }` and formats the result with `lineNumberFormatter()`. The status stays as it was.
218
+ A formatter that throws, or returns neither a string nor an array, does not reject the call. The core formats the result with `lineNumberFormatter()` and adds no note. The status stays as it was.
218
219
 
219
220
  ## Adapters
220
221
 
@@ -97,12 +97,13 @@ Field notes:
97
97
 
98
98
  `code` comes from the classifier, a converter refusal, or the core:
99
99
 
100
- | Code | Source |
101
- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
102
- | `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `NOTEBOOK`, `BINARY`, `UNKNOWN_ENCODING` | The default classifiers |
103
- | `INVALID_NOTEBOOK` | `notebookConverter()` on broken notebook JSON |
104
- | `TOO_LARGE` | The core: converter input over `maxConvertBytes`, or media over `maxMediaBytes`. A backend ceiling gives `status: "error"` with `TOO_LARGE` instead |
105
- | any other string | Your own classifier or converter |
100
+ | Code | Source |
101
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `BINARY`, `UNKNOWN_ENCODING` | The default classifiers |
103
+ | `NOTEBOOK` | `notebookClassifier()`, which is not in the default chain |
104
+ | `INVALID_NOTEBOOK` | `notebookConverter()` on broken notebook JSON |
105
+ | `TOO_LARGE` | The core: converter input over `maxConvertBytes`, or media over `maxMediaBytes`. A backend ceiling gives `status: "error"` with `TOO_LARGE` instead |
106
+ | any other string | Your own classifier or converter |
106
107
 
107
108
  ## `error`
108
109
 
@@ -162,8 +163,6 @@ Notes from the core:
162
163
  | `scan-limit` | warning | none | `{ maxScanBytes }` |
163
164
  | `offset-past-eof` | info | the last line | `{ totalLines }` |
164
165
  | `empty` | info | none | none |
165
- | `weak-identity` | info | none | `{ backend }` |
166
- | `buffered-backend` | info | none | `{ backend }` |
167
166
  | `not-found` | warning | none | `{ suggestions?, entriesTruncated?, detail?, cause? }` |
168
167
  | `not-a-file` | warning | none | `{ kind, detail?, cause? }` |
169
168
  | `dangerous-path`, `outside-allowed-roots`, `permission-denied`, `denied`, `unsupported-backend`, `io-error` | warning | none | `{ detail?, cause? }` from the filesystem. A thrown error gives `io-error` with `{ detail }`. |
@@ -172,20 +171,21 @@ Notes from the core:
172
171
  | `changed-during-read` | warning | the same range | none |
173
172
  | `aborted` | warning | none | `{ phase }` |
174
173
  | `invalid-input` | warning | none | `{ path }` when the input had a string path |
175
- | `extension-failed` | warning | none | `{ extension, phase, id? }`. A formatter failure gives `{ extension: "formatter", id? }`. |
174
+ | `extension-failed` | warning | none | `{ extension, phase, id? }` |
176
175
  | `view-modified` | info | none | `{ hook }` |
177
176
 
178
177
  Notes from the built-in helpers:
179
178
 
180
- | `code` | Severity | From | `data` |
181
- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- | ------------------------------- |
182
- | `utf8-bom` | info | `utf8Classifier` | none |
183
- | `unsupported-image`, `unsupported-pdf`, `unsupported-office-document`, `unsupported-notebook`, `unsupported-binary`, `unsupported-unknown-encoding` | warning | the default classifiers | as set by a `NoteOverride` |
184
- | `path-repaired` | warning | `unicodeRepair()` | `{ from, to }` |
185
- | `invalid-notebook` | warning | `notebookConverter()` | none |
186
- | `directory-truncated` | warning | `directoryListing()` | `{ maxDirectoryEntries }` |
187
- | `empty-directory` | info | `directoryListing()` | none |
188
- | `repeat-read` | info | `repeatReadGuard()` | `{ observationId, observedAt }` |
179
+ | `code` | Severity | From | `data` |
180
+ | --------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- | ------------------------------- |
181
+ | `utf8-bom` | info | `utf8Classifier` | none |
182
+ | `unsupported-image`, `unsupported-pdf`, `unsupported-office-document`, `unsupported-binary`, `unsupported-unknown-encoding` | warning | the default classifiers | as set by a `NoteOverride` |
183
+ | `unsupported-notebook` | warning | `notebookClassifier()` | as set by a `NoteOverride` |
184
+ | `path-repaired` | warning | `unicodeRepair()` | `{ from, to }` |
185
+ | `invalid-notebook` | warning | `notebookConverter()` | none |
186
+ | `directory-truncated` | warning | `directoryListing()` | `{ maxDirectoryEntries }` |
187
+ | `empty-directory` | info | `directoryListing()` | none |
188
+ | `repeat-read` | info | `repeatReadGuard()` | `{ observationId, observedAt }` |
189
189
 
190
190
  `directoryListing()` also has a `list-failed` refusal for a listing that failed. The core reports the listing's own error first, so a read with the built-in converter gives that error, for example `DENIED` or `IO_ERROR`.
191
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-fs-tools/read",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A bounded file reader for TypeScript agent tools.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@better-fs-tools/fs": "^0.1.0"
49
+ "@better-fs-tools/fs": "^0.2.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "typebox": "^1.3.16"