@manyoars/oars 0.2.0 → 0.4.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 (320) hide show
  1. package/README.md +147 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +10 -4
  5. package/dist/cli.js +440 -91
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.d.ts +1 -1
  9. package/dist/commands/login.js +3 -3
  10. package/dist/errors.d.ts +13 -13
  11. package/dist/errors.js +28 -11
  12. package/dist/gate/checks.d.ts +68 -0
  13. package/dist/gate/checks.js +584 -0
  14. package/dist/gate/command.d.ts +14 -0
  15. package/dist/gate/command.js +61 -0
  16. package/dist/gate/hook-fill-main.d.ts +1 -0
  17. package/dist/gate/hook-fill-main.js +16 -0
  18. package/dist/gate/hook-fill.d.ts +38 -0
  19. package/dist/gate/hook-fill.js +94 -0
  20. package/dist/gate/hook-main.d.ts +1 -0
  21. package/dist/gate/hook-main.js +47 -0
  22. package/dist/gate/hook-process.d.ts +24 -0
  23. package/dist/gate/hook-process.js +112 -0
  24. package/dist/gate/hook-record.d.ts +3 -0
  25. package/dist/gate/hook-record.js +8 -0
  26. package/dist/gate/hook.d.ts +48 -0
  27. package/dist/gate/hook.js +259 -0
  28. package/dist/gate/install.d.ts +25 -0
  29. package/dist/gate/install.js +126 -0
  30. package/dist/gate/local-bin.d.ts +8 -0
  31. package/dist/gate/local-bin.js +36 -0
  32. package/dist/gate/notes.d.ts +19 -0
  33. package/dist/gate/notes.js +27 -0
  34. package/dist/gate/novelty.d.ts +35 -0
  35. package/dist/gate/novelty.js +99 -0
  36. package/dist/gate/policy.d.ts +25 -0
  37. package/dist/gate/policy.js +73 -0
  38. package/dist/gate/record.d.ts +120 -0
  39. package/dist/gate/record.js +125 -0
  40. package/dist/gate/registry-state.d.ts +10 -0
  41. package/dist/gate/registry-state.js +30 -0
  42. package/dist/gate/registry.d.ts +56 -0
  43. package/dist/gate/registry.js +94 -0
  44. package/dist/gate/replay.d.ts +20 -0
  45. package/dist/gate/replay.js +254 -0
  46. package/dist/gate/report.d.ts +50 -0
  47. package/dist/gate/report.js +89 -0
  48. package/dist/gate/scope.d.ts +7 -0
  49. package/dist/gate/scope.js +27 -0
  50. package/dist/gate/transcripts.d.ts +68 -0
  51. package/dist/gate/transcripts.js +232 -0
  52. package/dist/hook-bin.d.ts +1 -0
  53. package/dist/hook-bin.js +50 -0
  54. package/dist/issuer.d.ts +10 -6
  55. package/dist/issuer.js +19 -24
  56. package/dist/main.d.ts +1 -0
  57. package/dist/main.js +67 -0
  58. package/dist/oidc.js +1 -1
  59. package/dist/sessions/actions.d.ts +35 -0
  60. package/dist/sessions/actions.js +429 -0
  61. package/dist/sessions/allow.d.ts +23 -0
  62. package/dist/sessions/allow.js +71 -0
  63. package/dist/sessions/archive.d.ts +11 -0
  64. package/dist/sessions/archive.js +38 -0
  65. package/dist/sessions/audit.d.ts +42 -0
  66. package/dist/sessions/audit.js +104 -0
  67. package/dist/sessions/baseline.d.ts +62 -0
  68. package/dist/sessions/baseline.js +190 -0
  69. package/dist/sessions/call-shape.d.ts +19 -0
  70. package/dist/sessions/call-shape.js +85 -0
  71. package/dist/sessions/capture-runtime.d.ts +10 -0
  72. package/dist/sessions/capture-runtime.js +12 -0
  73. package/dist/sessions/cell-increments.d.ts +47 -0
  74. package/dist/sessions/cell-increments.js +243 -0
  75. package/dist/sessions/cells.d.ts +109 -0
  76. package/dist/sessions/cells.js +261 -0
  77. package/dist/sessions/codex.d.ts +53 -0
  78. package/dist/sessions/codex.js +745 -0
  79. package/dist/sessions/command.d.ts +127 -0
  80. package/dist/sessions/command.js +419 -0
  81. package/dist/sessions/copilot.d.ts +40 -0
  82. package/dist/sessions/copilot.js +432 -0
  83. package/dist/sessions/coverage.d.ts +19 -0
  84. package/dist/sessions/coverage.js +56 -0
  85. package/dist/sessions/cursor.d.ts +24 -0
  86. package/dist/sessions/cursor.js +324 -0
  87. package/dist/sessions/derive.d.ts +57 -0
  88. package/dist/sessions/derive.js +386 -0
  89. package/dist/sessions/exclusion.d.ts +87 -0
  90. package/dist/sessions/exclusion.js +361 -0
  91. package/dist/sessions/extract.d.ts +181 -0
  92. package/dist/sessions/extract.js +1806 -0
  93. package/dist/sessions/fold.d.ts +17 -0
  94. package/dist/sessions/fold.js +71 -0
  95. package/dist/sessions/frame.d.ts +24 -0
  96. package/dist/sessions/frame.js +95 -0
  97. package/dist/sessions/gemini.d.ts +37 -0
  98. package/dist/sessions/gemini.js +613 -0
  99. package/dist/sessions/gold-arms.d.ts +67 -0
  100. package/dist/sessions/gold-arms.js +172 -0
  101. package/dist/sessions/gold.d.ts +197 -0
  102. package/dist/sessions/gold.js +463 -0
  103. package/dist/sessions/graph.d.ts +12 -0
  104. package/dist/sessions/graph.js +34 -0
  105. package/dist/sessions/grok.d.ts +36 -0
  106. package/dist/sessions/grok.js +421 -0
  107. package/dist/sessions/hook-calls.d.ts +3 -0
  108. package/dist/sessions/hook-calls.js +47 -0
  109. package/dist/sessions/hook-index.d.ts +66 -0
  110. package/dist/sessions/hook-index.js +164 -0
  111. package/dist/sessions/hooks.d.ts +17 -0
  112. package/dist/sessions/hooks.js +281 -0
  113. package/dist/sessions/identity.d.ts +27 -0
  114. package/dist/sessions/identity.js +131 -0
  115. package/dist/sessions/install.d.ts +70 -0
  116. package/dist/sessions/install.js +246 -0
  117. package/dist/sessions/lineage.d.ts +19 -0
  118. package/dist/sessions/lineage.js +118 -0
  119. package/dist/sessions/mcp.d.ts +7 -0
  120. package/dist/sessions/mcp.js +157 -0
  121. package/dist/sessions/merkle.d.ts +7 -0
  122. package/dist/sessions/merkle.js +28 -0
  123. package/dist/sessions/motif.d.ts +44 -0
  124. package/dist/sessions/motif.js +59 -0
  125. package/dist/sessions/opencode.d.ts +33 -0
  126. package/dist/sessions/opencode.js +319 -0
  127. package/dist/sessions/otel.d.ts +60 -0
  128. package/dist/sessions/otel.js +494 -0
  129. package/dist/sessions/package-books.d.ts +48 -0
  130. package/dist/sessions/package-books.js +120 -0
  131. package/dist/sessions/package-resolve.d.ts +32 -0
  132. package/dist/sessions/package-resolve.js +185 -0
  133. package/dist/sessions/pending.d.ts +34 -0
  134. package/dist/sessions/pending.js +142 -0
  135. package/dist/sessions/pi.d.ts +36 -0
  136. package/dist/sessions/pi.js +353 -0
  137. package/dist/sessions/pool-books.d.ts +21 -0
  138. package/dist/sessions/pool-books.js +75 -0
  139. package/dist/sessions/pool-choices.d.ts +5 -0
  140. package/dist/sessions/pool-choices.js +11 -0
  141. package/dist/sessions/pool-command.d.ts +115 -0
  142. package/dist/sessions/pool-command.js +669 -0
  143. package/dist/sessions/pool-delta.d.ts +54 -0
  144. package/dist/sessions/pool-delta.js +190 -0
  145. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  146. package/dist/sessions/pool-nightly-deps.js +11 -0
  147. package/dist/sessions/pool-nightly.d.ts +10 -0
  148. package/dist/sessions/pool-nightly.js +85 -0
  149. package/dist/sessions/pool-outbox.d.ts +76 -0
  150. package/dist/sessions/pool-outbox.js +313 -0
  151. package/dist/sessions/pool-print.d.ts +26 -0
  152. package/dist/sessions/pool-print.js +68 -0
  153. package/dist/sessions/pool-raw.d.ts +27 -0
  154. package/dist/sessions/pool-raw.js +68 -0
  155. package/dist/sessions/pool-replica.d.ts +35 -0
  156. package/dist/sessions/pool-replica.js +91 -0
  157. package/dist/sessions/pool-send-lock.d.ts +20 -0
  158. package/dist/sessions/pool-send-lock.js +87 -0
  159. package/dist/sessions/pool-sent.d.ts +4 -0
  160. package/dist/sessions/pool-sent.js +26 -0
  161. package/dist/sessions/pool-totals.d.ts +10 -0
  162. package/dist/sessions/pool-totals.js +21 -0
  163. package/dist/sessions/pool-wire.d.ts +435 -0
  164. package/dist/sessions/pool-wire.js +1100 -0
  165. package/dist/sessions/pool.d.ts +32 -0
  166. package/dist/sessions/pool.js +164 -0
  167. package/dist/sessions/projection.d.ts +75 -0
  168. package/dist/sessions/projection.js +165 -0
  169. package/dist/sessions/public-vocabulary.d.ts +72 -0
  170. package/dist/sessions/public-vocabulary.js +295 -0
  171. package/dist/sessions/query.d.ts +123 -0
  172. package/dist/sessions/query.js +268 -0
  173. package/dist/sessions/read-adoption.d.ts +2 -0
  174. package/dist/sessions/read-adoption.js +27 -0
  175. package/dist/sessions/read-call-shape.d.ts +2 -0
  176. package/dist/sessions/read-call-shape.js +25 -0
  177. package/dist/sessions/read-change.d.ts +2 -0
  178. package/dist/sessions/read-change.js +30 -0
  179. package/dist/sessions/read-cost.d.ts +4 -0
  180. package/dist/sessions/read-cost.js +48 -0
  181. package/dist/sessions/read-day-move.d.ts +3 -0
  182. package/dist/sessions/read-day-move.js +65 -0
  183. package/dist/sessions/read-fit.d.ts +7 -0
  184. package/dist/sessions/read-fit.js +41 -0
  185. package/dist/sessions/read-interaction.d.ts +5 -0
  186. package/dist/sessions/read-interaction.js +62 -0
  187. package/dist/sessions/read-lead-lag.d.ts +15 -0
  188. package/dist/sessions/read-lead-lag.js +81 -0
  189. package/dist/sessions/read-live.d.ts +6 -0
  190. package/dist/sessions/read-live.js +38 -0
  191. package/dist/sessions/read-method.d.ts +2 -0
  192. package/dist/sessions/read-method.js +29 -0
  193. package/dist/sessions/read-next-step.d.ts +2 -0
  194. package/dist/sessions/read-next-step.js +35 -0
  195. package/dist/sessions/read-order.d.ts +54 -0
  196. package/dist/sessions/read-order.js +62 -0
  197. package/dist/sessions/read-readable.d.ts +8 -0
  198. package/dist/sessions/read-readable.js +24 -0
  199. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  200. package/dist/sessions/read-recovery-edge.js +26 -0
  201. package/dist/sessions/read-recovery.d.ts +20 -0
  202. package/dist/sessions/read-recovery.js +97 -0
  203. package/dist/sessions/read-rows.d.ts +13 -0
  204. package/dist/sessions/read-rows.js +47 -0
  205. package/dist/sessions/read-substitution.d.ts +5 -0
  206. package/dist/sessions/read-substitution.js +51 -0
  207. package/dist/sessions/read-support.d.ts +48 -0
  208. package/dist/sessions/read-support.js +73 -0
  209. package/dist/sessions/read-tool-day.d.ts +2 -0
  210. package/dist/sessions/read-tool-day.js +26 -0
  211. package/dist/sessions/read-version-boundary.d.ts +31 -0
  212. package/dist/sessions/read-version-boundary.js +95 -0
  213. package/dist/sessions/read.d.ts +86 -0
  214. package/dist/sessions/read.js +87 -0
  215. package/dist/sessions/recall-bundle.d.ts +12 -0
  216. package/dist/sessions/recall-bundle.js +33 -0
  217. package/dist/sessions/recall-consequence.d.ts +45 -0
  218. package/dist/sessions/recall-consequence.js +179 -0
  219. package/dist/sessions/recall-frames.d.ts +18 -0
  220. package/dist/sessions/recall-frames.js +44 -0
  221. package/dist/sessions/recall-next.d.ts +145 -0
  222. package/dist/sessions/recall-next.js +241 -0
  223. package/dist/sessions/recall-object.d.ts +139 -0
  224. package/dist/sessions/recall-object.js +78 -0
  225. package/dist/sessions/recall-pool.d.ts +19 -0
  226. package/dist/sessions/recall-pool.js +96 -0
  227. package/dist/sessions/recall-push.d.ts +26 -0
  228. package/dist/sessions/recall-push.js +47 -0
  229. package/dist/sessions/recall-reads.d.ts +124 -0
  230. package/dist/sessions/recall-reads.js +417 -0
  231. package/dist/sessions/recall-render.d.ts +19 -0
  232. package/dist/sessions/recall-render.js +168 -0
  233. package/dist/sessions/recovery-calls.d.ts +21 -0
  234. package/dist/sessions/recovery-calls.js +25 -0
  235. package/dist/sessions/redact.d.ts +17 -0
  236. package/dist/sessions/redact.js +113 -0
  237. package/dist/sessions/repo-root.d.ts +10 -0
  238. package/dist/sessions/repo-root.js +101 -0
  239. package/dist/sessions/schema.d.ts +694 -0
  240. package/dist/sessions/schema.js +167 -0
  241. package/dist/sessions/session-shape.d.ts +76 -0
  242. package/dist/sessions/session-shape.js +355 -0
  243. package/dist/sessions/sources.d.ts +50 -0
  244. package/dist/sessions/sources.js +233 -0
  245. package/dist/sessions/steps.d.ts +105 -0
  246. package/dist/sessions/steps.js +250 -0
  247. package/dist/sessions/store.d.ts +309 -0
  248. package/dist/sessions/store.js +1498 -0
  249. package/dist/sessions/strata.d.ts +26 -0
  250. package/dist/sessions/strata.js +77 -0
  251. package/dist/sessions/survival.d.ts +27 -0
  252. package/dist/sessions/survival.js +230 -0
  253. package/dist/sessions/transcript-tail.d.ts +14 -0
  254. package/dist/sessions/transcript-tail.js +64 -0
  255. package/dist/sessions/verify.d.ts +53 -0
  256. package/dist/sessions/verify.js +286 -0
  257. package/dist/sessions/vocab-load.d.ts +26 -0
  258. package/dist/sessions/vocab-load.js +61 -0
  259. package/dist/sessions/vscode-copilot.d.ts +38 -0
  260. package/dist/sessions/vscode-copilot.js +494 -0
  261. package/dist/telemetry/command.d.ts +8 -0
  262. package/dist/telemetry/command.js +85 -0
  263. package/dist/telemetry/consent.d.ts +17 -0
  264. package/dist/telemetry/consent.js +63 -0
  265. package/dist/telemetry/crash.d.ts +8 -0
  266. package/dist/telemetry/crash.js +52 -0
  267. package/dist/telemetry/emit.d.ts +31 -0
  268. package/dist/telemetry/emit.js +42 -0
  269. package/dist/telemetry/fields.d.ts +16 -0
  270. package/dist/telemetry/fields.js +26 -0
  271. package/dist/telemetry/health.d.ts +21 -0
  272. package/dist/telemetry/health.js +108 -0
  273. package/dist/telemetry/hook-emit.d.ts +4 -0
  274. package/dist/telemetry/hook-emit.js +18 -0
  275. package/dist/telemetry/measures.d.ts +32 -0
  276. package/dist/telemetry/measures.js +128 -0
  277. package/dist/telemetry/observe.d.ts +13 -0
  278. package/dist/telemetry/observe.js +51 -0
  279. package/dist/telemetry/read-events.d.ts +9 -0
  280. package/dist/telemetry/read-events.js +17 -0
  281. package/dist/telemetry/record.d.ts +17 -0
  282. package/dist/telemetry/record.js +26 -0
  283. package/dist/telemetry/report.d.ts +77 -0
  284. package/dist/telemetry/report.js +188 -0
  285. package/dist/telemetry/schema.d.ts +40 -0
  286. package/dist/telemetry/schema.js +187 -0
  287. package/dist/telemetry/send-main.d.ts +1 -0
  288. package/dist/telemetry/send-main.js +11 -0
  289. package/dist/telemetry/send.d.ts +36 -0
  290. package/dist/telemetry/send.js +172 -0
  291. package/dist/telemetry/spool.d.ts +17 -0
  292. package/dist/telemetry/spool.js +70 -0
  293. package/dist/telemetry/wire.d.ts +25 -0
  294. package/dist/telemetry/wire.js +76 -0
  295. package/package.json +4 -5
  296. package/vocab/crates.json +1 -0
  297. package/vocab/go.json +1 -0
  298. package/vocab/harness.json +1 -0
  299. package/vocab/homebrew.json +1 -0
  300. package/vocab/manifest.json +1 -0
  301. package/vocab/maven.json +1 -0
  302. package/vocab/models.json +1 -0
  303. package/vocab/npm.json +1 -0
  304. package/vocab/programs.json +1 -0
  305. package/vocab/pypi.json +1 -0
  306. package/vocab/rubygems.json +1 -0
  307. package/dist/commands/deploy-input.d.ts +0 -38
  308. package/dist/commands/deploy-input.js +0 -150
  309. package/dist/commands/deploy.d.ts +0 -22
  310. package/dist/commands/deploy.js +0 -124
  311. package/dist/commands/status.d.ts +0 -7
  312. package/dist/commands/status.js +0 -18
  313. package/dist/commands/templates.d.ts +0 -11
  314. package/dist/commands/templates.js +0 -32
  315. package/dist/commands/test-charge.d.ts +0 -11
  316. package/dist/commands/test-charge.js +0 -34
  317. package/dist/journal.d.ts +0 -26
  318. package/dist/journal.js +0 -58
  319. package/parity/deploy-plans/paid-api.json +0 -34
  320. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,25 @@
1
+ /** The classes the policy sets. Each is something a row can already tell: a registry name first published recently, and a name the registry does not hold. */
2
+ export declare const POLICY_CLASSES: readonly ["young-name", "not-found"];
3
+ export type PolicyClass = (typeof POLICY_CLASSES)[number];
4
+ export declare const POLICY_ACTIONS: readonly ["warn", "block", "off"];
5
+ export type PolicyAction = (typeof POLICY_ACTIONS)[number];
6
+ export interface Policy {
7
+ classes: Readonly<Record<PolicyClass, PolicyAction>>;
8
+ /** A name first published fewer days ago than this is a young name. */
9
+ youngDays: number;
10
+ /** The share, 0 to 100, of notes that would show and do not. Never applied to a block. */
11
+ holdbackPercent: number;
12
+ }
13
+ export declare const DEFAULT_POLICY: Policy;
14
+ /** Where a row's policy came from: no file, a file that fit, or a file that was there and did not (so the defaults stood). */
15
+ export declare const POLICY_SOURCES: readonly ["default", "file", "invalid"];
16
+ export type PolicySource = (typeof POLICY_SOURCES)[number];
17
+ export interface PolicyRead {
18
+ policy: Policy;
19
+ source: PolicySource;
20
+ }
21
+ export declare const policyPath: (storeDir: string) => string;
22
+ /** Never throws. A file that is not there is the defaults; one that is there and cannot be read or does not fit is the defaults too, and says `invalid`. */
23
+ export declare const loadPolicy: (storeDir: string) => Promise<PolicyRead>;
24
+ /** What `oars gate policy` prints. */
25
+ export declare const formatPolicy: (read: PolicyRead, file: string) => string;
@@ -0,0 +1,73 @@
1
+ // The gate policy file: what the hook may say, per class, in a closed vocabulary. The operator writes it; nothing here writes it. A missing file reads as the defaults, and so
2
+ // does one that cannot be read or does not fit the shape: the hook never throws over it, and the row records which of the three it was, so `oars gate report` can count them.
3
+ import { readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ /** The classes the policy sets. Each is something a row can already tell: a registry name first published recently, and a name the registry does not hold. */
6
+ export const POLICY_CLASSES = ['young-name', 'not-found'];
7
+ export const POLICY_ACTIONS = ['warn', 'block', 'off'];
8
+ /** What a young name can be set to: a note or nothing. A block is for a name the registry does not hold, since only that is a fact the registry answers by itself. */
9
+ const ALLOWED = { 'young-name': ['warn', 'off'], 'not-found': POLICY_ACTIONS };
10
+ export const DEFAULT_POLICY = { classes: { 'young-name': 'warn', 'not-found': 'warn' }, youngDays: 30, holdbackPercent: 10 };
11
+ /** Where a row's policy came from: no file, a file that fit, or a file that was there and did not (so the defaults stood). */
12
+ export const POLICY_SOURCES = ['default', 'file', 'invalid'];
13
+ export const policyPath = (storeDir) => join(storeDir, 'gate-policy.json');
14
+ const KEYS = new Set(['v', 'classes', 'youngDays', 'holdbackPercent']);
15
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ const integerIn = (value, low, high) => (typeof value === 'number' && Number.isInteger(value) && value >= low && value <= high ? value : undefined);
17
+ /** `undefined` for anything outside the closed vocabulary: an unknown key, class, or action, a number out of range, or another version. */
18
+ const fit = (parsed) => {
19
+ if (!isObject(parsed) || Object.keys(parsed).some((key) => !KEYS.has(key)))
20
+ return undefined;
21
+ const version = Reflect.get(parsed, 'v');
22
+ if (version !== undefined && version !== 1)
23
+ return undefined;
24
+ const given = Reflect.get(parsed, 'classes');
25
+ if (given !== undefined && (!isObject(given) || Object.keys(given).some((key) => !POLICY_CLASSES.some((known) => known === key))))
26
+ return undefined;
27
+ const classes = { ...DEFAULT_POLICY.classes };
28
+ for (const cls of POLICY_CLASSES) {
29
+ const action = isObject(given) ? Reflect.get(given, cls) : undefined;
30
+ if (action === undefined)
31
+ continue;
32
+ const allowed = ALLOWED[cls].find((one) => one === action);
33
+ if (allowed === undefined)
34
+ return undefined;
35
+ classes[cls] = allowed;
36
+ }
37
+ const days = Reflect.get(parsed, 'youngDays');
38
+ const share = Reflect.get(parsed, 'holdbackPercent');
39
+ const youngDays = days === undefined ? DEFAULT_POLICY.youngDays : integerIn(days, 1, 365);
40
+ const holdbackPercent = share === undefined ? DEFAULT_POLICY.holdbackPercent : integerIn(share, 0, 100);
41
+ return youngDays === undefined || holdbackPercent === undefined ? undefined : { classes, youngDays, holdbackPercent };
42
+ };
43
+ /** Never throws. A file that is not there is the defaults; one that is there and cannot be read or does not fit is the defaults too, and says `invalid`. */
44
+ export const loadPolicy = async (storeDir) => {
45
+ let body;
46
+ try {
47
+ body = await readFile(policyPath(storeDir), 'utf8');
48
+ }
49
+ catch (error) {
50
+ const code = isObject(error) ? Reflect.get(error, 'code') : undefined;
51
+ return { policy: DEFAULT_POLICY, source: code === 'ENOENT' ? 'default' : 'invalid' };
52
+ }
53
+ let parsed;
54
+ try {
55
+ // Windows PowerShell 5.1 writes UTF-8 with a byte order mark, and `JSON.parse` refuses it: an operator's `block` would silently not apply.
56
+ parsed = JSON.parse(body.charCodeAt(0) === 0xfeff ? body.slice(1) : body);
57
+ }
58
+ catch {
59
+ return { policy: DEFAULT_POLICY, source: 'invalid' };
60
+ }
61
+ const policy = fit(parsed);
62
+ return policy === undefined ? { policy: DEFAULT_POLICY, source: 'invalid' } : { policy, source: 'file' };
63
+ };
64
+ /** What `oars gate policy` prints. */
65
+ export const formatPolicy = (read, file) => {
66
+ const origin = read.source === 'file' ? `Policy in force: ${file}` : read.source === 'default' ? `Policy in force: defaults (no file at ${file})` : `Policy in force: defaults (${file} is unreadable or invalid, so it is ignored)`;
67
+ return [
68
+ origin,
69
+ ...POLICY_CLASSES.map((cls) => `${cls}: ${read.policy.classes[cls]}`),
70
+ `Young name: first published within ${String(read.policy.youngDays)} days`,
71
+ `Holdback: ${String(read.policy.holdbackPercent)}% of notes are not shown`,
72
+ ].join('\n');
73
+ };
@@ -0,0 +1,120 @@
1
+ import type { PolicySource } from './policy.js';
2
+ import { CROSSING_CLASSES, CROSSING_FORMS, type CrossingClass, type CrossingForm, type UnreadShape, type VersionForm } from './checks.js';
3
+ export declare const GATE_VERSION = 1;
4
+ /** Where a row came from. A replay row is read from a transcript, a hook row was written as the call crossed, and `both` is a hook row a replay then filled in. */
5
+ export declare const VIAS: readonly ["hook", "replay", "both"];
6
+ export type Via = (typeof VIAS)[number];
7
+ /** `not-read` on a field says no one has read it for this row yet: a hook row has no transcript, so the fields a transcript gives read `not-read` until a replay fills them. */
8
+ export declare const NOT_READ = "not-read";
9
+ /** Every program a crossing can name: the package managers and runners the gate reads, their `python -m` forms, and the `mcp` and `skill` tools. */
10
+ export declare const PROGRAMS: readonly ["npm", "pnpm", "yarn", "bun", "npx", "pnpx", "bunx", "pip", "python -m pip", "python -m pipx", "python -m uv", "uv", "uvx", "pipx", "poetry", "pdm", "rye", "pipenv", "conda", "mamba", "micromamba", "mcp", "skill"];
11
+ export type Program = (typeof PROGRAMS)[number];
12
+ export declare const ORIGINS: readonly ["prompt", "repo-file", "web", "tool-output", "skill", "harness", "none-seen", "not-read"];
13
+ export type Origin = (typeof ORIGINS)[number];
14
+ export declare const NOVELTIES: readonly ["fleet", "repository", "session", "none", "not-read"];
15
+ export type Novelty = (typeof NOVELTIES)[number];
16
+ export declare const DECIDERS: readonly ["operator", "agent", "not-read"];
17
+ export type Decider = (typeof DECIDERS)[number];
18
+ export declare const PERMISSIONS: readonly ["default", "acceptEdits", "plan", "bypassPermissions", "dontAsk", "auto", "other", "unknown"];
19
+ export type Permission = (typeof PERMISSIONS)[number];
20
+ export declare const STATUSES: readonly ["ok", "error", "refused", "none", "not-read"];
21
+ export type Status = (typeof STATUSES)[number];
22
+ export declare const NEXTS: readonly ["proceeded", "retried", "routed-around", "changed-name", "stopped", "no-result", "not-read"];
23
+ export type Next = (typeof NEXTS)[number];
24
+ export declare const MISSES: readonly ["none", "not-found", "not-read"];
25
+ export type Miss = (typeof MISSES)[number];
26
+ export declare const AGAINST_LATEST: readonly ["not-read", "latest", "not-latest", "range", "unstated", "unknown"];
27
+ export type AgainstLatest = (typeof AGAINST_LATEST)[number];
28
+ export declare const REGISTRY_STATES: readonly ["not-read", "found", "not-found", "unreachable"];
29
+ export type RegistryState = (typeof REGISTRY_STATES)[number];
30
+ export declare const REGISTRY_AGES: readonly ["not-read", "after-crossing", "under-7-days", "under-30-days", "under-1-year", "older", "unknown"];
31
+ export type RegistryAge = (typeof REGISTRY_AGES)[number];
32
+ export declare const DEPTHS: readonly ["main", "subagent"];
33
+ export type Depth = (typeof DEPTHS)[number];
34
+ /** Whether a note that would show did (`shown`), was held back by the policy's holdback (`held`), or there was none to show or the line was a block (`none`). A replay shows nothing, so it reads `none`. */
35
+ export declare const ARMS: readonly ["none", "shown", "held"];
36
+ export type Arm = (typeof ARMS)[number];
37
+ /** The fixed ids of the lines the hook can show; the words are in `notes.ts`. `none` is no line shown, a held note included. */
38
+ export declare const LINE_IDS: readonly ["none", "young-name-note", "not-found-note", "not-found-block"];
39
+ export type LineId = (typeof LINE_IDS)[number];
40
+ export interface CrossingRecord {
41
+ kind: 'crossing';
42
+ v: number;
43
+ /** Absent in a row an earlier build wrote: those were all replays. */
44
+ via?: Via;
45
+ day: string | null;
46
+ class: CrossingClass;
47
+ form: CrossingForm;
48
+ program: Program;
49
+ versionForm: VersionForm;
50
+ statusReports: boolean;
51
+ origin: Origin;
52
+ novelty: Novelty;
53
+ decidedBy: Decider;
54
+ permission: Permission;
55
+ status: Status;
56
+ next: Next;
57
+ removedLater: boolean | typeof NOT_READ;
58
+ depth: Depth;
59
+ miss: Miss;
60
+ againstLatest: AgainstLatest;
61
+ registryState: RegistryState;
62
+ registryAge: RegistryAge;
63
+ arm: Arm;
64
+ lineShown: LineId;
65
+ /** The line a held note would have shown. Recorded apart from `lineShown` (which stays `none` when held) so the arms can be compared line by line; a row cannot be backfilled. */
66
+ lineHeld: LineId;
67
+ /** Which policy stood when the hook wrote the row. Absent in a replay row and in a row an earlier build wrote: no policy was read. */
68
+ policy?: PolicySource;
69
+ session: string;
70
+ agent: string | null;
71
+ toolUseId: string | null;
72
+ ts: string | null;
73
+ name: string;
74
+ version: string | null;
75
+ }
76
+ /** The run's own books, so `report` can say what this replay read and could not. Counts and a day: class P. */
77
+ export interface RunRow {
78
+ kind: 'run';
79
+ v: number;
80
+ day: string;
81
+ transcripts: number;
82
+ unreadable: number;
83
+ badLines: number;
84
+ calls: number;
85
+ crossings: number;
86
+ unread: Partial<Record<UnreadShape, number>>;
87
+ registryRead: boolean;
88
+ }
89
+ /** One kept registry answer, the cache of the `--registry-state` option. Held here so a replay writes one file. `name`, `latest`, and `created` are class A; `created` is a full timestamp, which class P cannot hold. */
90
+ export interface RegistryRow {
91
+ kind: 'registry';
92
+ v: number;
93
+ day: string;
94
+ /** When the answer was read, to the millisecond; a row an earlier build wrote has none. A not-found answer is trusted for minutes, not days. */
95
+ ts?: string;
96
+ /** Which folder the answer came from (`folderKey`): a folder's own config can name another registry, so a kept answer answers only that folder. A row with none is not fresh. Class A. */
97
+ folder?: string;
98
+ class: CrossingClass;
99
+ state: 'found' | 'not-found';
100
+ created: string | null;
101
+ name: string;
102
+ latest: string | null;
103
+ }
104
+ export type Row = CrossingRecord | RunRow | RegistryRow;
105
+ /** A truncated SHA-256 of the folder an ask ran in, so a kept answer names its folder without storing the path. */
106
+ export declare const folderKey: (cwd: string) => string;
107
+ /** The harness's permission mode, in the closed vocabulary: a mode this version does not know reads `other`. */
108
+ export declare const permissionOf: (value: string) => Permission;
109
+ /** A line this version cannot read is skipped: a torn tail or a newer row kind says nothing about the rest. */
110
+ export declare const parseRows: (text: string) => Row[];
111
+ export { CROSSING_CLASSES, CROSSING_FORMS };
112
+ /**
113
+ * Every crossing once. The rows of `replayed` are replay rows and the rows of `live` are hook rows (or merged ones); rows with the same session, tool use id, class, and
114
+ * name are one row, whether two sources saw it or one wrote it twice, so the same name twice in one call counts once. A row with no tool use id cannot be paired and stays.
115
+ */
116
+ export declare const mergeCrossings: (replayed: readonly CrossingRecord[], live: readonly CrossingRecord[]) => CrossingRecord[];
117
+ /** The crossings of a file's rows with each counted once: replay rows and hook rows of the same call become one. */
118
+ export declare const crossingsOf: (rows: readonly Row[]) => CrossingRecord[];
119
+ /** The hook-written rows (and merged ones) of a file, which a replay keeps when it rewrites the rest. */
120
+ export declare const liveCrossings: (rows: readonly Row[]) => CrossingRecord[];
@@ -0,0 +1,125 @@
1
+ // The shape of `crossings.jsonl`, in closed vocabulary. Field classes are in `docs/gate.md` and `docs/sessions-store.md`: a name and a stated
2
+ // version are class A because a private package's name can identify private work, and no path, working folder, or command text is kept at all.
3
+ import { createHash } from 'node:crypto';
4
+ import { CROSSING_CLASSES, CROSSING_FORMS } from './checks.js';
5
+ export const GATE_VERSION = 1;
6
+ /** Where a row came from. A replay row is read from a transcript, a hook row was written as the call crossed, and `both` is a hook row a replay then filled in. */
7
+ export const VIAS = ['hook', 'replay', 'both'];
8
+ /** `not-read` on a field says no one has read it for this row yet: a hook row has no transcript, so the fields a transcript gives read `not-read` until a replay fills them. */
9
+ export const NOT_READ = 'not-read';
10
+ /** Every program a crossing can name: the package managers and runners the gate reads, their `python -m` forms, and the `mcp` and `skill` tools. */
11
+ export const PROGRAMS = ['npm', 'pnpm', 'yarn', 'bun', 'npx', 'pnpx', 'bunx', 'pip', 'python -m pip', 'python -m pipx', 'python -m uv', 'uv', 'uvx', 'pipx', 'poetry', 'pdm', 'rye', 'pipenv', 'conda', 'mamba', 'micromamba', 'mcp', 'skill'];
12
+ export const ORIGINS = ['prompt', 'repo-file', 'web', 'tool-output', 'skill', 'harness', 'none-seen', 'not-read'];
13
+ export const NOVELTIES = ['fleet', 'repository', 'session', 'none', 'not-read'];
14
+ export const DECIDERS = ['operator', 'agent', 'not-read'];
15
+ export const PERMISSIONS = ['default', 'acceptEdits', 'plan', 'bypassPermissions', 'dontAsk', 'auto', 'other', 'unknown'];
16
+ export const STATUSES = ['ok', 'error', 'refused', 'none', 'not-read'];
17
+ export const NEXTS = ['proceeded', 'retried', 'routed-around', 'changed-name', 'stopped', 'no-result', 'not-read'];
18
+ export const MISSES = ['none', 'not-found', 'not-read'];
19
+ export const AGAINST_LATEST = ['not-read', 'latest', 'not-latest', 'range', 'unstated', 'unknown'];
20
+ export const REGISTRY_STATES = ['not-read', 'found', 'not-found', 'unreachable'];
21
+ export const REGISTRY_AGES = ['not-read', 'after-crossing', 'under-7-days', 'under-30-days', 'under-1-year', 'older', 'unknown'];
22
+ export const DEPTHS = ['main', 'subagent'];
23
+ /** Whether a note that would show did (`shown`), was held back by the policy's holdback (`held`), or there was none to show or the line was a block (`none`). A replay shows nothing, so it reads `none`. */
24
+ export const ARMS = ['none', 'shown', 'held'];
25
+ /** The fixed ids of the lines the hook can show; the words are in `notes.ts`. `none` is no line shown, a held note included. */
26
+ export const LINE_IDS = ['none', 'young-name-note', 'not-found-note', 'not-found-block'];
27
+ /** A truncated SHA-256 of the folder an ask ran in, so a kept answer names its folder without storing the path. */
28
+ export const folderKey = (cwd) => createHash('sha256').update(cwd).digest('hex').slice(0, 16);
29
+ /** The harness's permission mode, in the closed vocabulary: a mode this version does not know reads `other`. */
30
+ export const permissionOf = (value) => PERMISSIONS.find((known) => known === value) ?? 'other';
31
+ const isRow = (value) => {
32
+ const kind = typeof value === 'object' && value !== null ? Reflect.get(value, 'kind') : undefined;
33
+ return kind === 'crossing' || kind === 'run' || kind === 'registry';
34
+ };
35
+ /** A line this version cannot read is skipped: a torn tail or a newer row kind says nothing about the rest. */
36
+ export const parseRows = (text) => text.split('\n').flatMap((line) => {
37
+ if (line === '')
38
+ return [];
39
+ try {
40
+ const parsed = JSON.parse(line);
41
+ return isRow(parsed) ? [parsed] : [];
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ });
47
+ export { CROSSING_CLASSES, CROSSING_FORMS };
48
+ const isLive = (row) => row.via === 'hook' || row.via === 'both';
49
+ /** A hook never saw these, so a hook row says them as `not-read`, `null`, or `unknown`: then the replay's value stands. */
50
+ const unsaid = (value) => value === undefined || value === NOT_READ || value === null || value === 'unknown';
51
+ const pick = (key, live, replayed) => (unsaid(live[key]) ? replayed[key] : live[key]);
52
+ /**
53
+ * One crossing seen twice. The hook's fields from the time of the crossing win: its clock, its permission mode, the registry as it stood, and what was new. A replay's
54
+ * fields from the transcript fill in what the hook could not read. A row that came out of an earlier merge is a hook row again here, so a second replay refreshes
55
+ * only the transcript fields and keeps what the hook saw.
56
+ */
57
+ const merged = (live, replayed) => ({
58
+ ...replayed,
59
+ via: 'both',
60
+ // The folder as it was at the crossing: a bin installed or removed since must not turn a fetch into a local run, or the reverse.
61
+ form: live.form,
62
+ day: pick('day', live, replayed),
63
+ ts: pick('ts', live, replayed),
64
+ version: pick('version', live, replayed),
65
+ permission: pick('permission', live, replayed),
66
+ novelty: pick('novelty', live, replayed),
67
+ // One reading, so all three come from one side: the registry as it stood at the crossing when the hook read it, else the replay's. Picked field by field, a hook's
68
+ // `unreachable` (with `unknown` beside it) would take a later replay's `not-latest`, a row that contradicts itself.
69
+ // A local bin run was never asked of the registry (docs/gate.md), so a replay that saw no bin and asked cannot give it a reading.
70
+ ...(live.registryState === NOT_READ && live.form !== 'local-bin-run'
71
+ ? { againstLatest: replayed.againstLatest, registryState: replayed.registryState, registryAge: replayed.registryAge }
72
+ : live.form === 'local-bin-run'
73
+ ? { againstLatest: NOT_READ, registryState: NOT_READ, registryAge: NOT_READ }
74
+ : { againstLatest: live.againstLatest, registryState: live.registryState, registryAge: live.registryAge }),
75
+ ...(live.policy === undefined ? {} : { policy: live.policy }),
76
+ arm: pick('arm', live, replayed),
77
+ lineShown: pick('lineShown', live, replayed),
78
+ // A hook row from an earlier build has no `lineHeld`: `unsaid` reads it as nothing said, so the replay's `none` stands.
79
+ lineHeld: pick('lineHeld', live, replayed),
80
+ });
81
+ /** The key a call crossing is counted by: nothing counts twice when a row has a tool use id and another row has the same one. */
82
+ const crossingKey = (row) => (row.toolUseId === null ? null : [row.session, row.toolUseId, row.class, row.name].join('\0'));
83
+ /** The first row of each key; a row with no key cannot repeat, so it stays. */
84
+ const once = (rows) => {
85
+ const seen = new Set();
86
+ return rows.filter((row) => {
87
+ const key = crossingKey(row);
88
+ if (key === null)
89
+ return true;
90
+ if (seen.has(key))
91
+ return false;
92
+ seen.add(key);
93
+ return true;
94
+ });
95
+ };
96
+ /**
97
+ * Every crossing once. The rows of `replayed` are replay rows and the rows of `live` are hook rows (or merged ones); rows with the same session, tool use id, class, and
98
+ * name are one row, whether two sources saw it or one wrote it twice, so the same name twice in one call counts once. A row with no tool use id cannot be paired and stays.
99
+ */
100
+ export const mergeCrossings = (replayed, live) => {
101
+ const hooked = once(live);
102
+ const waiting = new Map();
103
+ for (const row of hooked) {
104
+ const key = crossingKey(row);
105
+ if (key !== null)
106
+ waiting.set(key, row);
107
+ }
108
+ const paired = new Set();
109
+ const result = once(replayed).map((row) => {
110
+ const key = crossingKey(row);
111
+ const mate = key === null ? undefined : waiting.get(key);
112
+ if (mate === undefined)
113
+ return row;
114
+ paired.add(mate);
115
+ return merged(mate, row);
116
+ });
117
+ return [...result, ...hooked.filter((row) => !paired.has(row))];
118
+ };
119
+ /** The crossings of a file's rows with each counted once: replay rows and hook rows of the same call become one. */
120
+ export const crossingsOf = (rows) => {
121
+ const crossings = rows.filter((row) => row.kind === 'crossing');
122
+ return mergeCrossings(crossings.filter((row) => !isLive(row)), crossings.filter(isLive));
123
+ };
124
+ /** The hook-written rows (and merged ones) of a file, which a replay keeps when it rewrites the rest. */
125
+ export const liveCrossings = (rows) => rows.filter((row) => row.kind === 'crossing' && isLive(row));
@@ -0,0 +1,10 @@
1
+ import type { Crossing } from './checks.js';
2
+ import type { AgainstLatest, RegistryAge } from './record.js';
3
+ import type { RegistryAnswer } from './registry.js';
4
+ export declare const DAY_MS = 86400000;
5
+ /** Only these managers read the registry the `npm view` and `pip index` asks reach. A `pnpm` (its own global `rc`), `poetry`, `uv`, `yarn`, or `conda` install may use one they do not. */
6
+ export declare const ASKED_PROGRAMS: ReadonlySet<string>;
7
+ export declare const ageOf: (answer: RegistryAnswer, ts: string | null) => RegistryAge;
8
+ export declare const againstLatestOf: (crossing: Crossing, answer: RegistryAnswer) => AgainstLatest;
9
+ /** An answer reaches a row only when that row's own program was asked and its command left the registry to the default: the same name can cross twice. */
10
+ export declare const askedOf: (crossing: Crossing) => boolean;
@@ -0,0 +1,30 @@
1
+ export const DAY_MS = 86_400_000;
2
+ /** Only these managers read the registry the `npm view` and `pip index` asks reach. A `pnpm` (its own global `rc`), `poetry`, `uv`, `yarn`, or `conda` install may use one they do not. */
3
+ export const ASKED_PROGRAMS = new Set(['npm', 'npx', 'pip', 'python -m pip']);
4
+ export const ageOf = (answer, ts) => {
5
+ if (answer.created === null || ts === null)
6
+ return 'unknown';
7
+ const days = (Date.parse(ts) - Date.parse(answer.created)) / DAY_MS;
8
+ if (Number.isNaN(days))
9
+ return 'unknown';
10
+ if (days < 0)
11
+ return 'after-crossing';
12
+ if (days < 7)
13
+ return 'under-7-days';
14
+ if (days < 30)
15
+ return 'under-30-days';
16
+ return days < 365 ? 'under-1-year' : 'older';
17
+ };
18
+ export const againstLatestOf = (crossing, answer) => {
19
+ if (answer.state !== 'found')
20
+ return 'unknown';
21
+ if (crossing.versionForm === 'unstated')
22
+ return 'unstated';
23
+ if (crossing.versionForm === 'range')
24
+ return 'range';
25
+ if (crossing.version === 'latest' || crossing.version === answer.latest)
26
+ return 'latest';
27
+ return crossing.versionForm === 'pinned' ? 'not-latest' : 'unknown';
28
+ };
29
+ /** An answer reaches a row only when that row's own program was asked and its command left the registry to the default: the same name can cross twice. */
30
+ export const askedOf = (crossing) => crossing.registryAsked && crossing.form !== 'local-bin-run' && ASKED_PROGRAMS.has(crossing.program);
@@ -0,0 +1,56 @@
1
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
2
+ import type { CrossingClass } from './checks.js';
3
+ export interface RegistryAsk {
4
+ class: CrossingClass;
5
+ name: string;
6
+ cwd: string;
7
+ /** Aborting it stops the manager the ask started, and the processes that manager started (the hook does so before it hands the name to a helper). */
8
+ signal?: AbortSignal;
9
+ }
10
+ export interface RegistryAnswer {
11
+ state: 'found' | 'not-found' | 'unreachable';
12
+ latest: string | null;
13
+ created: string | null;
14
+ }
15
+ export type RegistryReader = (ask: RegistryAsk) => Promise<RegistryAnswer>;
16
+ /** What a manager's run came back with: the exit code, null when it could not be started or ran out of time. */
17
+ export interface ManagerRun {
18
+ code: number | null;
19
+ stdout: string;
20
+ stderr: string;
21
+ }
22
+ export type ManagerRunner = (program: string, args: readonly string[], cwd: string, signal?: AbortSignal) => Promise<ManagerRun>;
23
+ /** Whether a name can be asked about: a package class, spelled as a registry could hold it. */
24
+ export declare const askable: (cls: CrossingClass, name: string) => boolean;
25
+ /** `npm view <name> dist-tags.latest time.created --json`, as an object or a one-element array. A not-found answer is an `E404` on either stream. */
26
+ export declare const readNpmView: (run: ManagerRun) => RegistryAnswer;
27
+ /**
28
+ * `pip index versions <name>` prints `name (latest)` first. It states no dates, so a Python name's age stays unknown. A Python name never reads
29
+ * `not-found`: pip prints "No matching distribution" for a missing name and for a down index alike, so the line cannot tell them apart.
30
+ */
31
+ export declare const readPipIndex: (run: ManagerRun) => RegistryAnswer;
32
+ /**
33
+ * How a manager is started. `cwd` comes from a transcript, and `cmd.exe` searches the current directory before PATH, so `NoDefaultCurrentDirectoryInExePath`
34
+ * keeps an `npm.cmd` left there from running: nothing here runs package code.
35
+ */
36
+ export declare const managerOptions: (cwd: string, platform?: NodeJS.Platform, timeout?: number) => {
37
+ cwd: string;
38
+ timeout: number;
39
+ shell: boolean;
40
+ windowsHide: boolean;
41
+ encoding: "utf8";
42
+ env: {
43
+ NoDefaultCurrentDirectoryInExePath: string;
44
+ };
45
+ };
46
+ /** What `stopTree` needs of `spawn`: a program, its arguments, and a child to hear an error from. */
47
+ export type StartProcess = (program: string, args: readonly string[], options: SpawnOptions) => {
48
+ on: (event: 'error', listener: () => void) => unknown;
49
+ };
50
+ /**
51
+ * Stops a manager and what it started. On Windows `shell: true` puts `cmd.exe` first, and killing it leaves the manager running, so the whole tree goes (`taskkill /T`); elsewhere
52
+ * the manager is the child itself.
53
+ */
54
+ export declare const stopTree: (child: Pick<ChildProcess, "pid" | "kill">, platform?: NodeJS.Platform, start?: StartProcess) => void;
55
+ /** The reader the option uses. `npm view` and `pip index` run no package code; the manager's own cache is the manager's. */
56
+ export declare const managerReader: (given?: ManagerRunner, timeout?: number) => RegistryReader;
@@ -0,0 +1,94 @@
1
+ // The `--registry-state` option: what a registry says about a name, asked through `npm` or `pip` in the folder the call ran in. For a crossing made
2
+ // with npm, npx, or pip whose command left the registry, the config, and the folder to their defaults (the replay asks for no other crossing),
3
+ // that is the registry the manager would contact, because those read the same project and user config; so a private name does not go to a public
4
+ // registry on the gate's account. Off by default; nothing here runs unless the operator asks.
5
+ import { execFile, spawn } from 'node:child_process';
6
+ const TIMEOUT_MS = 20_000;
7
+ const UNREACHABLE = { state: 'unreachable', latest: null, created: null };
8
+ const NOT_FOUND = { state: 'not-found', latest: null, created: null };
9
+ // On Windows the managers are `.cmd` shims, which run only through a shell, so a name reaches a command line. These accept only what a registry
10
+ // can hold, which has no shell character, and nothing that a manager would read as a flag.
11
+ const NPM_NAME = /^(?:@[a-z0-9~][a-z0-9._~-]*\/)?[a-z0-9~][a-z0-9._~-]*$/i;
12
+ const PYTHON_NAME = /^[a-z0-9][a-z0-9._-]*$/i;
13
+ /** Whether a name can be asked about: a package class, spelled as a registry could hold it. */
14
+ export const askable = (cls, name) => (cls === 'npm-package' ? NPM_NAME.test(name) : cls === 'python-package' && PYTHON_NAME.test(name));
15
+ const textField = (value, key) => {
16
+ const field = typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined;
17
+ return typeof field === 'string' && field !== '' ? field : null;
18
+ };
19
+ /** `npm view <name> dist-tags.latest time.created --json`, as an object or a one-element array. A not-found answer is an `E404` on either stream. */
20
+ export const readNpmView = (run) => {
21
+ if (/\bE404\b/.test(run.stdout) || /\bE404\b/.test(run.stderr))
22
+ return NOT_FOUND;
23
+ if (run.code !== 0)
24
+ return UNREACHABLE;
25
+ try {
26
+ const answer = JSON.parse(run.stdout);
27
+ // npm 12 wraps the two-field answer in a one-element array; older npm prints the object itself.
28
+ const parsed = Array.isArray(answer) ? answer[0] : answer;
29
+ const latest = textField(parsed, 'dist-tags.latest');
30
+ return latest === null ? UNREACHABLE : { state: 'found', latest, created: textField(parsed, 'time.created') };
31
+ }
32
+ catch {
33
+ return UNREACHABLE;
34
+ }
35
+ };
36
+ /**
37
+ * `pip index versions <name>` prints `name (latest)` first. It states no dates, so a Python name's age stays unknown. A Python name never reads
38
+ * `not-found`: pip prints "No matching distribution" for a missing name and for a down index alike, so the line cannot tell them apart.
39
+ */
40
+ export const readPipIndex = (run) => {
41
+ if (run.code !== 0)
42
+ return UNREACHABLE;
43
+ const latest = /^\S+ \(([^)]+)\)/m.exec(run.stdout)?.[1];
44
+ return latest === undefined ? UNREACHABLE : { state: 'found', latest, created: null };
45
+ };
46
+ /**
47
+ * How a manager is started. `cwd` comes from a transcript, and `cmd.exe` searches the current directory before PATH, so `NoDefaultCurrentDirectoryInExePath`
48
+ * keeps an `npm.cmd` left there from running: nothing here runs package code.
49
+ */
50
+ export const managerOptions = (cwd, platform = process.platform, timeout = TIMEOUT_MS) => ({
51
+ cwd,
52
+ timeout,
53
+ shell: platform === 'win32',
54
+ windowsHide: true,
55
+ encoding: 'utf8',
56
+ env: { ...process.env, NoDefaultCurrentDirectoryInExePath: '1' },
57
+ });
58
+ /**
59
+ * Stops a manager and what it started. On Windows `shell: true` puts `cmd.exe` first, and killing it leaves the manager running, so the whole tree goes (`taskkill /T`); elsewhere
60
+ * the manager is the child itself.
61
+ */
62
+ export const stopTree = (child, platform = process.platform, start = spawn) => {
63
+ if (platform === 'win32' && child.pid !== undefined) {
64
+ start('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }).on('error', () => undefined);
65
+ return;
66
+ }
67
+ child.kill();
68
+ };
69
+ const runnerWith = (timeout) => (program, args, cwd, signal) => new Promise((resolve) => {
70
+ const child = execFile(program, [...args], managerOptions(cwd, process.platform, timeout), (error, stdout, stderr) => {
71
+ const code = error === null ? 0 : typeof error.code === 'number' ? error.code : null;
72
+ resolve({ code, stdout, stderr });
73
+ });
74
+ if (signal?.aborted === true)
75
+ stopTree(child);
76
+ else
77
+ signal?.addEventListener('abort', () => { stopTree(child); }, { once: true });
78
+ });
79
+ /**
80
+ * With a deadline the child gets its own: the kill `execFile`'s timeout sends never comes when the caller's process exits first, and npm's default fetch retries run
81
+ * for minutes against an unreachable registry. Without one (a replay) the manager keeps its defaults.
82
+ */
83
+ const npmLimits = (timeout) => (timeout === undefined ? [] : [`--fetch-timeout=${String(timeout)}`, '--fetch-retries=0']);
84
+ /** pip counts its socket timeout in whole seconds. */
85
+ const pipLimits = (timeout) => (timeout === undefined ? [] : ['--timeout', String(Math.ceil(timeout / 1000)), '--retries', '0']);
86
+ /** The reader the option uses. `npm view` and `pip index` run no package code; the manager's own cache is the manager's. */
87
+ export const managerReader = (given, timeout) => async ({ class: cls, name, cwd, signal }) => {
88
+ const run = given ?? runnerWith(timeout ?? TIMEOUT_MS);
89
+ if (!askable(cls, name))
90
+ return UNREACHABLE;
91
+ if (cls === 'npm-package')
92
+ return readNpmView(await run('npm', ['view', name, 'dist-tags.latest', 'time.created', '--json', '--logs-max=0', ...npmLimits(timeout)], cwd, signal));
93
+ return readPipIndex(await run('pip', ['index', 'versions', name, '--disable-pip-version-check', ...pipLimits(timeout)], cwd, signal));
94
+ };
@@ -0,0 +1,20 @@
1
+ import { type SessionsRuntime } from '../sessions/store.js';
2
+ import { type RunRow } from './record.js';
3
+ import { type RegistryReader } from './registry.js';
4
+ export declare const crossingsFile: (storeDir: string) => string;
5
+ export declare const REGISTRY_NOT_READ = "registry state not read";
6
+ export interface ReplayOptions {
7
+ /** Project folders to read; empty means the saved allowlist. */
8
+ allow: readonly string[];
9
+ /** Only with `--registry-state`. Absent, nothing leaves this machine. */
10
+ registry?: RegistryReader;
11
+ now: () => number;
12
+ }
13
+ export interface ReplayResult {
14
+ run: RunRow;
15
+ file: string;
16
+ text: string;
17
+ }
18
+ /** A kept registry answer older than this many days is asked again: a `latest` goes stale, and a row would otherwise read "as of this run" forever. */
19
+ export declare const REGISTRY_MAX_DAYS = 30;
20
+ export declare const replay: (runtime: Pick<SessionsRuntime, "storeDir" | "projectsRoot">, options: ReplayOptions) => Promise<ReplayResult>;