@manyoars/oars 0.2.0 → 0.3.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 +132 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +367 -88
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.js +2 -2
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +257 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +25 -0
- package/dist/issuer.d.ts +7 -1
- package/dist/issuer.js +18 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +188 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +37 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +477 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RepoSize } from './public-vocabulary.js';
|
|
2
|
+
import { type LanguageShare, type RepoFilesBucket, type Strata } from './schema.js';
|
|
3
|
+
import type { CanonicalAction } from './actions.js';
|
|
4
|
+
/** How many of the given steps only look. */
|
|
5
|
+
export declare const exploreCount: (actions: readonly CanonicalAction[]) => number;
|
|
6
|
+
/**
|
|
7
|
+
* A language's share of the touched files: absent, incidental (under 10 percent), secondary (10 up to 50), primary (50 and over). The lower edge is inclusive.
|
|
8
|
+
* Integers are compared, not a quotient, so 1 of 10 files is exactly 10 percent.
|
|
9
|
+
*/
|
|
10
|
+
export declare const languageShareBucket: (files: number, total: number) => LanguageShare;
|
|
11
|
+
export declare const repoFilesBucketOf: (size: RepoSize | null) => RepoFilesBucket | null;
|
|
12
|
+
/**
|
|
13
|
+
* A package the vocabulary marks as a framework, by `<ecosystem>:<name>` as `resolveName` writes it, to the framework's name in `FRAMEWORKS`. A package
|
|
14
|
+
* is not a program or a file, so this is the one source of `framework` that names a registry entry.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FRAMEWORK_OF_PACKAGE: ReadonlyMap<string, string>;
|
|
17
|
+
/** The strata of a file's evidence but `explore`, which the caller counts for the file and for each prompt. Files are counted, not touches: a path read ten times is one. */
|
|
18
|
+
export declare const strataOf: (evidence: {
|
|
19
|
+
paths: readonly string[];
|
|
20
|
+
packages: readonly string[];
|
|
21
|
+
size: RepoSize | null;
|
|
22
|
+
}) => Omit<Strata, "explore">;
|
|
23
|
+
/** A stored language as it pools: a member of the pinned list, else `other`. */
|
|
24
|
+
export declare const projectLanguage: (stored: string) => string;
|
|
25
|
+
/** A stored share bucket as it pools: itself when it is one of the four, else null (no bucket is guessed from a value this code does not know). */
|
|
26
|
+
export declare const projectLanguageShare: (stored: string) => LanguageShare | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The stack strata a capture stores (pool vocabulary spec Sections 1.1 and 7): language and its share of the touched files, the repository's file
|
|
2
|
+
// count by order of magnitude, the framework a resolved package shows, and how many steps only looked. Every value is a member of a closed list or a count,
|
|
3
|
+
// so no path, package name, or program text is kept. The language and share projections are here; the rest that gate these at deposit are in `projection.ts`.
|
|
4
|
+
import { LANGUAGE_OF_EXTENSION, LANGUAGES } from './public-vocabulary.js';
|
|
5
|
+
import { LANGUAGE_SHARES } from './schema.js';
|
|
6
|
+
/** The steps that only look at the code or the web. `file.read`, `file.search`, `web.search`, and `web.fetch`; a shell step is not counted, because what it does is not known from its action. */
|
|
7
|
+
const EXPLORE_ACTIONS = new Set(['file.read', 'file.search', 'web.search', 'web.fetch']);
|
|
8
|
+
/** How many of the given steps only look. */
|
|
9
|
+
export const exploreCount = (actions) => actions.filter((action) => EXPLORE_ACTIONS.has(action)).length;
|
|
10
|
+
/**
|
|
11
|
+
* A language's share of the touched files: absent, incidental (under 10 percent), secondary (10 up to 50), primary (50 and over). The lower edge is inclusive.
|
|
12
|
+
* Integers are compared, not a quotient, so 1 of 10 files is exactly 10 percent.
|
|
13
|
+
*/
|
|
14
|
+
export const languageShareBucket = (files, total) => {
|
|
15
|
+
if (files <= 0 || total <= 0)
|
|
16
|
+
return '0';
|
|
17
|
+
if (files * 100 < total * 10)
|
|
18
|
+
return 'under-10';
|
|
19
|
+
return files * 100 < total * 50 ? '10-50' : 'over-50';
|
|
20
|
+
};
|
|
21
|
+
/** The repository's size bucket (`repoSizeOf`) as the pool's `repoFiles` bucket: both cut at 100, 1000, 10000, and 100000 with the lower edge inclusive. */
|
|
22
|
+
const REPO_FILES_OF_SIZE = new Map([
|
|
23
|
+
['xs', 'under-100'],
|
|
24
|
+
['s', '100-1k'],
|
|
25
|
+
['m', '1k-10k'],
|
|
26
|
+
['l', '10k-100k'],
|
|
27
|
+
['xl', 'over-100k'],
|
|
28
|
+
]);
|
|
29
|
+
export const repoFilesBucketOf = (size) => (size === null ? null : (REPO_FILES_OF_SIZE.get(size) ?? null));
|
|
30
|
+
/**
|
|
31
|
+
* A package the vocabulary marks as a framework, by `<ecosystem>:<name>` as `resolveName` writes it, to the framework's name in `FRAMEWORKS`. A package
|
|
32
|
+
* is not a program or a file, so this is the one source of `framework` that names a registry entry.
|
|
33
|
+
*/
|
|
34
|
+
export const FRAMEWORK_OF_PACKAGE = new Map([
|
|
35
|
+
['npm:vite', 'vite'],
|
|
36
|
+
['npm:vitest', 'vitest'],
|
|
37
|
+
['npm:astro', 'astro'],
|
|
38
|
+
['npm:next', 'next'],
|
|
39
|
+
['npm:nuxt', 'nuxt'],
|
|
40
|
+
['npm:svelte', 'svelte'],
|
|
41
|
+
['npm:@angular/core', 'angular'],
|
|
42
|
+
['npm:tailwindcss', 'tailwind'],
|
|
43
|
+
['npm:jest', 'jest'],
|
|
44
|
+
['npm:playwright', 'playwright'],
|
|
45
|
+
['npm:@playwright/test', 'playwright'],
|
|
46
|
+
['npm:wrangler', 'cloudflare-workers'],
|
|
47
|
+
['pypi:django', 'django'],
|
|
48
|
+
['rubygems:rails', 'rails'],
|
|
49
|
+
]);
|
|
50
|
+
/** The language of one touched path: the vocabulary's name for its extension, else `other`; null for a path with no extension, which shows no language. A dotfile (`.gitignore`) has none: its only dot leads the name. */
|
|
51
|
+
const languageOfPath = (path) => {
|
|
52
|
+
const base = path.replace(/\\/g, '/').split('/').pop() ?? path;
|
|
53
|
+
if (base.lastIndexOf('.') <= 0)
|
|
54
|
+
return null;
|
|
55
|
+
return LANGUAGE_OF_EXTENSION.get(base.split('.').pop()?.toLowerCase() ?? '') ?? 'other';
|
|
56
|
+
};
|
|
57
|
+
/** The strata of a file's evidence but `explore`, which the caller counts for the file and for each prompt. Files are counted, not touches: a path read ten times is one. */
|
|
58
|
+
export const strataOf = (evidence) => {
|
|
59
|
+
const counts = new Map();
|
|
60
|
+
let total = 0;
|
|
61
|
+
for (const path of new Set(evidence.paths)) {
|
|
62
|
+
const language = languageOfPath(path);
|
|
63
|
+
if (language === null)
|
|
64
|
+
continue;
|
|
65
|
+
counts.set(language, (counts.get(language) ?? 0) + 1);
|
|
66
|
+
total += 1;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
languages: [...counts].sort(([a], [b]) => a.localeCompare(b)).map(([language, files]) => ({ language, share: languageShareBucket(files, total) })),
|
|
70
|
+
repoFiles: repoFilesBucketOf(evidence.size),
|
|
71
|
+
frameworks: [...new Set(evidence.packages.flatMap((name) => FRAMEWORK_OF_PACKAGE.get(name) ?? []))].sort(),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** A stored language as it pools: a member of the pinned list, else `other`. */
|
|
75
|
+
export const projectLanguage = (stored) => (LANGUAGES.has(stored) ? stored : 'other');
|
|
76
|
+
/** A stored share bucket as it pools: itself when it is one of the four, else null (no bucket is guessed from a value this code does not know). */
|
|
77
|
+
export const projectLanguageShare = (stored) => LANGUAGE_SHARES.find((bucket) => bucket === stored) ?? null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type SessionsRuntime } from './store.js';
|
|
2
|
+
/** A commit younger than this has not had time to be reverted or dropped, so it is not read yet. */
|
|
3
|
+
export declare const SURVIVAL_DAYS = 7;
|
|
4
|
+
declare const STATES: readonly ["survived", "reverted", "unreachable", "unknown"];
|
|
5
|
+
export type SurvivalState = (typeof STATES)[number];
|
|
6
|
+
/** One line of `commit-survival.jsonl`. `sha` as captured (usually short), and a day-resolution date. */
|
|
7
|
+
export interface SurvivalObservation {
|
|
8
|
+
sha: string;
|
|
9
|
+
state: SurvivalState;
|
|
10
|
+
day: string;
|
|
11
|
+
}
|
|
12
|
+
export type SurvivalCounts = {
|
|
13
|
+
[state in SurvivalState]: number;
|
|
14
|
+
};
|
|
15
|
+
/** Runs one read-only git command in a repository and answers its stdout; rejects on any failure. */
|
|
16
|
+
export type Git = (root: string, args: readonly string[], stdin?: string) => Promise<string>;
|
|
17
|
+
export declare const survivalFile: (storeDir: string) => string;
|
|
18
|
+
/** The latest state of each sha. The file only adds to what `books` shows, so a missing or unreadable one reads as nothing observed yet. */
|
|
19
|
+
export declare const readSurvival: (storeDir: string) => Promise<Map<string, SurvivalState>>;
|
|
20
|
+
/**
|
|
21
|
+
* Observes every commit older than `SURVIVAL_DAYS` in the allowlisted folders. Never rejects, since a deposit that has already succeeded
|
|
22
|
+
* must not fail on what it adds: null when the store could not be read or written, and the next run tries again.
|
|
23
|
+
*/
|
|
24
|
+
export declare const recordSurvival: (runtime: SessionsRuntime, allow: readonly string[], now: () => number, git?: Git) => Promise<SurvivalCounts | null>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Commit survival: what became of a commit is an observation made later, not a fact of the transcript, so it never
|
|
2
|
+
// runs at derive time and never from a hook a session waits on. A manual deposit and the nightly run read the repository of each
|
|
3
|
+
// allowlisted folder and append to `commit-survival.jsonl` beside the store, so the log `verify` checks stays untouched.
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { createReadStream } from 'node:fs';
|
|
6
|
+
import { appendFile, readFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { compileAllow, slugOf } from './allow.js';
|
|
10
|
+
import { makeRepoRoot } from './repo-root.js';
|
|
11
|
+
import { loadStore, paths } from './store.js';
|
|
12
|
+
/** A commit younger than this has not had time to be reverted or dropped, so it is not read yet. */
|
|
13
|
+
export const SURVIVAL_DAYS = 7;
|
|
14
|
+
const DAY_MS = 86_400_000;
|
|
15
|
+
const GIT_TIMEOUT_MS = 120_000;
|
|
16
|
+
/** `rev-list` prints 41 bytes a commit; this holds about 6 million of them. */
|
|
17
|
+
const GIT_MAX_BUFFER = 256 * 1024 * 1024;
|
|
18
|
+
/** How far into a transcript to look for its working directory: the harness stamps every line, so a file without one by here has none. */
|
|
19
|
+
const CWD_SCAN_LINES = 200;
|
|
20
|
+
/** Tried in this order when `origin/HEAD` is not set, which `git clone` sets and `git init` plus `git remote add` does not. */
|
|
21
|
+
const DEFAULT_REFS = ['refs/remotes/origin/main', 'refs/remotes/origin/master', 'refs/heads/main', 'refs/heads/master'];
|
|
22
|
+
const STATES = ['survived', 'reverted', 'unreachable', 'unknown'];
|
|
23
|
+
export const survivalFile = (storeDir) => join(storeDir, 'commit-survival.jsonl');
|
|
24
|
+
const runGit = (root, args, stdin) => new Promise((resolve, reject) => {
|
|
25
|
+
const child = execFile('git', ['-C', root, ...args], {
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
timeout: GIT_TIMEOUT_MS,
|
|
28
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
// A partial clone would fetch a missing object from its remote, and this reads no network. `GIT_NO_LAZY_FETCH` needs about 2.45 or later, and a `protocol.allow` key loses to a `protocol.<name>.allow` key in the
|
|
31
|
+
// repository's own config. `GIT_ALLOW_PROTOCOL` is checked first on any git and beats every config key, and setting it here replaces one inherited from the caller. A `GIT_DIR` inherited from a caller's hook would read some other repository than the one named.
|
|
32
|
+
env: { ...process.env, GIT_ALLOW_PROTOCOL: 'none', GIT_NO_LAZY_FETCH: '1', GIT_OPTIONAL_LOCKS: '0', GIT_DIR: undefined, GIT_WORK_TREE: undefined },
|
|
33
|
+
}, (error, stdout) => {
|
|
34
|
+
if (error === null)
|
|
35
|
+
resolve(stdout);
|
|
36
|
+
else
|
|
37
|
+
reject(new Error(error.message));
|
|
38
|
+
});
|
|
39
|
+
// A git that exits before reading its stdin raises EPIPE here; its exit status already reports the failure.
|
|
40
|
+
child.stdin?.on('error', () => undefined);
|
|
41
|
+
child.stdin?.end(stdin ?? '');
|
|
42
|
+
});
|
|
43
|
+
const outputLines = (text) => text.split(/\r?\n/).filter((line) => line !== '');
|
|
44
|
+
const defaultRef = async (git, root) => {
|
|
45
|
+
const head = (await git(root, ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']).catch(() => '')).trim();
|
|
46
|
+
if (head !== '')
|
|
47
|
+
return head;
|
|
48
|
+
const present = new Set(outputLines(await git(root, ['for-each-ref', '--format=%(refname)', ...DEFAULT_REFS])));
|
|
49
|
+
return DEFAULT_REFS.find((ref) => present.has(ref)) ?? null;
|
|
50
|
+
};
|
|
51
|
+
const REVERT_RECORD = /^([0-9a-f]{40})\n([\s\S]*)$/;
|
|
52
|
+
const REVERTS = /This reverts commit ([0-9a-f]{40})/g;
|
|
53
|
+
/**
|
|
54
|
+
* The commits the default branch has undone, from `git revert`'s own message. `git log --topo-order` runs children before parents, so by the
|
|
55
|
+
* time a revert is reached anything that undid it has been seen, and a reverted revert undoes nothing.
|
|
56
|
+
*/
|
|
57
|
+
const revertedIn = (log) => {
|
|
58
|
+
const reverted = new Set();
|
|
59
|
+
for (const record of log.split('\0')) {
|
|
60
|
+
const found = REVERT_RECORD.exec(record.trimStart());
|
|
61
|
+
if (found === null || reverted.has(found[1] ?? ''))
|
|
62
|
+
continue;
|
|
63
|
+
for (const match of (found[2] ?? '').matchAll(REVERTS))
|
|
64
|
+
reverted.add(match[1] ?? '');
|
|
65
|
+
}
|
|
66
|
+
return reverted;
|
|
67
|
+
};
|
|
68
|
+
/** What one repository says of each sha. A sha it does not hold reads `unknown`: the session may have committed in another repository, which a path rule cannot follow. */
|
|
69
|
+
const classify = async (git, root, shas) => {
|
|
70
|
+
const states = new Map();
|
|
71
|
+
if ((await git(root, ['rev-parse', '--is-shallow-repository'])).trim() !== 'false')
|
|
72
|
+
return states;
|
|
73
|
+
const ref = await defaultRef(git, root);
|
|
74
|
+
if (ref === null)
|
|
75
|
+
return states;
|
|
76
|
+
const reachable = new Set(outputLines(await git(root, ['rev-list', ref])));
|
|
77
|
+
const reverted = revertedIn(await git(root, ['log', ref, '--topo-order', '--no-show-signature', '--grep=This reverts commit', '--format=%H%n%B%x00']));
|
|
78
|
+
const resolved = outputLines(await git(root, ['cat-file', '--batch-check=%(objectname) %(objecttype)'], `${shas.join('\n')}\n`));
|
|
79
|
+
shas.forEach((sha, index) => {
|
|
80
|
+
const [full = '', type] = (resolved[index] ?? '').split(' ');
|
|
81
|
+
if (type !== 'commit')
|
|
82
|
+
return;
|
|
83
|
+
states.set(sha, reverted.has(full) ? 'reverted' : reachable.has(full) ? 'survived' : 'unreachable');
|
|
84
|
+
});
|
|
85
|
+
return states;
|
|
86
|
+
};
|
|
87
|
+
/** The shas old enough to read, per allowlisted project. Main-line calls only: a subagent's commits are not attributed yet. */
|
|
88
|
+
const dueByProject = (store, allows, cutoff) => {
|
|
89
|
+
const projectOf = new Map(store.sessions.map((session) => [session.session, session.project]));
|
|
90
|
+
const due = new Map();
|
|
91
|
+
for (const tool of store.tools) {
|
|
92
|
+
const project = projectOf.get(tool.session);
|
|
93
|
+
if (project === undefined || !allows(project) || tool.side || tool.ts === null || tool.ts >= cutoff)
|
|
94
|
+
continue;
|
|
95
|
+
for (const sha of tool.commits ?? []) {
|
|
96
|
+
if (sha === null)
|
|
97
|
+
continue;
|
|
98
|
+
const entry = due.get(project) ?? { sessions: new Set(), shas: new Set() };
|
|
99
|
+
entry.sessions.add(tool.session);
|
|
100
|
+
entry.shas.add(sha);
|
|
101
|
+
due.set(project, entry);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return due;
|
|
105
|
+
};
|
|
106
|
+
const topLevelCwd = (line) => {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(line);
|
|
109
|
+
const cwd = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'cwd') : undefined;
|
|
110
|
+
return typeof cwd === 'string' ? cwd : undefined;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/** The transcript's first `cwd` whose slug is the project. A later `cd` moves `cwd`, and a path a transcript merely names must not choose what gets read. */
|
|
117
|
+
const cwdIn = async (file, project) => {
|
|
118
|
+
const stream = createReadStream(file, { encoding: 'utf8' });
|
|
119
|
+
// An absent file errors here, not in the loop.
|
|
120
|
+
const opened = new Promise((resolve) => {
|
|
121
|
+
stream.once('error', () => {
|
|
122
|
+
resolve(false);
|
|
123
|
+
});
|
|
124
|
+
stream.once('open', () => {
|
|
125
|
+
resolve(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
if (!(await opened))
|
|
129
|
+
return undefined;
|
|
130
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
131
|
+
let read = 0;
|
|
132
|
+
try {
|
|
133
|
+
for await (const line of reader) {
|
|
134
|
+
read += 1;
|
|
135
|
+
if (read > CWD_SCAN_LINES)
|
|
136
|
+
return undefined;
|
|
137
|
+
const cwd = topLevelCwd(line);
|
|
138
|
+
if (cwd !== undefined && slugOf(cwd) === project)
|
|
139
|
+
return cwd;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
reader.close();
|
|
145
|
+
stream.destroy();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const cwdOfProject = async (runtime, project, sessions) => {
|
|
149
|
+
for (const session of sessions) {
|
|
150
|
+
for (const root of [runtime.projectsRoot, paths(runtime.storeDir).raw]) {
|
|
151
|
+
const found = await cwdIn(join(root, project, `${session}.jsonl`), project);
|
|
152
|
+
if (found !== undefined)
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
};
|
|
158
|
+
const isObservation = (value) => {
|
|
159
|
+
if (typeof value !== 'object' || value === null)
|
|
160
|
+
return false;
|
|
161
|
+
const state = Reflect.get(value, 'state');
|
|
162
|
+
return typeof Reflect.get(value, 'sha') === 'string' && STATES.some((known) => known === state);
|
|
163
|
+
};
|
|
164
|
+
/** The latest state of each sha. The file only adds to what `books` shows, so a missing or unreadable one reads as nothing observed yet. */
|
|
165
|
+
export const readSurvival = async (storeDir) => {
|
|
166
|
+
const states = new Map();
|
|
167
|
+
const text = await readFile(survivalFile(storeDir), 'utf8').catch(() => '');
|
|
168
|
+
for (const line of text.split('\n')) {
|
|
169
|
+
if (line === '')
|
|
170
|
+
continue;
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(line);
|
|
173
|
+
if (isObservation(parsed))
|
|
174
|
+
states.set(parsed.sha, parsed.state);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// A torn last line from a crashed run: the next run observes that sha again.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return states;
|
|
181
|
+
};
|
|
182
|
+
const observe = async (runtime, allowEntries, now, git) => {
|
|
183
|
+
const allow = compileAllow(allowEntries);
|
|
184
|
+
const due = dueByProject(await loadStore(runtime.storeDir), allow.allows, new Date(now() - SURVIVAL_DAYS * DAY_MS).toISOString());
|
|
185
|
+
const repoRoot = makeRepoRoot();
|
|
186
|
+
const byRoot = new Map();
|
|
187
|
+
const shas = new Set();
|
|
188
|
+
for (const [project, entry] of due) {
|
|
189
|
+
for (const sha of entry.shas)
|
|
190
|
+
shas.add(sha);
|
|
191
|
+
const cwd = await cwdOfProject(runtime, project, entry.sessions);
|
|
192
|
+
const root = cwd === undefined ? null : repoRoot(cwd);
|
|
193
|
+
if (root !== null)
|
|
194
|
+
byRoot.set(root, new Set([...(byRoot.get(root) ?? []), ...entry.shas]));
|
|
195
|
+
}
|
|
196
|
+
const fresh = new Map();
|
|
197
|
+
for (const [root, wanted] of byRoot) {
|
|
198
|
+
// Fail-open per repository: one that cannot be read leaves its shas unknown and the others are still read.
|
|
199
|
+
const states = await classify(git, root, [...wanted]).catch(() => new Map());
|
|
200
|
+
for (const [sha, state] of states)
|
|
201
|
+
fresh.set(sha, state);
|
|
202
|
+
}
|
|
203
|
+
const known = await readSurvival(runtime.storeDir);
|
|
204
|
+
const day = new Date(now()).toISOString().slice(0, 10);
|
|
205
|
+
const counts = { survived: 0, reverted: 0, unreachable: 0, unknown: 0 };
|
|
206
|
+
const appended = [];
|
|
207
|
+
for (const sha of shas) {
|
|
208
|
+
const before = known.get(sha);
|
|
209
|
+
// An observation is not undone by a later failure to look: only a state git answered replaces one.
|
|
210
|
+
const state = fresh.get(sha) ?? before ?? 'unknown';
|
|
211
|
+
counts[state] += 1;
|
|
212
|
+
if (state !== before)
|
|
213
|
+
appended.push({ sha, state, day });
|
|
214
|
+
}
|
|
215
|
+
if (appended.length > 0)
|
|
216
|
+
await appendFile(survivalFile(runtime.storeDir), appended.map((line) => `${JSON.stringify(line)}\n`).join(''), { mode: 0o600 });
|
|
217
|
+
return counts;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Observes every commit older than `SURVIVAL_DAYS` in the allowlisted folders. Never rejects, since a deposit that has already succeeded
|
|
221
|
+
* must not fail on what it adds: null when the store could not be read or written, and the next run tries again.
|
|
222
|
+
*/
|
|
223
|
+
export const recordSurvival = async (runtime, allow, now, git = runGit) => {
|
|
224
|
+
try {
|
|
225
|
+
return await observe(runtime, allow, now, git);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type SessionObservation, type Skeleton, type SkeletonLine, type ToolObservation } from './schema.js';
|
|
2
|
+
export interface InvariantResult {
|
|
3
|
+
name: string;
|
|
4
|
+
/** What was compared: lines, pairs, or tool results, as each invariant defines it. */
|
|
5
|
+
checked: number;
|
|
6
|
+
violations: number;
|
|
7
|
+
/** Checks that neither hold nor fail, each counted under the cause it was traced to (the cost-state check only). */
|
|
8
|
+
observed?: ObservedClass[];
|
|
9
|
+
}
|
|
10
|
+
export type CostClass = 'resume scope' | 'subagent transcripts' | 'harness side calls' | 'other';
|
|
11
|
+
export interface ObservedClass {
|
|
12
|
+
cls: CostClass;
|
|
13
|
+
/** True for a check that holds only once its scope is applied; false for a total above the recorded sum. */
|
|
14
|
+
holds: boolean;
|
|
15
|
+
checks: number;
|
|
16
|
+
/** The first session seen in the class. */
|
|
17
|
+
example: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare const INVARIANTS: readonly ["tool_result names an earlier tool_use", "parentUuid resolves in the file", "message timestamps never step back more than 1 s", "assistant lines carry a request id; shared ids have consecutive apiBlockIndex", "thinking tokens never exceed output tokens", "cost-state totals equal summed usage since the process start, subagents included"];
|
|
20
|
+
/** `delegated` holds the lines of the session's subagent transcripts, which its cost-state totals include. */
|
|
21
|
+
export declare const verifySkeleton: (skeleton: Skeleton, delegated?: readonly SkeletonLine[]) => InvariantResult[];
|
|
22
|
+
/** Sums each invariant over many skeletons, each main session's cost-state checked with its subagent transcripts' lines. */
|
|
23
|
+
export declare const verifyAll: (skeletons: readonly Skeleton[], delegatedFrom?: readonly Skeleton[]) => InvariantResult[];
|
|
24
|
+
export declare const formatVerify: (results: readonly InvariantResult[], sessions: number) => string;
|
|
25
|
+
export declare const CAPTURE_INVARIANTS: readonly ["every session is captured at the current capture version (none is cv-behind)", "sessions captured at the current version share one sid", "every fileHash on a session is a path target of one of its tool calls", "a path target names its base; no other target does"];
|
|
26
|
+
/**
|
|
27
|
+
* The CAPTURE 4 fields, checked over the store's capture records: cv-behind sessions (a session captured at an older readable
|
|
28
|
+
* version, not yet captured again, unless `frozen` names it: its transcript is gone from the harness and `raw/` (or, for a Cursor
|
|
29
|
+
* session, its composer is gone from Cursor), so it is counted apart, never a violation), a mixed `sid` (two salts in one store, so
|
|
30
|
+
* path joins hold only within each), `fileHashes` that no tool call explains, and `targetBase` set on, or missing from, the wrong
|
|
31
|
+
* kind of target. Skeleton invariants stay in {@link verifyAll}.
|
|
32
|
+
*/
|
|
33
|
+
export declare const verifyCaptured: (sessions: readonly SessionObservation[], tools: readonly ToolObservation[], frozen?: ReadonlySet<string>) => InvariantResult[];
|
|
34
|
+
/** `not run` when a row had nothing to check (a session behind the current capture version for rows 8 to 10, a transcript with no stamped lines for row 3, and so on). */
|
|
35
|
+
export type RowStatus = 'pass' | 'fail' | 'not run';
|
|
36
|
+
/** One session's result: the ten rows of {@link INVARIANTS} then {@link CAPTURE_INVARIANTS}, each with its status (spec 5.4 item 5). */
|
|
37
|
+
export interface SessionVerification {
|
|
38
|
+
session: string;
|
|
39
|
+
invariants: Array<InvariantResult & {
|
|
40
|
+
status: RowStatus;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A result for every session with a skeleton or a capture record: {@link verifySkeleton} with the session's subagent transcripts' lines as `delegated`,
|
|
45
|
+
* then {@link verifyCaptured} over the session's own capture records. A session with no skeleton gets rows 1 to 6 at `checked: 0` (`not run`) and its real
|
|
46
|
+
* rows 7 to 10. Row 8 (one `sid`) is read within the one session, so it cannot fail for a session with a non-null `sid`; a current-version session with a
|
|
47
|
+
* null `sid` fails it. The store-wide row stays in `verifyCaptured` over all sessions.
|
|
48
|
+
*/
|
|
49
|
+
export declare const verifySessions: (skeletons: readonly Skeleton[], sessions: readonly SessionObservation[], tools: readonly ToolObservation[], frozen?: ReadonlySet<string>) => SessionVerification[];
|
|
50
|
+
/** Per-session lines the default text prints; the JSON lists them all. */
|
|
51
|
+
export declare const SESSION_LINE_CAP = 20;
|
|
52
|
+
/** One line per session with a failing row, naming the rows, capped at {@link SESSION_LINE_CAP} with a count of the rest; empty when none fails. Ids and row names only. */
|
|
53
|
+
export declare const formatSessionResults: (results: readonly SessionVerification[]) => string;
|