@vibe-commit/capture 0.4.2 → 0.4.4
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 +26 -0
- package/dist/commands/finalize.js +102 -2
- package/dist/commands/finalize.js.map +1 -1
- package/dist/commands/status.js +41 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/copy/strings.d.ts +39 -1
- package/dist/copy/strings.js +65 -1
- package/dist/copy/strings.js.map +1 -1
- package/dist/credential.js +14 -2
- package/dist/credential.js.map +1 -1
- package/dist/git.d.ts +10 -0
- package/dist/git.js +16 -0
- package/dist/git.js.map +1 -1
- package/dist/header_safety.d.ts +41 -0
- package/dist/header_safety.js +48 -0
- package/dist/header_safety.js.map +1 -0
- package/dist/hooks/entry.d.ts +2 -0
- package/dist/hooks/entry.js +18 -3
- package/dist/hooks/entry.js.map +1 -1
- package/dist/hooks/post_commit.js +23 -4
- package/dist/hooks/post_commit.js.map +1 -1
- package/dist/inferred_squash.d.ts +67 -0
- package/dist/inferred_squash.js +151 -0
- package/dist/inferred_squash.js.map +1 -0
- package/dist/inferred_tally.d.ts +50 -0
- package/dist/inferred_tally.js +99 -0
- package/dist/inferred_tally.js.map +1 -0
- package/dist/policy.d.ts +17 -0
- package/dist/policy.js +7 -1
- package/dist/policy.js.map +1 -1
- package/dist/post.d.ts +60 -5
- package/dist/post.js +373 -50
- package/dist/post.js.map +1 -1
- package/dist/spool.d.ts +113 -0
- package/dist/spool.js +466 -1
- package/dist/spool.js.map +1 -1
- package/dist/state.d.ts +46 -0
- package/dist/state.js +114 -0
- package/dist/state.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TODOS[177]` — INFER a squash that git never reported.
|
|
3
|
+
*
|
|
4
|
+
* ## ⛔ WHY IT EXISTS
|
|
5
|
+
*
|
|
6
|
+
* `git reset --soft B && git commit -m …` collapses `B..T` into one commit `F`, and **git fires no `post-rewrite` for it**: a
|
|
7
|
+
* reset is not a rewrite as far as the hook is concerned, and the commit that follows is an ordinary `post-commit`. So the pairing
|
|
8
|
+
* `T-and-friends → F` — which the amend and the interactive rebase get from git itself — is never recorded, and `blame_commit`
|
|
9
|
+
* on any of the folded commits calls them untouched (measured by Gate C probe 3, in-session squash agent).
|
|
10
|
+
*
|
|
11
|
+
* ## ⛔ AN INFERENCE IS NOT A FACT, AND THE WIRE SAYS SO
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is reported by git. The pairs go out on their own header (`x-rewrites-inferred`, with the patch-id as the stored
|
|
14
|
+
* evidence) and NEVER on `x-rewrites`, which the server stores as exact. See `IngestDelta.inferredRewrites`.
|
|
15
|
+
*
|
|
16
|
+
* ## THE DETECTOR — all five, or nothing
|
|
17
|
+
*
|
|
18
|
+
* For the commit `F` just made on a BRANCH (not a detached HEAD), an inferred squash is emitted iff ALL of:
|
|
19
|
+
*
|
|
20
|
+
* 1. the branch reflog entry immediately before F's own `commit: …` entry is `reset: moving to <ref>`, whose result is `B` and
|
|
21
|
+
* whose predecessor tip was `T`;
|
|
22
|
+
* 2. `B` is a STRICT ancestor of `T` (`merge-base --is-ancestor`, `B != T`);
|
|
23
|
+
* 3. `parent(F) == B`, and `F` has exactly ONE parent;
|
|
24
|
+
* 4. `tree(F) == tree(T)`;
|
|
25
|
+
* 5. the patch-ids agree: the diff `B → T` and the diff of `F` hash to the same `git patch-id --stable` (redundant with 4 — equal
|
|
26
|
+
* trees on an equal base are the same diff — and kept because it is the EVIDENCE the header stores).
|
|
27
|
+
*
|
|
28
|
+
* The pairs are `(each c in rev-list B..T) → F`, each carrying that patch-id.
|
|
29
|
+
*
|
|
30
|
+
* Everything else emits NOTHING, and each is a different reason with the same silence: the trees differ (reset, edit, commit);
|
|
31
|
+
* `B` is not an ancestor; the reflog is missing, expired, or too short; two resets stand in between; `F` is a merge or an amend or
|
|
32
|
+
* an initial commit (`commit (merge)`, `commit (amend)`, `commit (initial)` are not `commit:`); HEAD is detached; the diff is
|
|
33
|
+
* empty (a squash whose net effect is nothing has no patch-id to store).
|
|
34
|
+
*
|
|
35
|
+
* ## DECISIONS (said here so a reader does not have to guess)
|
|
36
|
+
*
|
|
37
|
+
* - **A ONE-commit "squash" counts.** `reset --soft HEAD~1 && git commit -m "new message"` with the SAME tree is a reword done by
|
|
38
|
+
* reset: `B..T` is `{T}`, `F` has the same tree and parent. The pair `T → F` is as true as any other, and it is exactly what a
|
|
39
|
+
* `git commit --amend` would have reported. The same reset WITH an edit changes the tree and emits nothing (condition 4).
|
|
40
|
+
* - **At most `MAX_INFERRED_COMMITS` (64) commits per squash, and a larger fold emits NOTHING** (server contract: the reader counts
|
|
41
|
+
* anything past 64 per request as dropped, and a partial edge set is worse than none). It is TALLIED (`inferred_tally.ts`), not
|
|
42
|
+
* truncated: the squash was detected, the contract could not carry it, and `status` says so.
|
|
43
|
+
* - **Only 40-hex repositories.** The wire contract is `ancestor40:successor40:patchid40` and the server refuses 64-hex, so a sha256
|
|
44
|
+
* repository emits nothing — also TALLIED, but only once the five conditions hold (a sha256 repo's ordinary commits are not noise).
|
|
45
|
+
* - **Needs a reflog.** `core.logAllRefUpdates=false` and bare repositories keep none, so nothing is inferred there.
|
|
46
|
+
*
|
|
47
|
+
* ## ⛔ THIS RUNS INSIDE THE USER'S `git commit`
|
|
48
|
+
*
|
|
49
|
+
* Never throws; no network; writes nothing itself. +1 spawn on every ordinary commit (the reflog read) and a handful more ONLY when
|
|
50
|
+
* the reflog says a reset came first. `post_commit.ts` arms no spawn budget (see its header), so each probe takes its own ceiling.
|
|
51
|
+
*/
|
|
52
|
+
import { gitPatchId, gitProbe, gitRangePatchId } from "./git.js";
|
|
53
|
+
/** Most folded commits one squash may name. More than this and the squash emits NOTHING (and is tallied): the server caps a request at 64. */
|
|
54
|
+
export const MAX_INFERRED_COMMITS = 64;
|
|
55
|
+
/** Object names as git prints them: 40-hex (sha1) or 64-hex (sha256). Only 40-hex may be SENT. */
|
|
56
|
+
const OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
57
|
+
const SHA1 = /^[0-9a-f]{40}$/;
|
|
58
|
+
const NONE = { pairs: [], skipped: null };
|
|
59
|
+
/**
|
|
60
|
+
* The newest `n` entries of `refs/heads/<branch>`'s reflog, newest first, or null if git cannot answer. A line that is not
|
|
61
|
+
* `<40-hex> TAB <subject>` makes the whole answer null: half a reflog is how a wrong predecessor gets picked.
|
|
62
|
+
*/
|
|
63
|
+
function branchReflog(dir, branch, n) {
|
|
64
|
+
// ⛔ `--no-show-signature` (VG pre-review, 2026-10-08): with the user's `log.showSignature=true` and an
|
|
65
|
+
// SSH-signed commit, `git log` prepends a signature-verification banner to the formatted line, which
|
|
66
|
+
// breaks the `%H\t%gs` parse — measured as the fold reading 0 pairs, not an error. The flag is unconditional:
|
|
67
|
+
// this output is parsed by byte position, never shown to anyone, so there is nothing for the banner to serve.
|
|
68
|
+
const out = gitProbe(dir, ["log", "--no-show-signature", "-g", "--no-abbrev", "--format=%H%x09%gs", "-n", String(n), `refs/heads/${branch}`, "--"]);
|
|
69
|
+
if (out === null)
|
|
70
|
+
return null;
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const line of out.split("\n")) {
|
|
73
|
+
if (line === "")
|
|
74
|
+
continue;
|
|
75
|
+
const tab = line.indexOf("\t");
|
|
76
|
+
const sha = tab < 0 ? "" : line.slice(0, tab);
|
|
77
|
+
if (!OID.test(sha))
|
|
78
|
+
return null;
|
|
79
|
+
entries.push({ sha, subject: line.slice(tab + 1) });
|
|
80
|
+
}
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The inferred pairs for the commit `head` just made — or, for a squash that WAS detected but cannot be sent, why not (so the caller can
|
|
85
|
+
* tally it). `head.branch` is null on a detached HEAD, which is nothing to infer. Never throws.
|
|
86
|
+
*/
|
|
87
|
+
export function detectSquash(dir, head) {
|
|
88
|
+
try {
|
|
89
|
+
return infer(dir, head);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return NONE;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function infer(dir, head) {
|
|
96
|
+
const F = head.sha;
|
|
97
|
+
if (head.branch === null || !OID.test(F))
|
|
98
|
+
return NONE;
|
|
99
|
+
// (1) the reflog: F's own plain `commit:` entry, directly preceded by a reset, itself preceded by the tip it moved away from.
|
|
100
|
+
const log = branchReflog(dir, head.branch, 3);
|
|
101
|
+
if (log === null || log.length < 3)
|
|
102
|
+
return NONE;
|
|
103
|
+
const [commitEntry, resetEntry, before] = log;
|
|
104
|
+
// `commit: ` exactly. `commit (amend): `, `commit (merge): `, `commit (initial): ` and `commit (cherry-pick): ` are other events.
|
|
105
|
+
if (commitEntry.sha !== F || !commitEntry.subject.startsWith("commit: "))
|
|
106
|
+
return NONE;
|
|
107
|
+
if (!/^reset: moving to \S/.test(resetEntry.subject))
|
|
108
|
+
return NONE;
|
|
109
|
+
const B = resetEntry.sha;
|
|
110
|
+
const T = before.sha;
|
|
111
|
+
// (2) B is a STRICT ancestor of T. `merge-base --is-ancestor` exits 0 for yes: gitProbe answers "" for yes and null for no/unanswerable.
|
|
112
|
+
if (B === T)
|
|
113
|
+
return NONE;
|
|
114
|
+
if (gitProbe(dir, ["merge-base", "--is-ancestor", B, T]) === null)
|
|
115
|
+
return NONE;
|
|
116
|
+
// (3) parent(F) == B, exactly one parent. `rev-list --parents -n 1 F` prints `F P1 [P2 …]`.
|
|
117
|
+
const parents = gitProbe(dir, ["rev-list", "--parents", "-n", "1", F]);
|
|
118
|
+
if (parents === null)
|
|
119
|
+
return NONE;
|
|
120
|
+
const fields = parents.trim().split(/\s+/);
|
|
121
|
+
if (fields.length !== 2 || fields[0] !== F || fields[1] !== B)
|
|
122
|
+
return NONE;
|
|
123
|
+
// (4) tree(F) == tree(T).
|
|
124
|
+
const trees = gitProbe(dir, ["rev-parse", `${F}^{tree}`, `${T}^{tree}`]);
|
|
125
|
+
if (trees === null)
|
|
126
|
+
return NONE;
|
|
127
|
+
const [treeF, treeT] = trees.trim().split("\n");
|
|
128
|
+
if (!OID.test(treeF ?? "") || treeF !== treeT)
|
|
129
|
+
return NONE;
|
|
130
|
+
// (5) the patch-ids, the evidence we store. A missing one (an empty diff, a git that would not answer) is "no evidence": nothing.
|
|
131
|
+
const patchB = gitRangePatchId(dir, B, T);
|
|
132
|
+
const patchF = gitPatchId(dir, F);
|
|
133
|
+
if (patchB === null || patchF === null || patchB !== patchF || !OID.test(patchB))
|
|
134
|
+
return NONE;
|
|
135
|
+
// The squash is DETECTED. From here on, "nothing" is a tallied skip, not a non-event.
|
|
136
|
+
// One more than the cap, so a fold of exactly 64 is sendable and 65 is known to be over.
|
|
137
|
+
const listed = gitProbe(dir, ["rev-list", `--max-count=${MAX_INFERRED_COMMITS + 1}`, `${B}..${T}`]);
|
|
138
|
+
if (listed === null)
|
|
139
|
+
return NONE;
|
|
140
|
+
const folded = listed.split("\n").filter((l) => l !== "");
|
|
141
|
+
if (folded.length === 0 || !folded.every((c) => OID.test(c) && c !== F))
|
|
142
|
+
return NONE;
|
|
143
|
+
// sha256 repository (any object name 64-hex): the server refuses 64-hex, so nothing is sent.
|
|
144
|
+
if (![F, B, patchB, ...folded].every((x) => SHA1.test(x)))
|
|
145
|
+
return { pairs: [], skipped: "sha256" };
|
|
146
|
+
// A fold of more than 64: the server counts the excess as dropped and a partial edge set is worse than none, so none.
|
|
147
|
+
if (folded.length > MAX_INFERRED_COMMITS)
|
|
148
|
+
return { pairs: [], skipped: "oversize" };
|
|
149
|
+
return { pairs: folded.map((ancestor) => ({ ancestor, successor: F, patchId: patchB })), skipped: null };
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=inferred_squash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inferred_squash.js","sourceRoot":"","sources":["../src/inferred_squash.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TODOS[177]` — the counts of squash pairs that were DETECTED but never ended up recorded by the server, so the
|
|
3
|
+
* silence is not a non-event.
|
|
4
|
+
*
|
|
5
|
+
* `oversize`/`sha256` are a contract limit of the server's `x-rewrites-inferred` reader, caught before the squash is
|
|
6
|
+
* ever spooled: a fold of more than 64 commits (the server counts anything past 64 per request as dropped, and a
|
|
7
|
+
* partial edge set is worse than none), and a sha256 repository (the server refuses 64-hex shas). Either way the
|
|
8
|
+
* client emits NOTHING for that squash and counts it here instead — one per squash, since neither ever had pairs on
|
|
9
|
+
* disk to count.
|
|
10
|
+
*
|
|
11
|
+
* ⛔⛔ `set_aside`/`pruned` (VL, 2026-10-08, VG's silent-loss finding over a real `finalize` short-ack run) exist
|
|
12
|
+
* because set-aside and the 7-day prune both end by deleting or quietly carrying forward the group line a reader
|
|
13
|
+
* would otherwise count from the live spool file. Without a durable record here, a group that was set aside and
|
|
14
|
+
* then `finalize` deleted the (now fully-resolved) spool for — or one that aged out under the prune — left no
|
|
15
|
+
* trace anywhere: MEASURED, `status` read 0 before the event and 0 after, as if 70 real pairs had never existed.
|
|
16
|
+
* These two count PAIRS, recorded at the moment of the event, before any deletion can happen — which is what
|
|
17
|
+
* makes deleting the spool afterward fine.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ THIS TALLY IS THE ONE SOURCE OF TRUTH FOR `set_aside` (VG's follow-up finding, item (b)): an earlier version
|
|
20
|
+
* of this fix also added a LIVE gauge (`inferredSetAsideForRepo` in `spool.ts`, reading the still-on-disk file
|
|
21
|
+
* directly), which double-counted the SAME pairs once a group aged into the prune — `status` announced 80 pairs
|
|
22
|
+
* for one 40-pair group, 40 from each source. That gauge is gone; `inferredUnacknowledgedForRepo` ("still
|
|
23
|
+
* waiting on an ack," a genuinely different fact) is the only live gauge left in this area.
|
|
24
|
+
*
|
|
25
|
+
* Counts only — never a sha, a path or a branch. One small file per repo beside the session files, named so no
|
|
26
|
+
* scanner matches it (`pruned-pending.tally`'s rule); a tally older than 7 days restarts instead of accumulating.
|
|
27
|
+
* Never throws: this can run inside the user's `git commit`.
|
|
28
|
+
*/
|
|
29
|
+
export type SkippedReason = "oversize" | "sha256" | "set_aside" | "pruned";
|
|
30
|
+
export interface SkippedTally {
|
|
31
|
+
readonly oversize: number;
|
|
32
|
+
readonly sha256: number;
|
|
33
|
+
readonly set_aside: number;
|
|
34
|
+
readonly pruned: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Count `count` (default 1) pairs/squashes skipped for one reason. Never throws.
|
|
38
|
+
*
|
|
39
|
+
* ⛔⛔ `set_aside` and `pruned` (VL, 2026-10-08, VG's silent-loss finding) are PAIR counts — unlike `oversize`/
|
|
40
|
+
* `sha256`, which count SQUASHES (an oversize/sha256 squash is never spooled, so there is nothing to count by
|
|
41
|
+
* pair). Both record the ONLY durable trace of pairs that a live gauge (`inferredUnacknowledgedForRepo`) stops
|
|
42
|
+
* counting the moment the file backing it is deleted or pruned. Called BEFORE that deletion — `set_aside` from
|
|
43
|
+
* `post.ts` at the moment it calls `setAsideInferredGroup`, `pruned` from the prune sweep at the moment it
|
|
44
|
+
* discards a group with NEITHER mark (never a delivered or already-tallied-set-aside one, or this would
|
|
45
|
+
* double-count — `spool.ts`'s `settleInferred`) — so "deleting the spool is fine" (VL) because this tally is
|
|
46
|
+
* what makes it fine.
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordInferredSkipped(home: string, repoKey: string, reason: SkippedReason, nowMs?: number, count?: number): void;
|
|
49
|
+
/** The counts to announce, or null when nothing was skipped in the last 7 days. */
|
|
50
|
+
export declare function readInferredSkipped(home: string, repoKey: string, nowMs: number): SkippedTally | null;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TODOS[177]` — the counts of squash pairs that were DETECTED but never ended up recorded by the server, so the
|
|
3
|
+
* silence is not a non-event.
|
|
4
|
+
*
|
|
5
|
+
* `oversize`/`sha256` are a contract limit of the server's `x-rewrites-inferred` reader, caught before the squash is
|
|
6
|
+
* ever spooled: a fold of more than 64 commits (the server counts anything past 64 per request as dropped, and a
|
|
7
|
+
* partial edge set is worse than none), and a sha256 repository (the server refuses 64-hex shas). Either way the
|
|
8
|
+
* client emits NOTHING for that squash and counts it here instead — one per squash, since neither ever had pairs on
|
|
9
|
+
* disk to count.
|
|
10
|
+
*
|
|
11
|
+
* ⛔⛔ `set_aside`/`pruned` (VL, 2026-10-08, VG's silent-loss finding over a real `finalize` short-ack run) exist
|
|
12
|
+
* because set-aside and the 7-day prune both end by deleting or quietly carrying forward the group line a reader
|
|
13
|
+
* would otherwise count from the live spool file. Without a durable record here, a group that was set aside and
|
|
14
|
+
* then `finalize` deleted the (now fully-resolved) spool for — or one that aged out under the prune — left no
|
|
15
|
+
* trace anywhere: MEASURED, `status` read 0 before the event and 0 after, as if 70 real pairs had never existed.
|
|
16
|
+
* These two count PAIRS, recorded at the moment of the event, before any deletion can happen — which is what
|
|
17
|
+
* makes deleting the spool afterward fine.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ THIS TALLY IS THE ONE SOURCE OF TRUTH FOR `set_aside` (VG's follow-up finding, item (b)): an earlier version
|
|
20
|
+
* of this fix also added a LIVE gauge (`inferredSetAsideForRepo` in `spool.ts`, reading the still-on-disk file
|
|
21
|
+
* directly), which double-counted the SAME pairs once a group aged into the prune — `status` announced 80 pairs
|
|
22
|
+
* for one 40-pair group, 40 from each source. That gauge is gone; `inferredUnacknowledgedForRepo` ("still
|
|
23
|
+
* waiting on an ack," a genuinely different fact) is the only live gauge left in this area.
|
|
24
|
+
*
|
|
25
|
+
* Counts only — never a sha, a path or a branch. One small file per repo beside the session files, named so no
|
|
26
|
+
* scanner matches it (`pruned-pending.tally`'s rule); a tally older than 7 days restarts instead of accumulating.
|
|
27
|
+
* Never throws: this can run inside the user's `git commit`.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { repoSessionsDir } from "./paths.js";
|
|
32
|
+
const TALLY_NAME = "inferred-skipped.tally";
|
|
33
|
+
const TTL_MS = 7 * 24 * 3600_000;
|
|
34
|
+
/**
|
|
35
|
+
* ⚠ ADDITIVE: `set_aside`/`pruned` default to 0 when absent, rather than invalidating a tally written by 0.4.2 or
|
|
36
|
+
* earlier (which has `oversize`/`sha256` and no concept of the other two) — a missing field means "before this
|
|
37
|
+
* existed," never "corrupt," same instinct as `SpoolEntry.observedAt?`.
|
|
38
|
+
*/
|
|
39
|
+
function read(dir) {
|
|
40
|
+
try {
|
|
41
|
+
const t = JSON.parse(readFileSync(join(dir, TALLY_NAME), "utf8"));
|
|
42
|
+
if (!(Number.isSafeInteger(t.oversize) && Number.isSafeInteger(t.sha256) && typeof t.at === "number" && Number.isFinite(t.at)))
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
oversize: t.oversize,
|
|
46
|
+
sha256: t.sha256,
|
|
47
|
+
set_aside: Number.isSafeInteger(t.set_aside) ? t.set_aside : 0,
|
|
48
|
+
pruned: Number.isSafeInteger(t.pruned) ? t.pruned : 0,
|
|
49
|
+
at: t.at,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* absent or damaged: no tally */
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Count `count` (default 1) pairs/squashes skipped for one reason. Never throws.
|
|
59
|
+
*
|
|
60
|
+
* ⛔⛔ `set_aside` and `pruned` (VL, 2026-10-08, VG's silent-loss finding) are PAIR counts — unlike `oversize`/
|
|
61
|
+
* `sha256`, which count SQUASHES (an oversize/sha256 squash is never spooled, so there is nothing to count by
|
|
62
|
+
* pair). Both record the ONLY durable trace of pairs that a live gauge (`inferredUnacknowledgedForRepo`) stops
|
|
63
|
+
* counting the moment the file backing it is deleted or pruned. Called BEFORE that deletion — `set_aside` from
|
|
64
|
+
* `post.ts` at the moment it calls `setAsideInferredGroup`, `pruned` from the prune sweep at the moment it
|
|
65
|
+
* discards a group with NEITHER mark (never a delivered or already-tallied-set-aside one, or this would
|
|
66
|
+
* double-count — `spool.ts`'s `settleInferred`) — so "deleting the spool is fine" (VL) because this tally is
|
|
67
|
+
* what makes it fine.
|
|
68
|
+
*/
|
|
69
|
+
export function recordInferredSkipped(home, repoKey, reason, nowMs = Date.now(), count = 1) {
|
|
70
|
+
try {
|
|
71
|
+
const dir = repoSessionsDir(home, repoKey);
|
|
72
|
+
if (dir === null || count <= 0)
|
|
73
|
+
return;
|
|
74
|
+
const prev = read(dir);
|
|
75
|
+
const carry = prev !== null && nowMs - prev.at <= TTL_MS ? prev : { oversize: 0, sha256: 0, set_aside: 0, pruned: 0, at: nowMs };
|
|
76
|
+
const next = {
|
|
77
|
+
oversize: carry.oversize + (reason === "oversize" ? count : 0),
|
|
78
|
+
sha256: carry.sha256 + (reason === "sha256" ? count : 0),
|
|
79
|
+
set_aside: carry.set_aside + (reason === "set_aside" ? count : 0),
|
|
80
|
+
pruned: carry.pruned + (reason === "pruned" ? count : 0),
|
|
81
|
+
at: nowMs,
|
|
82
|
+
};
|
|
83
|
+
writeFileSync(join(dir, TALLY_NAME), `${JSON.stringify(next)}\n`, { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* the event already happened; failing to count it must not throw */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The counts to announce, or null when nothing was skipped in the last 7 days. */
|
|
90
|
+
export function readInferredSkipped(home, repoKey, nowMs) {
|
|
91
|
+
const dir = repoSessionsDir(home, repoKey);
|
|
92
|
+
if (dir === null)
|
|
93
|
+
return null;
|
|
94
|
+
const t = read(dir);
|
|
95
|
+
if (t === null || nowMs - t.at > TTL_MS || (t.oversize === 0 && t.sha256 === 0 && t.set_aside === 0 && t.pruned === 0))
|
|
96
|
+
return null;
|
|
97
|
+
return { oversize: t.oversize, sha256: t.sha256, set_aside: t.set_aside, pruned: t.pruned };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=inferred_tally.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inferred_tally.js","sourceRoot":"","sources":["../src/inferred_tally.ts"],"names":[],"mappings":"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"}
|
package/dist/policy.d.ts
CHANGED
|
@@ -109,6 +109,23 @@ export interface FileState {
|
|
|
109
109
|
* would be a confident lie about a value we simply do not have.
|
|
110
110
|
*/
|
|
111
111
|
readonly lastSentAt: number;
|
|
112
|
+
/**
|
|
113
|
+
* ⛔⛔ `TODOS[182]` — STICKY, and that word is load-bearing. Set exactly once,
|
|
114
|
+
* on this file's first 431 (a header the server's limit refused), and never
|
|
115
|
+
* cleared for the rest of this session: every attempt after that — hook or
|
|
116
|
+
* `finalize`, this span or the next one — builds the deny-list (lean) header
|
|
117
|
+
* set from the start, never re-evaluated size or charset attempt by attempt.
|
|
118
|
+
*
|
|
119
|
+
* ⚠ **Not re-evaluated is the point.** A flag that cleared itself once a
|
|
120
|
+
* smaller delta happened to fit would alternate between full and lean
|
|
121
|
+
* requests depending on what this session's NEXT turn happens to contain,
|
|
122
|
+
* which is exactly the "sometimes it works" shape a user cannot debug.
|
|
123
|
+
* `false` in `EMPTY_FILE_STATE`, so a state file written before this field
|
|
124
|
+
* existed reads as "never gone lean" — the correct default: nothing about an
|
|
125
|
+
* old file's history recorded a 431 for `markSkipped`/`enforceCaps` to carry
|
|
126
|
+
* forward if it did not ask.
|
|
127
|
+
*/
|
|
128
|
+
readonly lean: boolean;
|
|
112
129
|
}
|
|
113
130
|
export declare const EMPTY_FILE_STATE: FileState;
|
|
114
131
|
/**
|
package/dist/policy.js
CHANGED
|
@@ -108,6 +108,7 @@ export const EMPTY_FILE_STATE = {
|
|
|
108
108
|
gapBytes: 0,
|
|
109
109
|
gapCount: 0,
|
|
110
110
|
lastSentAt: 0,
|
|
111
|
+
lean: false,
|
|
111
112
|
};
|
|
112
113
|
/**
|
|
113
114
|
* The caps.
|
|
@@ -193,6 +194,11 @@ export function markSkipped(state, from, to) {
|
|
|
193
194
|
backlog: [],
|
|
194
195
|
gapBytes: state.gapBytes + skipped,
|
|
195
196
|
gapCount: state.gapCount + (skipped > 0 ? 1 : 0),
|
|
197
|
+
// `TODOS[182]`. Carried, never cleared here: `lean` is STICKY, and a `never`
|
|
198
|
+
// disposition (including the "already lean, still 431" terminal case) says
|
|
199
|
+
// nothing about whether the NEXT span on this file should go back to
|
|
200
|
+
// building full headers.
|
|
201
|
+
lean: state.lean,
|
|
196
202
|
};
|
|
197
203
|
}
|
|
198
204
|
/**
|
|
@@ -239,6 +245,6 @@ export function enforceCaps(state, nowMs, caps) {
|
|
|
239
245
|
gapCount += 1;
|
|
240
246
|
backlog = backlog.slice(1);
|
|
241
247
|
}
|
|
242
|
-
return { sentOffset, backlog, gapBytes, gapCount, lastSentAt: state.lastSentAt };
|
|
248
|
+
return { sentOffset, backlog, gapBytes, gapCount, lastSentAt: state.lastSentAt, lean: state.lean };
|
|
243
249
|
}
|
|
244
250
|
//# sourceMappingURL=policy.js.map
|
package/dist/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAQzC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsC,IAAI,GAAG,CAAC;IACvE,gFAAgF;IAChF,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,uEAAuE;IACvE,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,2EAA2E;IAC3E,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,0DAA0D;IAC1D,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,6CAA6C;IAC7C,CAAC,GAAG,EAAE,OAAO,CAAC;CACf,CAAC,CAAC;AAEH,uDAAuD;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAiB,OAAO,CAAC;AAExD,kEAAkE;AAClE,MAAM,CAAC,MAAM,aAAa,GAAiB,OAAO,CAAC;AAEnD;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB,OAAO,CAAC;AAEnD,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,IAAI,CAAC;IAC/C,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,kBAAkB,CAAC;IAC7D,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,QAAQ,CAAC,OAAoB;IAC3C,OAAO,OAAO,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACzF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAiB;IAC3C,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,SAAS,CAAC;IAC5B,CAAC;AACH,CAAC;
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
package/dist/post.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { type SelectedAgentId } from "./agents/types.js";
|
|
21
21
|
import type { IngestCredential } from "./credential.js";
|
|
22
22
|
import { type Disposition } from "./policy.js";
|
|
23
|
-
import { type Observed } from "./spool.js";
|
|
23
|
+
import { type InferredGroupId, type Observed } from "./spool.js";
|
|
24
24
|
/** Production data plane. Overridable for tests and for a self-hosted server. */
|
|
25
25
|
export declare const DEFAULT_INGEST_URL = "https://api.vibecommit.ai/ingest/v1/session";
|
|
26
26
|
/**
|
|
@@ -122,6 +122,15 @@ export interface IngestDelta {
|
|
|
122
122
|
* be wrong about.
|
|
123
123
|
*/
|
|
124
124
|
readonly rewrites?: readonly string[];
|
|
125
|
+
/**
|
|
126
|
+
* INFERRED squash pairs — `ancestor:successor:patchid`, already in wire spelling (`capInferred`). `TODOS[177]`.
|
|
127
|
+
*
|
|
128
|
+
* ⛔ **THEIR OWN HEADER, `x-rewrites-inferred`, AND NEVER `x-rewrites`.** `x-rewrites` is what GIT REPORTED (`post-rewrite`) and the
|
|
129
|
+
* server stores it as exact; a pair this client INFERRED from a reflog and a patch-id is a different claim and must be stored as
|
|
130
|
+
* one. Folding the two into one header would launder an inference into a fact, which is the one error this wire cannot take back
|
|
131
|
+
* (`match_kind` is in the primary key and the history is append-only).
|
|
132
|
+
*/
|
|
133
|
+
readonly inferredRewrites?: readonly string[];
|
|
125
134
|
}
|
|
126
135
|
/**
|
|
127
136
|
* Build the request headers.
|
|
@@ -140,6 +149,19 @@ export interface IngestDelta {
|
|
|
140
149
|
* at all is not. So the wire carries the irreplaceable half.
|
|
141
150
|
*/
|
|
142
151
|
export declare function buildIngestHeaders(credential: IngestCredential, delta: IngestDelta): Record<string, string>;
|
|
152
|
+
/**
|
|
153
|
+
* ⛔⛔ `TODOS[182]` — THE FIVE, AND EXACTLY THE FIVE. `lean` is defined by
|
|
154
|
+
* SUBTRACTION from this set, never by an allow-list: an earlier draft of this
|
|
155
|
+
* fix hand-picked which headers to KEEP and silently dropped `content-type` /
|
|
156
|
+
* `content-encoding` (the server could no longer parse the zstd body) and
|
|
157
|
+
* `x-agent` (a lean FIRST delta would pin `unknown` forever under STICKY
|
|
158
|
+
* `lean` — `UNKNOWN_AGENT_ID`'s own docblock above). A deny-list fails SAFE:
|
|
159
|
+
* anything not named here defaults to "stays", and the only headers worth
|
|
160
|
+
* dropping are the ones whose SIZE is unbounded by a count cap — a branch name
|
|
161
|
+
* and the four list headers. `leanHeaders` is tested by SET EQUALITY against
|
|
162
|
+
* `buildIngestHeaders`'s full output, never by eyeballing an allow-list.
|
|
163
|
+
*/
|
|
164
|
+
export declare const LEAN_OMIT_HEADERS: ReadonlySet<string>;
|
|
143
165
|
/**
|
|
144
166
|
* The wire's error code for org-approval-pending — `CR-128`, D81.
|
|
145
167
|
*
|
|
@@ -232,6 +254,15 @@ export type SendOutcome = {
|
|
|
232
254
|
* verb sends, so `null` after asking for finality means "not done", never "done".
|
|
233
255
|
*/
|
|
234
256
|
readonly finalAck: "sealed" | "already" | "refused" | null;
|
|
257
|
+
/**
|
|
258
|
+
* ⭐ `X-Rewrites-Inferred-Stored` (VG pre-review, 2026-10-08) — how many of the pairs we SENT on
|
|
259
|
+
* `x-rewrites-inferred` the server actually recorded. `null` when the header is absent (an older
|
|
260
|
+
* server, including the study servers before this deploys) or malformed: "not determinable" is not
|
|
261
|
+
* "zero". A group is only marked delivered when this equals the exact count we sent — anything less,
|
|
262
|
+
* including `null`, keeps every group for the next hook. `ON CONFLICT DO NOTHING` on the server makes
|
|
263
|
+
* a resend idempotent, so keeping on doubt costs a duplicate request, never a duplicate row.
|
|
264
|
+
*/
|
|
265
|
+
readonly inferredStored: number | null;
|
|
235
266
|
}
|
|
236
267
|
/** No response at all — DNS, refused, TLS, timeout. Not an HTTP status. */
|
|
237
268
|
| {
|
|
@@ -289,6 +320,19 @@ export type Delivery =
|
|
|
289
320
|
* behaves exactly as before — which is what the hook path does.
|
|
290
321
|
*/
|
|
291
322
|
readonly detail: ErrorDetail | null;
|
|
323
|
+
/**
|
|
324
|
+
* `TODOS[177]` follow-up (VL, 2026-10-08) — the server's `x-rewrites-inferred-stored` ack, pass-through from
|
|
325
|
+
* `SendOutcome` (so `null` whenever the response didn't carry it, including a request that sent no inferred
|
|
326
|
+
* pairs at all — the caller already knows whether it asked). `finalize`'s drain loop reads this to decide
|
|
327
|
+
* whether to keep looping: an ABSENT ack (old server) means stop immediately, a SHORT one means move on.
|
|
328
|
+
*/
|
|
329
|
+
readonly inferredStored: number | null;
|
|
330
|
+
/**
|
|
331
|
+
* `TODOS[177]` follow-up (VL, 2026-10-08, VG's silent-loss finding) — how many pairs THIS call just set aside
|
|
332
|
+
* (0 when it didn't). `finalize`'s drain loop sums this across the main delivery and every drain request to
|
|
333
|
+
* print ONE stderr line for the whole run, rather than one per request.
|
|
334
|
+
*/
|
|
335
|
+
readonly inferredSetAsidePairs: number;
|
|
292
336
|
};
|
|
293
337
|
export interface DeliveryContext {
|
|
294
338
|
readonly home: string;
|
|
@@ -344,6 +388,14 @@ export interface DeliveryContext {
|
|
|
344
388
|
readonly commits?: Observed;
|
|
345
389
|
/** Rewrite pairs, wire-spelled. Main delivery only, for the same reason. */
|
|
346
390
|
readonly rewrites?: readonly string[];
|
|
391
|
+
/** `TODOS[177]` — the inferred pairs this delivery carries (`capInferred`), wire-spelled. */
|
|
392
|
+
readonly inferredRewrites?: readonly string[];
|
|
393
|
+
/**
|
|
394
|
+
* The IDENTITIES of the groups behind `inferredRewrites` (`capInferred`'s `groups`) — what `dropInferredGroups` marks
|
|
395
|
+
* delivered, and ONLY when the server's `x-rewrites-inferred-stored` ack equals `inferredRewrites.length` exactly.
|
|
396
|
+
* `undefined` here means the same as an empty `inferredRewrites`: nothing to drop.
|
|
397
|
+
*/
|
|
398
|
+
readonly inferredGroups?: readonly InferredGroupId[];
|
|
347
399
|
/**
|
|
348
400
|
* ⭐ `TODOS[146]` — assert the session is over (`X-Session-Final: 1`) and send EVEN WHEN THE
|
|
349
401
|
* OFFSET IS AT EOF, as an empty delta: the common one-shot case is a session whose bytes all
|
|
@@ -361,10 +413,13 @@ export interface DeliveryContext {
|
|
|
361
413
|
/**
|
|
362
414
|
* Send the undelivered span of one transcript file and apply the failure policy.
|
|
363
415
|
*
|
|
364
|
-
* **
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
416
|
+
* **At most one NETWORK attempt per hook, still — exactly D56 §D8's rule.** A
|
|
417
|
+
* `finalize` lean retry (below) is the one exception D56 §D8 itself does not
|
|
418
|
+
* cover: it governs HOOKS, and `finalize` is a synchronous, user-invoked verb
|
|
419
|
+
* that already makes 1 + up to `FINALIZE_DRAIN_MAX_ITERATIONS` requests by
|
|
420
|
+
* design (VL's explicit ruling, `TODOS[182]`). On the hook path, `later` still
|
|
421
|
+
* does not retry here; it leaves the offset where it is so the hook that fires
|
|
422
|
+
* on the NEXT turn picks the same bytes up.
|
|
368
423
|
*
|
|
369
424
|
* Never throws: the caller is a hook, and an exception on this path is the
|
|
370
425
|
* contract breaking. Every failure resolves to a `Delivery`.
|