@better-fs-tools/read 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +799 -2
  3. package/dist/authorize/ask-user.d.ts +7 -0
  4. package/dist/authorize/ask-user.js +25 -0
  5. package/dist/authorize/authorizers.d.ts +7 -0
  6. package/dist/authorize/authorizers.js +34 -0
  7. package/dist/authorize/deny-paths.d.ts +7 -0
  8. package/dist/authorize/deny-paths.js +25 -0
  9. package/dist/authorize/glob.d.ts +7 -0
  10. package/dist/authorize/glob.js +46 -0
  11. package/dist/authorize/index.d.ts +5 -0
  12. package/dist/authorize/index.js +5 -0
  13. package/dist/authorize/shared.d.ts +9 -0
  14. package/dist/authorize/shared.js +12 -0
  15. package/dist/authorize/size-ceiling.d.ts +10 -0
  16. package/dist/authorize/size-ceiling.js +34 -0
  17. package/dist/budget/chars-per-token.d.ts +9 -0
  18. package/dist/budget/chars-per-token.js +15 -0
  19. package/dist/budget/index.d.ts +2 -0
  20. package/dist/budget/index.js +1 -0
  21. package/dist/classifiers/binary.d.ts +5 -0
  22. package/dist/classifiers/binary.js +13 -0
  23. package/dist/classifiers/extension.d.ts +17 -0
  24. package/dist/classifiers/extension.js +45 -0
  25. package/dist/classifiers/image.d.ts +5 -0
  26. package/dist/classifiers/image.js +22 -0
  27. package/dist/classifiers/index.d.ts +15 -0
  28. package/dist/classifiers/index.js +32 -0
  29. package/dist/classifiers/notebook.d.ts +5 -0
  30. package/dist/classifiers/notebook.js +14 -0
  31. package/dist/classifiers/office.d.ts +5 -0
  32. package/dist/classifiers/office.js +30 -0
  33. package/dist/classifiers/pdf.d.ts +4 -0
  34. package/dist/classifiers/pdf.js +8 -0
  35. package/dist/classifiers/shared.d.ts +17 -0
  36. package/dist/classifiers/shared.js +57 -0
  37. package/dist/classifiers/utf8.d.ts +8 -0
  38. package/dist/classifiers/utf8.js +61 -0
  39. package/dist/contract/base.d.ts +126 -0
  40. package/dist/contract/base.js +0 -0
  41. package/dist/contract/classify.d.ts +50 -0
  42. package/dist/contract/classify.js +0 -0
  43. package/dist/contract/context.d.ts +13 -0
  44. package/dist/contract/context.js +0 -0
  45. package/dist/contract/deps.d.ts +60 -0
  46. package/dist/contract/deps.js +0 -0
  47. package/dist/contract/digest.d.ts +13 -0
  48. package/dist/contract/digest.js +0 -0
  49. package/dist/contract/extensions.d.ts +115 -0
  50. package/dist/contract/extensions.js +0 -0
  51. package/dist/contract/format.d.ts +16 -0
  52. package/dist/contract/format.js +0 -0
  53. package/dist/contract/input.d.ts +16 -0
  54. package/dist/contract/input.js +0 -0
  55. package/dist/contract/json.d.ts +6 -0
  56. package/dist/contract/json.js +0 -0
  57. package/dist/contract/limits.d.ts +20 -0
  58. package/dist/contract/limits.js +0 -0
  59. package/dist/contract/messages.d.ts +120 -0
  60. package/dist/contract/messages.js +0 -0
  61. package/dist/contract/result.d.ts +153 -0
  62. package/dist/contract/result.js +0 -0
  63. package/dist/contract/state.d.ts +36 -0
  64. package/dist/contract/state.js +0 -0
  65. package/dist/converters/directory-listing.d.ts +13 -0
  66. package/dist/converters/directory-listing.js +63 -0
  67. package/dist/converters/image.d.ts +7 -0
  68. package/dist/converters/image.js +25 -0
  69. package/dist/converters/index.d.ts +8 -0
  70. package/dist/converters/index.js +4 -0
  71. package/dist/converters/notebook.d.ts +12 -0
  72. package/dist/converters/notebook.js +106 -0
  73. package/dist/converters/shared.d.ts +5 -0
  74. package/dist/converters/shared.js +18 -0
  75. package/dist/converters/text.d.ts +12 -0
  76. package/dist/converters/text.js +24 -0
  77. package/dist/core/authorize.d.ts +8 -0
  78. package/dist/core/authorize.js +76 -0
  79. package/dist/core/budget.d.ts +7 -0
  80. package/dist/core/budget.js +23 -0
  81. package/dist/core/call-scope.d.ts +36 -0
  82. package/dist/core/call-scope.js +112 -0
  83. package/dist/core/classify.d.ts +9 -0
  84. package/dist/core/classify.js +27 -0
  85. package/dist/core/convert-source.d.ts +29 -0
  86. package/dist/core/convert-source.js +85 -0
  87. package/dist/core/convert.d.ts +23 -0
  88. package/dist/core/convert.js +133 -0
  89. package/dist/core/converted.d.ts +18 -0
  90. package/dist/core/converted.js +111 -0
  91. package/dist/core/create-read-tool.d.ts +7 -0
  92. package/dist/core/create-read-tool.js +18 -0
  93. package/dist/core/cursor.d.ts +14 -0
  94. package/dist/core/cursor.js +93 -0
  95. package/dist/core/deps.d.ts +6 -0
  96. package/dist/core/deps.js +121 -0
  97. package/dist/core/directory.d.ts +16 -0
  98. package/dist/core/directory.js +113 -0
  99. package/dist/core/extension-error.d.ts +9 -0
  100. package/dist/core/extension-error.js +18 -0
  101. package/dist/core/format.d.ts +8 -0
  102. package/dist/core/format.js +43 -0
  103. package/dist/core/hooks.d.ts +3 -0
  104. package/dist/core/hooks.js +140 -0
  105. package/dist/core/input.d.ts +10 -0
  106. package/dist/core/input.js +45 -0
  107. package/dist/core/limits.d.ts +9 -0
  108. package/dist/core/limits.js +43 -0
  109. package/dist/core/messages.d.ts +8 -0
  110. package/dist/core/messages.js +81 -0
  111. package/dist/core/notes.d.ts +16 -0
  112. package/dist/core/notes.js +114 -0
  113. package/dist/core/observation.d.ts +11 -0
  114. package/dist/core/observation.js +51 -0
  115. package/dist/core/open.d.ts +16 -0
  116. package/dist/core/open.js +33 -0
  117. package/dist/core/outcomes.d.ts +27 -0
  118. package/dist/core/outcomes.js +172 -0
  119. package/dist/core/pipeline.d.ts +4 -0
  120. package/dist/core/pipeline.js +175 -0
  121. package/dist/core/record.d.ts +3 -0
  122. package/dist/core/record.js +37 -0
  123. package/dist/core/resolve.d.ts +15 -0
  124. package/dist/core/resolve.js +43 -0
  125. package/dist/core/same.d.ts +2 -0
  126. package/dist/core/same.js +22 -0
  127. package/dist/core/sample.d.ts +6 -0
  128. package/dist/core/sample.js +24 -0
  129. package/dist/core/scan.d.ts +26 -0
  130. package/dist/core/scan.js +95 -0
  131. package/dist/core/scanner.d.ts +29 -0
  132. package/dist/core/scanner.js +124 -0
  133. package/dist/core/suggest.d.ts +6 -0
  134. package/dist/core/suggest.js +41 -0
  135. package/dist/core/text-outcome.d.ts +19 -0
  136. package/dist/core/text-outcome.js +73 -0
  137. package/dist/core/verify.d.ts +7 -0
  138. package/dist/core/verify.js +15 -0
  139. package/dist/digest/sha256.d.ts +7 -0
  140. package/dist/digest/sha256.js +131 -0
  141. package/dist/formats/deep-agents.d.ts +7 -0
  142. package/dist/formats/deep-agents.js +23 -0
  143. package/dist/formats/hashline.d.ts +6 -0
  144. package/dist/formats/hashline.js +16 -0
  145. package/dist/formats/hermes.d.ts +9 -0
  146. package/dist/formats/hermes.js +62 -0
  147. package/dist/formats/index.d.ts +4 -0
  148. package/dist/formats/index.js +4 -0
  149. package/dist/formats/opencode.d.ts +7 -0
  150. package/dist/formats/opencode.js +35 -0
  151. package/dist/formats/shared.d.ts +4 -0
  152. package/dist/formats/shared.js +9 -0
  153. package/dist/formatters/eof.d.ts +7 -0
  154. package/dist/formatters/eof.js +20 -0
  155. package/dist/formatters/file-hash.d.ts +7 -0
  156. package/dist/formatters/file-hash.js +13 -0
  157. package/dist/formatters/hashline.d.ts +11 -0
  158. package/dist/formatters/hashline.js +18 -0
  159. package/dist/formatters/index.d.ts +10 -0
  160. package/dist/formatters/index.js +7 -0
  161. package/dist/formatters/json.d.ts +17 -0
  162. package/dist/formatters/json.js +36 -0
  163. package/dist/formatters/layout.d.ts +15 -0
  164. package/dist/formatters/layout.js +33 -0
  165. package/dist/formatters/line-number.d.ts +11 -0
  166. package/dist/formatters/line-number.js +23 -0
  167. package/dist/formatters/plain.d.ts +7 -0
  168. package/dist/formatters/plain.js +14 -0
  169. package/dist/hooks/index.d.ts +4 -0
  170. package/dist/hooks/index.js +2 -0
  171. package/dist/hooks/redact.d.ts +14 -0
  172. package/dist/hooks/redact.js +53 -0
  173. package/dist/hooks/repeat-read-guard.d.ts +13 -0
  174. package/dist/hooks/repeat-read-guard.js +49 -0
  175. package/dist/index.d.ts +35 -0
  176. package/dist/index.js +15 -0
  177. package/dist/resolve/expand-home.d.ts +5 -0
  178. package/dist/resolve/expand-home.js +17 -0
  179. package/dist/resolve/index.d.ts +5 -0
  180. package/dist/resolve/index.js +5 -0
  181. package/dist/resolve/path-resolvers.d.ts +7 -0
  182. package/dist/resolve/path-resolvers.js +38 -0
  183. package/dist/resolve/reanchor-leading-slash.d.ts +9 -0
  184. package/dist/resolve/reanchor-leading-slash.js +23 -0
  185. package/dist/resolve/strip-prefixes.d.ts +6 -0
  186. package/dist/resolve/strip-prefixes.js +30 -0
  187. package/dist/resolve/unicode-repair.d.ts +9 -0
  188. package/dist/resolve/unicode-repair.js +38 -0
  189. package/dist/signature/contract.d.ts +18 -0
  190. package/dist/signature/contract.js +0 -0
  191. package/dist/signature/index.d.ts +4 -0
  192. package/dist/signature/index.js +3 -0
  193. package/dist/signature/line-range.d.ts +7 -0
  194. package/dist/signature/line-range.js +59 -0
  195. package/dist/signature/messages.d.ts +4 -0
  196. package/dist/signature/messages.js +9 -0
  197. package/dist/signature/offset-limit.d.ts +13 -0
  198. package/dist/signature/offset-limit.js +59 -0
  199. package/dist/signature/schema.d.ts +10 -0
  200. package/dist/signature/schema.js +80 -0
  201. package/dist/state/index.d.ts +15 -0
  202. package/dist/state/index.js +77 -0
  203. package/dist/suggest/canonicalize.d.ts +6 -0
  204. package/dist/suggest/canonicalize.js +12 -0
  205. package/dist/suggest/default-suggest.d.ts +3 -0
  206. package/dist/suggest/default-suggest.js +12 -0
  207. package/dist/suggest/index.d.ts +3 -0
  208. package/dist/suggest/index.js +3 -0
  209. package/dist/suggest/suggest-file-names.d.ts +2 -0
  210. package/dist/suggest/suggest-file-names.js +46 -0
  211. package/docs/architecture.md +249 -0
  212. package/docs/result-schema.md +222 -0
  213. package/package.json +52 -4
@@ -0,0 +1,222 @@
1
+ # Result schema
2
+
3
+ Every read returns a `ReadResult`: a `ReadReport` plus `content`, the parts that the formatter made for the model.
4
+
5
+ ```ts
6
+ type ReadResult = ReadReport & { readonly content: readonly ContentPart[] };
7
+ type ReadReport = ReadOk | ReadMedia | ReadUnsupported | ReadFailure;
8
+
9
+ type ContentPart =
10
+ | { type: "text"; text: string }
11
+ | { type: "media"; mediaType: string; data: Uint8Array; name?: string };
12
+ ```
13
+
14
+ `ReadReport` is a discriminated union on `status`. Every variant has `tool: "read"`. Only the `error` variant has an `error` field, and it is never null there, so a plain `if (result.status === "error")` narrows it:
15
+
16
+ ```ts
17
+ if (result.status === "error") console.log(result.error.code, result.error.phase);
18
+ ```
19
+
20
+ There is no `result.text`. `textOf(result)` joins the text parts of `content` with `"\n"`.
21
+
22
+ The outcome is plain data. Only media parts hold bytes, as `Uint8Array`. Nothing in the result comes from `ctx.host`.
23
+
24
+ ## Statuses
25
+
26
+ | Status | When |
27
+ | ------------- | ------------------------------------------------------------------------------------------------------------------- |
28
+ | `ok` | The file, the converted text, or the directory listing was read as text. This includes empty files and empty views. |
29
+ | `media` | A converter returned content parts, for example an image. |
30
+ | `unsupported` | A classifier or a converter refused the content. `code` is open. |
31
+ | `error` | The read failed. `error.code` is a `ReadErrorCode`. |
32
+
33
+ ## `ok`
34
+
35
+ ```ts
36
+ {
37
+ tool: "read",
38
+ status: "ok",
39
+ request: { path, offset, limit, ranged },
40
+ file: {
41
+ requestedPath, resolvedPath, displayPath, backend,
42
+ size, mtimeMs, identity, mimeType, resolvedFrom, version
43
+ },
44
+ classification: { kind, classifier, code, mimeType, confidence, reasons },
45
+ conversion: { converter, mimeType } | null,
46
+ view: {
47
+ lines: [{ number, text, clamped, sourceChars }],
48
+ startLine, endLine, bytes, partial
49
+ },
50
+ truncation: { truncated, reasons, primary },
51
+ continuation: { available, next },
52
+ totals: { lines, exact, bytes },
53
+ observation: { id, statId, contentId, viewId, observedAt, wholeFileVisible } | null,
54
+ notes: [...],
55
+ content: [{ type: "text", text: "1|..." }]
56
+ }
57
+ ```
58
+
59
+ Field notes:
60
+
61
+ - `request` is the validated canonical input. `offset` and `limit` are concrete: `offset` defaults to 1, and `limit` defaults to and is clamped to `limits.maxLines`. A clamp adds a `clamped` info note. `ranged` is `true` when the input set `offset` or `limit`.
62
+ - `file.requestedPath` is the model's path. `file.resolvedFrom` is the model's path when a resolver changed it, and `null` otherwise. `file.backend` is the filesystem `id`. `file.identity` is `null` unless the filesystem has stable identity. `file.version` is the backend's change token from `open()` (`info.version`), or `null` when the backend gives none. It is kept when the filesystem has no stable identity.
63
+ - `classification.kind` is `"text"`, `"unsupported"`, or `"directory"`. `classifier` is the classifier id, or `"fs"` for a directory. `code` is the classifier's unsupported code, and `null` for text and directories. A converted file keeps its classification, so a converted notebook has `kind: "unsupported"` and `code: "NOTEBOOK"`.
64
+ - `conversion` names the converter when one produced the text, and `null` for plain text.
65
+ - `view.lines[].text` is source text clamped to `limits.maxCharsPerLine`, with no gutter and no clamp marker. Both belong to the formatter.
66
+ - `view.lines[].sourceChars` is the original length in characters, and `null` unless the line was clamped.
67
+ - `view.endLine` is `startLine - 1` when the view is empty.
68
+ - `view.bytes` counts UTF-8 source bytes plus one separator byte for each line after the first. It does not count the gutter.
69
+ - `view.partial` is `true` when the view starts after line 1, stops before the end, or has any truncation reason.
70
+ - `truncation.reasons` can hold `lines`, `bytes`, `budget`, `scan-limit`, and `line-length`, in that order. `primary` is the first, or `null`.
71
+ - `continuation.next` is a complete canonical input, `{ path, offset, limit }`. Send it again to continue. It uses canonical names even when the tool uses a renamed signature.
72
+ - `totals.lines` is `null` and `totals.exact` is `false` when the scan limit stopped counting. `totals.bytes` is the scanned byte count when the scan reached the end, and the file size otherwise. For converted text, it is the size of the converted text that was scanned.
73
+ - `observation` is `null` with no `digest`, and for directories. `contentId` is `null` when the scan stopped before the end. For converted text it is still the hash of the source bytes. `wholeFileVisible` is `true` only when the offset is 1, nothing was truncated or clamped, and no hook changed the view.
74
+
75
+ ## `media`
76
+
77
+ ```ts
78
+ {
79
+ tool: "read",
80
+ status: "media",
81
+ request, file, classification,
82
+ conversion: { converter, mimeType },
83
+ parts: [{ type: "media", mediaType, data, name? }],
84
+ observation: { ... wholeFileVisible: false } | null,
85
+ notes: [...],
86
+ content: [...]
87
+ }
88
+ ```
89
+
90
+ `parts` is what the converter returned. `conversion.mimeType` is the first media part's type. The formatter puts note text first and then the parts in `content`. `observation.viewId` hashes the parts, and `wholeFileVisible` is always `false`.
91
+
92
+ ## `unsupported`
93
+
94
+ ```ts
95
+ { tool: "read", status: "unsupported", code, request, file, classification, notes, content }
96
+ ```
97
+
98
+ `code` comes from the classifier, a converter refusal, or the core:
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 |
106
+
107
+ ## `error`
108
+
109
+ ```ts
110
+ {
111
+ tool: "read",
112
+ status: "error",
113
+ error: { code, phase, message, data? },
114
+ request: ReadRequest | null,
115
+ file: FileInfo | null,
116
+ notes,
117
+ content
118
+ }
119
+ ```
120
+
121
+ `error` has the same shape in every tool (`ToolError`). `code` is an UPPER_SNAKE `ReadErrorCode`. `phase` is the `ReadPhase` that failed: `input`, `resolve`, `open`, `authorize`, `sampling`, `conversion`, `scan`, `verification`, or `hooks`. `message` and `data` are copied from the error note. The error note is always first in `notes`. Its code is the error code in kebab case, for example `not-found` for `NOT_FOUND`. When an authorizer refuses with its own `note`, the note keeps its message and data, its code becomes the error code in kebab case, its severity becomes `warning`, and the host's own code moves to `data.source`. A `retry` on that note stays. Write and bash use the same rule.
122
+
123
+ `request` is `null` only for `INVALID_INPUT`. `file` is set only for failures found after the file was opened and allowed: `CHANGED_DURING_READ`, `UNSUPPORTED_BACKEND` from the classifiers, and a failed `verify()`. It is `null` for every other failure. `DENIED` from an authorizer always has `file: null`.
124
+
125
+ | `error.code` | `error.phase` | Meaning |
126
+ | ----------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `INVALID_INPUT` | `input` | The input failed validation |
128
+ | `NOT_FOUND` | `resolve` | No such file. `note.data.suggestions` can hold nearby names. A miss in `open()` is reported in this phase too |
129
+ | `NOT_A_FILE` | `open`, `verification` | A directory with no directory converter, or a FIFO, socket, or device. `note.data.kind` names it. |
130
+ | `DANGEROUS_PATH` | `open`, `verification`, `conversion` | A refused namespace such as `/dev`, or a filesystem deny root |
131
+ | `OUTSIDE_ALLOWED_ROOTS` | `open`, `verification`, `conversion` | Outside every allowed root |
132
+ | `PERMISSION_DENIED` | `open`, `verification`, `conversion` | The operating system refused access |
133
+ | `DENIED` | `authorize`, `open`, `verification`, `conversion` | A policy refusal: an authorizer (`authorize`), or a filesystem rule such as a rejected symlink |
134
+ | `TOO_LARGE` | `open` | The file is over a byte ceiling of the filesystem backend, such as `maxBufferedBytes` |
135
+ | `CHANGED_DURING_READ` | `verification` | The file changed while it was read. The note has a retry of the same range. |
136
+ | `ABORTED` | `input`, or the phase at the time | The signal fired. A signal that had fired before the call is `input`, as in write and bash. `note.data.phase` names the stage. Nothing was recorded. |
137
+ | `UNSUPPORTED_BACKEND` | `open`, `sampling`, `scan` | No classifier had an opinion, or the filesystem cannot serve the read |
138
+ | `EXTENSION_FAILED` | the phase of the host code | Host code threw or broke a rule. `note.data` has `extension`, `phase`, and `id` when the extension has one. |
139
+ | `IO_ERROR` | any | Any other backend failure, including a filesystem method that threw |
140
+
141
+ `conversion` is the phase of a directory listing that failed. `hooks` is the phase of an `afterRead` hook failure.
142
+
143
+ The statuses and error codes of all five tools are in one table in `docs/result-schema.md` of `@better-fs-tools/write`. The bash result is in `docs/result-schema.md` of `@better-fs-tools/shell`.
144
+
145
+ ## Notes
146
+
147
+ ```ts
148
+ { code, severity: "info" | "warning", message, retry?: ReadInput, data?: JsonObject }
149
+ ```
150
+
151
+ `code` is stable. The default formatter prints a note as `[read:<code>] <message>`. Key your code on `code`, not on the wording. `messages` overrides change the wording only. `retry` is always canonical. The message prints it with `messages.retry`, so the text can use the tool's own names.
152
+
153
+ Notes from the core:
154
+
155
+ | `code` | Severity | `retry` | `data` |
156
+ | ----------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
157
+ | `continue` | info | the next view | `{ reason }` |
158
+ | `clamped` | info | none | `{ param: "limit", requested, max }`: the requested `limit` was over `maxLines` |
159
+ | `offset-unreached` | warning | a lower offset that the scan reached | `{ reachedLine }` |
160
+ | `first-line-exceeds-byte-limit` | warning | the view after that line | `{ line }` |
161
+ | `line-clamped` | warning | none | `{ lines, total, maxChars }`. `lines` lists at most 20 line numbers. |
162
+ | `scan-limit` | warning | none | `{ maxScanBytes }` |
163
+ | `offset-past-eof` | info | the last line | `{ totalLines }` |
164
+ | `empty` | info | none | none |
165
+ | `weak-identity` | info | none | `{ backend }` |
166
+ | `buffered-backend` | info | none | `{ backend }` |
167
+ | `not-found` | warning | none | `{ suggestions?, entriesTruncated?, detail?, cause? }` |
168
+ | `not-a-file` | warning | none | `{ kind, detail?, cause? }` |
169
+ | `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 }`. |
170
+ | `denied` from an authorizer | warning | set by `sizeCeiling({ unrangedOnly: true })` | the authorizer's data, for example `{ pattern }` or `{ size, maxBytes }` |
171
+ | `too-large` | warning | none | `{ stage, limit }`. `stage` is `"convert"` or `"media"`. From a backend: `{ limit, size, detail?, cause? }` |
172
+ | `changed-during-read` | warning | the same range | none |
173
+ | `aborted` | warning | none | `{ phase }` |
174
+ | `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? }`. |
176
+ | `view-modified` | info | none | `{ hook }` |
177
+
178
+ Notes from the built-in helpers:
179
+
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 }` |
189
+
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
+
192
+ Order: the view notes come first, then the classifier or converter notes, then `empty`, then the capability notes. Allow notes from the authorizer and then the resolver note follow. Hooks add their notes last.
193
+
194
+ ## Content
195
+
196
+ The formatter makes `content` once, with `mode: "model"`. A formatter that returns a string gives one text part.
197
+
198
+ - `lineNumberFormatter()`: `1|source` lines, then a blank line, then `[read:<code>]` note lines. Media: a text part with the note lines, then the media parts.
199
+ - `plainFormatter()`: the source lines with no gutter, then the note lines.
200
+ - `jsonFormatter()`: JSON text of the outcome, or of `pick(outcome)`. Byte arrays become `{ "bytes": n }`, and media parts follow as parts.
201
+ - `@better-fs-tools/read/formats`: `opencodeFormat()`, `deepAgentsFormat()`, `hashlineFormat()`, and `hermesFormat()`.
202
+
203
+ Suggestions live in `note.data` and in the message, so a formatter that shows the note shows the names.
204
+
205
+ ## The stored record
206
+
207
+ With a `state` store and a `digest`, an `ok` or `media` result is stored under `file.resolvedPath`:
208
+
209
+ ```ts
210
+ {
211
+ schema: 2,
212
+ origin: "read",
213
+ observationId, resolvedPath, identity, version, digest,
214
+ contentId, viewId, observedAt, wholeFileVisible, totalsExact,
215
+ request: { offset, limit }
216
+ }
217
+ ```
218
+
219
+ - `version` is `file.version`. `digest` is the `Digest.id` that made `contentId` and `viewId`.
220
+ - A write tool stores a record with `origin: "write"` after a commit. Its `viewId` equals its `contentId`, and its `request` is `null`.
221
+
222
+ Hooks see the earlier record as `ctx.previous`. A record of another schema, such as a schema 1 record from an older store, gives `previous: null`. `repeatReadGuard` only fires on a record with `origin: "read"`.
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@better-fs-tools/read",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A bounded file reader for TypeScript agent tools.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/gingerhendrix/better-fs-tools.git",
9
+ "directory": "packages/read"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "docs"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./formats": {
23
+ "types": "./dist/formats/index.d.ts",
24
+ "import": "./dist/formats/index.js"
25
+ },
26
+ "./signature": {
27
+ "types": "./dist/signature/index.d.ts",
28
+ "import": "./dist/signature/index.js"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ },
37
+ "./formats": {
38
+ "types": "./dist/formats/index.d.ts",
39
+ "import": "./dist/formats/index.js"
40
+ },
41
+ "./signature": {
42
+ "types": "./dist/signature/index.d.ts",
43
+ "import": "./dist/signature/index.js"
44
+ }
45
+ },
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@better-fs-tools/fs": "^0.1.0"
50
+ },
51
+ "devDependencies": {
52
+ "typebox": "^1.3.16"
53
+ }
54
+ }