@webpieces/rules-config 0.4.523 → 0.4.524
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.524",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/branch-archiver.d.ts
CHANGED
|
@@ -48,6 +48,14 @@ export declare class BranchArchiver {
|
|
|
48
48
|
* would silently destroy the older archive, which is the exact failure the archive exists to prevent.
|
|
49
49
|
* Returns ok=false (with git's stderr) rather than throwing — an archive that cannot be written must
|
|
50
50
|
* turn into "then do not delete either", a decision the CALLER makes.
|
|
51
|
+
*
|
|
52
|
+
* IDEMPOTENT for the same tip: if the base name is already taken by a tag pointing at the SAME sha,
|
|
53
|
+
* that tag is returned as-is rather than minting a `-2` beside it. Re-archiving an identical tip is
|
|
54
|
+
* not a collision — it is the same archive — and `wp-land-pr` produces exactly that case, tagging
|
|
55
|
+
* the branch in its own recap and then handing the worktree reap to a child that (correctly,
|
|
56
|
+
* because it trusts nothing the parent told it) archives again a second later. Without this the
|
|
57
|
+
* archive namespace fills with `…` / `…-2` pairs at one sha, which reads like two distinct
|
|
58
|
+
* snapshots and is one more ref to explain.
|
|
51
59
|
*/
|
|
52
60
|
archive(repoRoot: string, branch: string, when?: Date): ArchiveResult;
|
|
53
61
|
/** The literal command a human runs to bring an archived branch back, byte-identical to what was deleted. */
|
|
@@ -55,6 +63,7 @@ export declare class BranchArchiver {
|
|
|
55
63
|
/** Every archive tag currently in the repo, newest namespace segment first (plain lexical on the date). */
|
|
56
64
|
listArchiveTags(repoRoot: string): string[];
|
|
57
65
|
private freeTagName;
|
|
66
|
+
private tagSha;
|
|
58
67
|
protected tagExists(repoRoot: string, tag: string): boolean;
|
|
59
68
|
private dateSegment;
|
|
60
69
|
private capture;
|
package/src/branch-archiver.js
CHANGED
|
@@ -74,13 +74,25 @@ let BranchArchiver = class BranchArchiver {
|
|
|
74
74
|
* would silently destroy the older archive, which is the exact failure the archive exists to prevent.
|
|
75
75
|
* Returns ok=false (with git's stderr) rather than throwing — an archive that cannot be written must
|
|
76
76
|
* turn into "then do not delete either", a decision the CALLER makes.
|
|
77
|
+
*
|
|
78
|
+
* IDEMPOTENT for the same tip: if the base name is already taken by a tag pointing at the SAME sha,
|
|
79
|
+
* that tag is returned as-is rather than minting a `-2` beside it. Re-archiving an identical tip is
|
|
80
|
+
* not a collision — it is the same archive — and `wp-land-pr` produces exactly that case, tagging
|
|
81
|
+
* the branch in its own recap and then handing the worktree reap to a child that (correctly,
|
|
82
|
+
* because it trusts nothing the parent told it) archives again a second later. Without this the
|
|
83
|
+
* archive namespace fills with `…` / `…-2` pairs at one sha, which reads like two distinct
|
|
84
|
+
* snapshots and is one more ref to explain.
|
|
77
85
|
*/
|
|
78
86
|
archive(repoRoot, branch, when = new Date()) {
|
|
79
87
|
const resolved = this.capture(repoRoot, ['rev-parse', branch]);
|
|
80
88
|
if (!resolved.ok)
|
|
81
89
|
return new ArchiveResult('', '', false, `cannot resolve ${branch}: ${resolved.err}`);
|
|
82
90
|
const sha = resolved.out;
|
|
83
|
-
const
|
|
91
|
+
const base = this.archiveTagName(branch, when);
|
|
92
|
+
if (this.tagExists(repoRoot, base) && this.tagSha(repoRoot, base) === sha) {
|
|
93
|
+
return new ArchiveResult(base, sha, true, '');
|
|
94
|
+
}
|
|
95
|
+
const tag = this.freeTagName(repoRoot, base);
|
|
84
96
|
const tagged = this.capture(repoRoot, ['tag', tag, sha]);
|
|
85
97
|
if (!tagged.ok)
|
|
86
98
|
return new ArchiveResult('', sha, false, tagged.err);
|
|
@@ -114,6 +126,12 @@ let BranchArchiver = class BranchArchiver {
|
|
|
114
126
|
}
|
|
115
127
|
return candidate;
|
|
116
128
|
}
|
|
129
|
+
// The COMMIT an existing tag points at, '' when it cannot be resolved. `^{commit}` peels an
|
|
130
|
+
// annotated tag, so an archive written by hand with `git tag -a` compares equal to a lightweight one.
|
|
131
|
+
tagSha(repoRoot, tag) {
|
|
132
|
+
const result = this.capture(repoRoot, ['rev-parse', `refs/tags/${tag}^{commit}`]);
|
|
133
|
+
return result.ok ? result.out : '';
|
|
134
|
+
}
|
|
117
135
|
// Seam: overridden in the spec so name-collision logic is testable with no git and no repo.
|
|
118
136
|
tagExists(repoRoot, tag) {
|
|
119
137
|
return this.capture(repoRoot, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`]).ok;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-archiver.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/branch-archiver.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Archive a branch tip as a git TAG immediately before the branch is deleted.\n *\n * WHY this exists: `<feature>PreMerge<n>` snapshot BRANCHES exist for a real reason — after a squash\n * merge you sometimes need the original pre-merge history to debug — but they are branches, so they\n * accumulate forever, count toward `branch-creation-guard`'s maxLocalBranches, and can be committed\n * onto by accident. Observed in the wild: 6 parked branches against a cap of 5, which then REFUSED to\n * create the escape branch and pushed the agent into editing webpieces.config.json to get out.\n *\n * A tag is strictly better at the one job the branch was doing:\n * - vs KEEPING THE BRANCH — a tag is invisible to `git branch`, does not count toward the branch cap,\n * and cannot be accidentally committed onto.\n * - vs storing a PATCH in merge-info — a patch is lossy: it drops commit boundaries, messages,\n * authorship and parentage. A tag preserves the exact objects.\n * - vs relying on the REFLOG — the reflog expires (90 days by default) and is local to the one clone\n * that did the work. A tag survives `gc` and can be pushed if durability beyond one machine is wanted.\n *\n * Cost: one ref, zero new objects — the commits are already in the object store.\n *\n * Round trip (verified by hand, and asserted in branch-archiver.spec.ts against a real repo):\n * git tag archive/2026-07-30/dean/foo dean/foo\n * git branch -D dean/foo\n * git checkout -b dean/foo archive/2026-07-30/dean/foo # exact objects restored\n */\n\n// Retention policy for a branch whose PR has landed (or which wp-cleanup proved dead).\n// DELETE — delete outright, recoverable only via the reflog (the pre-0.4.492 behaviour).\n// ARCHIVE_TAG — tag the tip first, then delete. The default: same disk cost, permanently recoverable.\n// KEEP — do not delete at all (the branch keeps counting toward the branch cap).\nexport const BRANCH_RETENTION_DELETE = 'delete';\nexport const BRANCH_RETENTION_ARCHIVE_TAG = 'archive-tag';\nexport const BRANCH_RETENTION_KEEP = 'keep';\nexport const BRANCH_RETENTIONS: readonly string[] = [\n BRANCH_RETENTION_DELETE,\n BRANCH_RETENTION_ARCHIVE_TAG,\n BRANCH_RETENTION_KEEP,\n];\n\n// The one namespace every archive tag lives under, so `git tag --list 'archive/*'` is the whole\n// inventory and `git push origin 'refs/tags/archive/*'` is the whole backup.\nexport const ARCHIVE_TAG_PREFIX = 'archive';\n\n// Data-only (per CLAUDE.md, classes for data): what archiving one branch produced.\n// `tag` is '' when nothing was tagged — either the policy said not to, or git refused; `error` says which.\nexport class ArchiveResult {\n tag: string;\n sha: string;\n ok: boolean;\n error: string;\n\n constructor(tag: string, sha: string, ok: boolean, error: string) {\n this.tag = tag;\n this.sha = sha;\n this.ok = ok;\n this.error = error;\n }\n}\n\n// Result of a captured git invocation. `err` carries stderr so a refused tag can be reported verbatim.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n err: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class BranchArchiver {\n /**\n * `archive/<YYYY-MM-DD>/<branch>`. The date segment is what makes the namespace browsable\n * chronologically (`git tag --list 'archive/2026-07-*'`) and is also what keeps two archives of the\n * SAME branch name from colliding across days — the common case, since a branch name gets reused.\n */\n archiveTagName(branch: string, when: Date = new Date()): string {\n return `${ARCHIVE_TAG_PREFIX}/${this.dateSegment(when)}/${branch}`;\n }\n\n /**\n * Tag `branch`'s current tip, never overwriting an existing tag.\n *\n * Same branch archived twice on the SAME day gets `…-2`, `…-3`, … rather than `git tag -f`: force\n * would silently destroy the older archive, which is the exact failure the archive exists to prevent.\n * Returns ok=false (with git's stderr) rather than throwing — an archive that cannot be written must\n * turn into \"then do not delete either\", a decision the CALLER makes.\n */\n archive(repoRoot: string, branch: string, when: Date = new Date()): ArchiveResult {\n const resolved = this.capture(repoRoot, ['rev-parse', branch]);\n if (!resolved.ok) return new ArchiveResult('', '', false, `cannot resolve ${branch}: ${resolved.err}`);\n const sha = resolved.out;\n\n const tag = this.freeTagName(repoRoot, this.archiveTagName(branch, when));\n const tagged = this.capture(repoRoot, ['tag', tag, sha]);\n if (!tagged.ok) return new ArchiveResult('', sha, false, tagged.err);\n return new ArchiveResult(tag, sha, true, '');\n }\n\n /** The literal command a human runs to bring an archived branch back, byte-identical to what was deleted. */\n restoreCommand(branch: string, tag: string): string {\n return `git checkout -b ${branch} ${tag}`;\n }\n\n /** Every archive tag currently in the repo, newest namespace segment first (plain lexical on the date). */\n listArchiveTags(repoRoot: string): string[] {\n const result = this.capture(repoRoot, ['tag', '--list', `${ARCHIVE_TAG_PREFIX}/*`]);\n if (!result.ok || result.out === '') return [];\n return result.out\n .split('\\n')\n .map((line: string): string => line.trim())\n .filter((line: string): boolean => line !== '')\n .sort()\n .reverse();\n }\n\n // First unused name in the `<base>`, `<base>-2`, `<base>-3` … series. Bounded so a pathological repo\n // cannot spin: past the bound we hand back the last candidate and let `git tag` report the collision.\n private freeTagName(repoRoot: string, base: string): string {\n const MAX_ATTEMPTS = 50;\n let candidate = base;\n for (let attempt = 2; attempt <= MAX_ATTEMPTS; attempt += 1) {\n if (!this.tagExists(repoRoot, candidate)) return candidate;\n candidate = `${base}-${String(attempt)}`;\n }\n return candidate;\n }\n\n // Seam: overridden in the spec so name-collision logic is testable with no git and no repo.\n protected tagExists(repoRoot: string, tag: string): boolean {\n return this.capture(repoRoot, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`]).ok;\n }\n\n // YYYY-MM-DD in LOCAL time: the archive is browsed by the human who created it, on their calendar.\n private dateSegment(when: Date): string {\n const pad = (value: number): string => String(value).padStart(2, '0');\n return `${String(when.getFullYear())}-${pad(when.getMonth() + 1)}-${pad(when.getDate())}`;\n }\n\n private capture(repoRoot: string, args: string[]): CmdCapture {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n const err = typeof result.stderr === 'string' ? result.stderr.trim() : '';\n if (result.status !== 0 || typeof result.stdout !== 'string') {\n return { ok: false, out: '', err: err !== '' ? err : 'git command failed' };\n }\n return { ok: true, out: result.stdout.trim(), err };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"branch-archiver.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/branch-archiver.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Archive a branch tip as a git TAG immediately before the branch is deleted.\n *\n * WHY this exists: `<feature>PreMerge<n>` snapshot BRANCHES exist for a real reason — after a squash\n * merge you sometimes need the original pre-merge history to debug — but they are branches, so they\n * accumulate forever, count toward `branch-creation-guard`'s maxLocalBranches, and can be committed\n * onto by accident. Observed in the wild: 6 parked branches against a cap of 5, which then REFUSED to\n * create the escape branch and pushed the agent into editing webpieces.config.json to get out.\n *\n * A tag is strictly better at the one job the branch was doing:\n * - vs KEEPING THE BRANCH — a tag is invisible to `git branch`, does not count toward the branch cap,\n * and cannot be accidentally committed onto.\n * - vs storing a PATCH in merge-info — a patch is lossy: it drops commit boundaries, messages,\n * authorship and parentage. A tag preserves the exact objects.\n * - vs relying on the REFLOG — the reflog expires (90 days by default) and is local to the one clone\n * that did the work. A tag survives `gc` and can be pushed if durability beyond one machine is wanted.\n *\n * Cost: one ref, zero new objects — the commits are already in the object store.\n *\n * Round trip (verified by hand, and asserted in branch-archiver.spec.ts against a real repo):\n * git tag archive/2026-07-30/dean/foo dean/foo\n * git branch -D dean/foo\n * git checkout -b dean/foo archive/2026-07-30/dean/foo # exact objects restored\n */\n\n// Retention policy for a branch whose PR has landed (or which wp-cleanup proved dead).\n// DELETE — delete outright, recoverable only via the reflog (the pre-0.4.492 behaviour).\n// ARCHIVE_TAG — tag the tip first, then delete. The default: same disk cost, permanently recoverable.\n// KEEP — do not delete at all (the branch keeps counting toward the branch cap).\nexport const BRANCH_RETENTION_DELETE = 'delete';\nexport const BRANCH_RETENTION_ARCHIVE_TAG = 'archive-tag';\nexport const BRANCH_RETENTION_KEEP = 'keep';\nexport const BRANCH_RETENTIONS: readonly string[] = [\n BRANCH_RETENTION_DELETE,\n BRANCH_RETENTION_ARCHIVE_TAG,\n BRANCH_RETENTION_KEEP,\n];\n\n// The one namespace every archive tag lives under, so `git tag --list 'archive/*'` is the whole\n// inventory and `git push origin 'refs/tags/archive/*'` is the whole backup.\nexport const ARCHIVE_TAG_PREFIX = 'archive';\n\n// Data-only (per CLAUDE.md, classes for data): what archiving one branch produced.\n// `tag` is '' when nothing was tagged — either the policy said not to, or git refused; `error` says which.\nexport class ArchiveResult {\n tag: string;\n sha: string;\n ok: boolean;\n error: string;\n\n constructor(tag: string, sha: string, ok: boolean, error: string) {\n this.tag = tag;\n this.sha = sha;\n this.ok = ok;\n this.error = error;\n }\n}\n\n// Result of a captured git invocation. `err` carries stderr so a refused tag can be reported verbatim.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n err: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class BranchArchiver {\n /**\n * `archive/<YYYY-MM-DD>/<branch>`. The date segment is what makes the namespace browsable\n * chronologically (`git tag --list 'archive/2026-07-*'`) and is also what keeps two archives of the\n * SAME branch name from colliding across days — the common case, since a branch name gets reused.\n */\n archiveTagName(branch: string, when: Date = new Date()): string {\n return `${ARCHIVE_TAG_PREFIX}/${this.dateSegment(when)}/${branch}`;\n }\n\n /**\n * Tag `branch`'s current tip, never overwriting an existing tag.\n *\n * Same branch archived twice on the SAME day gets `…-2`, `…-3`, … rather than `git tag -f`: force\n * would silently destroy the older archive, which is the exact failure the archive exists to prevent.\n * Returns ok=false (with git's stderr) rather than throwing — an archive that cannot be written must\n * turn into \"then do not delete either\", a decision the CALLER makes.\n *\n * IDEMPOTENT for the same tip: if the base name is already taken by a tag pointing at the SAME sha,\n * that tag is returned as-is rather than minting a `-2` beside it. Re-archiving an identical tip is\n * not a collision — it is the same archive — and `wp-land-pr` produces exactly that case, tagging\n * the branch in its own recap and then handing the worktree reap to a child that (correctly,\n * because it trusts nothing the parent told it) archives again a second later. Without this the\n * archive namespace fills with `…` / `…-2` pairs at one sha, which reads like two distinct\n * snapshots and is one more ref to explain.\n */\n archive(repoRoot: string, branch: string, when: Date = new Date()): ArchiveResult {\n const resolved = this.capture(repoRoot, ['rev-parse', branch]);\n if (!resolved.ok) return new ArchiveResult('', '', false, `cannot resolve ${branch}: ${resolved.err}`);\n const sha = resolved.out;\n\n const base = this.archiveTagName(branch, when);\n if (this.tagExists(repoRoot, base) && this.tagSha(repoRoot, base) === sha) {\n return new ArchiveResult(base, sha, true, '');\n }\n\n const tag = this.freeTagName(repoRoot, base);\n const tagged = this.capture(repoRoot, ['tag', tag, sha]);\n if (!tagged.ok) return new ArchiveResult('', sha, false, tagged.err);\n return new ArchiveResult(tag, sha, true, '');\n }\n\n /** The literal command a human runs to bring an archived branch back, byte-identical to what was deleted. */\n restoreCommand(branch: string, tag: string): string {\n return `git checkout -b ${branch} ${tag}`;\n }\n\n /** Every archive tag currently in the repo, newest namespace segment first (plain lexical on the date). */\n listArchiveTags(repoRoot: string): string[] {\n const result = this.capture(repoRoot, ['tag', '--list', `${ARCHIVE_TAG_PREFIX}/*`]);\n if (!result.ok || result.out === '') return [];\n return result.out\n .split('\\n')\n .map((line: string): string => line.trim())\n .filter((line: string): boolean => line !== '')\n .sort()\n .reverse();\n }\n\n // First unused name in the `<base>`, `<base>-2`, `<base>-3` … series. Bounded so a pathological repo\n // cannot spin: past the bound we hand back the last candidate and let `git tag` report the collision.\n private freeTagName(repoRoot: string, base: string): string {\n const MAX_ATTEMPTS = 50;\n let candidate = base;\n for (let attempt = 2; attempt <= MAX_ATTEMPTS; attempt += 1) {\n if (!this.tagExists(repoRoot, candidate)) return candidate;\n candidate = `${base}-${String(attempt)}`;\n }\n return candidate;\n }\n\n // The COMMIT an existing tag points at, '' when it cannot be resolved. `^{commit}` peels an\n // annotated tag, so an archive written by hand with `git tag -a` compares equal to a lightweight one.\n private tagSha(repoRoot: string, tag: string): string {\n const result = this.capture(repoRoot, ['rev-parse', `refs/tags/${tag}^{commit}`]);\n return result.ok ? result.out : '';\n }\n\n // Seam: overridden in the spec so name-collision logic is testable with no git and no repo.\n protected tagExists(repoRoot: string, tag: string): boolean {\n return this.capture(repoRoot, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`]).ok;\n }\n\n // YYYY-MM-DD in LOCAL time: the archive is browsed by the human who created it, on their calendar.\n private dateSegment(when: Date): string {\n const pad = (value: number): string => String(value).padStart(2, '0');\n return `${String(when.getFullYear())}-${pad(when.getMonth() + 1)}-${pad(when.getDate())}`;\n }\n\n private capture(repoRoot: string, args: string[]): CmdCapture {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n const err = typeof result.stderr === 'string' ? result.stderr.trim() : '';\n if (result.status !== 0 || typeof result.stdout !== 'string') {\n return { ok: false, out: '', err: err !== '' ? err : 'git command failed' };\n }\n return { ok: true, out: result.stdout.trim(), err };\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DotWebpieces } from './state-dir';
|
|
2
|
-
export type MutationVerb = 'wp-start-update' | 'wp-finish-update' | 'wp-start-upsert-pr' | 'wp-review-upsert-pr' | 'wp-finish-upsert-pr' | 'wp-cleanup' | 'auto-reap';
|
|
2
|
+
export type MutationVerb = 'wp-start-update' | 'wp-finish-update' | 'wp-start-upsert-pr' | 'wp-review-upsert-pr' | 'wp-finish-upsert-pr' | 'wp-cleanup' | 'wp-land-pr' | 'auto-reap';
|
|
3
3
|
export type MutationPhase = 'START' | 'BACKUP' | 'CHECKOUT_MAIN' | 'PULL' | 'SQUASH' | 'RENAME' | 'FINALIZE' | 'CONFLICT' | 'INTERRUPTED' | 'END' | 'REAP' | 'REAP_WORKTREE';
|
|
4
4
|
export declare class BranchMutationEvent {
|
|
5
5
|
verb: MutationVerb;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-mutation-log.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/branch-mutation-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { DotWebpieces, dotWebpieces } from './state-dir';\nimport { toError } from './to-error';\n\n// The BRANCH-MUTATION log — an audit trail for every workflow verb that RENAMES or MOVES branches.\n// Records START / each phase boundary / END-with-outcome so the next agent (or a human) can\n// reconstruct what the tooling did to the branches. Writes to `.webpieces/hooks/branch-mutations.log`.\n// Lives in rules-config (the shared dep of pr-gate) so the pr-gate scripts can call it directly.\n\nconst HOOKS_DIR = 'hooks';\nconst LOG_FILE = 'branch-mutations.log';\nconst LOG_FILE_PREV = 'branch-mutations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors the other webpieces logs)\nconst MAX_DETAIL_LEN = 400;\n\n// The workflow verb whose branch mutation is being logged (the bin the AI/human invoked).\n// `auto-reap` is the odd one out: no human invoked it — it is the detached background refresher\n// (sync-main.ts) deleting dead branches on its own. It gets a verb precisely BECAUSE it is\n// unattended: a deletion nobody watched happen is the one that most needs an audit line.\nexport type MutationVerb =\n | 'wp-start-update' | 'wp-finish-update' | 'wp-start-upsert-pr' | 'wp-review-upsert-pr'\n | 'wp-finish-upsert-pr' | 'wp-cleanup' | 'auto-reap';\n\n// A boundary within a verb's execution. START/END bracket the whole run; the middle phases mark each\n// irreversible git step so an interrupt leaves a breadcrumb at the last phase reached.\n// REAP is a whole mutation in one line (a branch delete has no phases) — see BranchReaper.\n// REAP_WORKTREE is its worktree twin: archive → `git worktree remove` → `git branch -D`, all three of\n// which succeed or fail as one act. It is a SEPARATE phase, not just a REAP with a path, so that\n// `grep REAP_WORKTREE` answers \"what directories did the tooling delete?\" — a strictly scarier\n// question than \"what refs did it delete?\", since a worktree removal takes real files with it.\nexport type MutationPhase =\n | 'START' | 'BACKUP' | 'CHECKOUT_MAIN' | 'PULL' | 'SQUASH' | 'RENAME'\n | 'FINALIZE' | 'CONFLICT' | 'INTERRUPTED' | 'END' | 'REAP' | 'REAP_WORKTREE';\n\n// Data-only record of one branch-mutation event (per CLAUDE.md: classes for data, explicit construction).\nexport class BranchMutationEvent {\n verb: MutationVerb;\n phase: MutationPhase;\n fromBranch: string = '';\n toBranch: string = '';\n oldMain: string = '';\n newMain: string = '';\n conflict: boolean = false;\n conflictFiles: string[] = [];\n outcome: string = '';\n artifacts: string[] = [];\n // The commit a DELETED branch pointed at, captured immediately before the delete. This is what\n // makes a reap auditable AND reversible: the work is already in main, and the pre-delete tip is\n // still addressable by hash (the reflog holds it ~90 days), so formatDetail renders a literal\n // `recover=git branch <name> <sha>` next to it. Empty for mutations that delete nothing.\n sha: string = '';\n // The `archive/<date>/<branch>` tag written immediately BEFORE a REAP deleted the branch. When set,\n // formatDetail renders `recover=` against the TAG instead of the sha: a tag is a permanent ref that\n // survives `gc` and reflog expiry and can be pushed, whereas a bare sha is only recoverable while\n // this clone's reflog still holds it. Empty when nothing was tagged (retention policy 'delete').\n archiveTag: string = '';\n // The directory a REAP_WORKTREE removed. When set, `recover=` becomes the WORKTREE form\n // (`git worktree add -b <branch> <path> <ref>`) rather than the bare `git branch` form: putting the\n // ref back does not put the directory back, and a recover line that restores half of what was\n // destroyed is worse than none — it reads as done. Empty for every mutation that removes no directory.\n worktreePath: string = '';\n\n constructor(verb: MutationVerb, phase: MutationPhase) {\n this.verb = verb;\n this.phase = phase;\n }\n}\n\n/** Appends branch-mutation audit lines. `@injectable(bindingScopeValues.Singleton)` so it's injectable + drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class BranchMutationLog {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n /**\n * LOCAL scope, deliberately — one log per worktree, not one per repo.\n *\n * A SHARED append-only log would genuinely corrupt. `O_APPEND` makes a write indivisible only up to\n * PIPE_BUF, which is 512 bytes on macOS, and a REAP_WORKTREE line carrying\n * `recover=git worktree add -b <branch> <absolute-path> <tag>` exceeds that — concurrent appenders\n * from seven worktrees would interleave into an unrecoverable audit trail, which is the one thing\n * this file exists not to be. Per-worktree it has exactly ONE writer and cannot tear.\n *\n * Nothing is lost by keeping it local: under the `worktrees/<name>/` layout the log lives in the\n * PRIMARY clone, so it survives `git worktree remove`, and the whole history is one glob —\n * `<primary>/.webpieces/worktrees/*/hooks/branch-mutations.log`.\n */\n branchMutationLogPath(root: string): string {\n return this.dotDir.localFile(root, HOOKS_DIR, LOG_FILE);\n }\n\n /**\n * Append one tab-separated line per branch-mutation event to\n * `.webpieces/hooks/branch-mutations.log`. Swallows all errors — logging must NEVER block or fail\n * the workflow it is observing.\n */\n logBranchMutation(root: string, event: BranchMutationEvent): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n const hooksDir = this.dotDir.localFile(root, HOOKS_DIR);\n fs.mkdirSync(hooksDir, { recursive: true });\n\n const logPath = path.join(hooksDir, LOG_FILE);\n this.rotateLogFile(logPath, path.join(hooksDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n event.verb,\n event.phase,\n this.oneLine(this.formatDetail(event)),\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // Render only the fields this event actually set, as `key=value` tokens — greppable on one line.\n private formatDetail(event: BranchMutationEvent): string {\n const parts: string[] = [];\n // A rename/move has both ends; a REAP has only the branch it destroyed. Printing `to=?` for\n // the latter reads like a lost destination rather than \"there was never one\".\n if (event.fromBranch !== '' && event.toBranch !== '') parts.push(`from=${event.fromBranch} to=${event.toBranch}`);\n else if (event.fromBranch !== '') parts.push(`branch=${event.fromBranch}`);\n else if (event.toBranch !== '') parts.push(`from=? to=${event.toBranch}`);\n if (event.oldMain !== '' || event.newMain !== '') parts.push(`oldMain=${event.oldMain || '?'} newMain=${event.newMain || '?'}`);\n if (event.conflict) parts.push('conflict=true');\n if (event.conflictFiles.length > 0) parts.push(`conflictFiles=${event.conflictFiles.length}(${event.conflictFiles.join(',')})`);\n if (event.outcome !== '') parts.push(`outcome=${event.outcome}`);\n // Emitted as one unit so the hash is never separated from the command that undoes the delete.\n // Prefer the archive TAG as the recover ref when there is one — it does not expire.\n if (event.worktreePath !== '') {\n parts.push(this.worktreeDetail(event));\n } else if (event.sha !== '' && event.archiveTag !== '') {\n parts.push(\n `sha=${event.sha} archiveTag=${event.archiveTag} ` +\n `recover=git checkout -b ${event.fromBranch || '?'} ${event.archiveTag}`,\n );\n } else if (event.sha !== '') {\n parts.push(`sha=${event.sha} recover=git branch ${event.fromBranch || '?'} ${event.sha}`);\n }\n for (const artifact of event.artifacts) parts.push(`artifact=${artifact}`);\n return parts.join(' ');\n }\n\n /**\n * The worktree flavour of the sha/recover token: path, tip, archive tag and the ONE command that\n * puts the directory AND the branch back together.\n *\n * `git worktree add -b <branch> <path> <ref>` is verified by hand and in worktree-reaper.spec.ts —\n * plain `git worktree add <path> <tag>` would restore the files at a DETACHED HEAD, silently losing\n * the branch name the reap destroyed. Falls back to the sha when nothing was archived (retention\n * 'delete'), and to a bare `git worktree add <path>` when there was no branch at all (detached).\n */\n private worktreeDetail(event: BranchMutationEvent): string {\n const ref = event.archiveTag !== '' ? event.archiveTag : event.sha;\n const tokens = [`worktree=${event.worktreePath}`];\n if (event.sha !== '') tokens.push(`sha=${event.sha}`);\n if (event.archiveTag !== '') tokens.push(`archiveTag=${event.archiveTag}`);\n if (ref === '') return tokens.join(' ');\n const recover = event.fromBranch !== ''\n ? `git worktree add -b ${event.fromBranch} ${event.worktreePath} ${ref}`\n : `git worktree add ${event.worktreePath} ${ref}`;\n tokens.push(`recover=${recover}`);\n return tokens.join(' ');\n }\n\n // Collapse newlines/tabs and cap length so one event is always exactly one log line.\n private oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_DETAIL_LEN ? flat : flat.slice(0, MAX_DETAIL_LEN) + '…';\n }\n\n private rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Temporary migration delegators to BranchMutationLog — removed once consumers inject it.\nconst branchMutationLogSvc = new BranchMutationLog();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function branchMutationLogPath(root: string): string {\n return branchMutationLogSvc.branchMutationLogPath(root);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function logBranchMutation(root: string, event: BranchMutationEvent): void {\n branchMutationLogSvc.logBranchMutation(root, event);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"branch-mutation-log.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/branch-mutation-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { DotWebpieces, dotWebpieces } from './state-dir';\nimport { toError } from './to-error';\n\n// The BRANCH-MUTATION log — an audit trail for every workflow verb that RENAMES or MOVES branches.\n// Records START / each phase boundary / END-with-outcome so the next agent (or a human) can\n// reconstruct what the tooling did to the branches. Writes to `.webpieces/hooks/branch-mutations.log`.\n// Lives in rules-config (the shared dep of pr-gate) so the pr-gate scripts can call it directly.\n\nconst HOOKS_DIR = 'hooks';\nconst LOG_FILE = 'branch-mutations.log';\nconst LOG_FILE_PREV = 'branch-mutations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors the other webpieces logs)\nconst MAX_DETAIL_LEN = 400;\n\n// The workflow verb whose branch mutation is being logged (the bin the AI/human invoked).\n// `auto-reap` is the odd one out: no human invoked it — it is the detached background refresher\n// (sync-main.ts) deleting dead branches on its own. It gets a verb precisely BECAUSE it is\n// unattended: a deletion nobody watched happen is the one that most needs an audit line.\nexport type MutationVerb =\n | 'wp-start-update' | 'wp-finish-update' | 'wp-start-upsert-pr' | 'wp-review-upsert-pr'\n | 'wp-finish-upsert-pr' | 'wp-cleanup' | 'wp-land-pr' | 'auto-reap';\n\n// A boundary within a verb's execution. START/END bracket the whole run; the middle phases mark each\n// irreversible git step so an interrupt leaves a breadcrumb at the last phase reached.\n// REAP is a whole mutation in one line (a branch delete has no phases) — see BranchReaper.\n// REAP_WORKTREE is its worktree twin: archive → `git worktree remove` → `git branch -D`, all three of\n// which succeed or fail as one act. It is a SEPARATE phase, not just a REAP with a path, so that\n// `grep REAP_WORKTREE` answers \"what directories did the tooling delete?\" — a strictly scarier\n// question than \"what refs did it delete?\", since a worktree removal takes real files with it.\nexport type MutationPhase =\n | 'START' | 'BACKUP' | 'CHECKOUT_MAIN' | 'PULL' | 'SQUASH' | 'RENAME'\n | 'FINALIZE' | 'CONFLICT' | 'INTERRUPTED' | 'END' | 'REAP' | 'REAP_WORKTREE';\n\n// Data-only record of one branch-mutation event (per CLAUDE.md: classes for data, explicit construction).\nexport class BranchMutationEvent {\n verb: MutationVerb;\n phase: MutationPhase;\n fromBranch: string = '';\n toBranch: string = '';\n oldMain: string = '';\n newMain: string = '';\n conflict: boolean = false;\n conflictFiles: string[] = [];\n outcome: string = '';\n artifacts: string[] = [];\n // The commit a DELETED branch pointed at, captured immediately before the delete. This is what\n // makes a reap auditable AND reversible: the work is already in main, and the pre-delete tip is\n // still addressable by hash (the reflog holds it ~90 days), so formatDetail renders a literal\n // `recover=git branch <name> <sha>` next to it. Empty for mutations that delete nothing.\n sha: string = '';\n // The `archive/<date>/<branch>` tag written immediately BEFORE a REAP deleted the branch. When set,\n // formatDetail renders `recover=` against the TAG instead of the sha: a tag is a permanent ref that\n // survives `gc` and reflog expiry and can be pushed, whereas a bare sha is only recoverable while\n // this clone's reflog still holds it. Empty when nothing was tagged (retention policy 'delete').\n archiveTag: string = '';\n // The directory a REAP_WORKTREE removed. When set, `recover=` becomes the WORKTREE form\n // (`git worktree add -b <branch> <path> <ref>`) rather than the bare `git branch` form: putting the\n // ref back does not put the directory back, and a recover line that restores half of what was\n // destroyed is worse than none — it reads as done. Empty for every mutation that removes no directory.\n worktreePath: string = '';\n\n constructor(verb: MutationVerb, phase: MutationPhase) {\n this.verb = verb;\n this.phase = phase;\n }\n}\n\n/** Appends branch-mutation audit lines. `@injectable(bindingScopeValues.Singleton)` so it's injectable + drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class BranchMutationLog {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n /**\n * LOCAL scope, deliberately — one log per worktree, not one per repo.\n *\n * A SHARED append-only log would genuinely corrupt. `O_APPEND` makes a write indivisible only up to\n * PIPE_BUF, which is 512 bytes on macOS, and a REAP_WORKTREE line carrying\n * `recover=git worktree add -b <branch> <absolute-path> <tag>` exceeds that — concurrent appenders\n * from seven worktrees would interleave into an unrecoverable audit trail, which is the one thing\n * this file exists not to be. Per-worktree it has exactly ONE writer and cannot tear.\n *\n * Nothing is lost by keeping it local: under the `worktrees/<name>/` layout the log lives in the\n * PRIMARY clone, so it survives `git worktree remove`, and the whole history is one glob —\n * `<primary>/.webpieces/worktrees/*/hooks/branch-mutations.log`.\n */\n branchMutationLogPath(root: string): string {\n return this.dotDir.localFile(root, HOOKS_DIR, LOG_FILE);\n }\n\n /**\n * Append one tab-separated line per branch-mutation event to\n * `.webpieces/hooks/branch-mutations.log`. Swallows all errors — logging must NEVER block or fail\n * the workflow it is observing.\n */\n logBranchMutation(root: string, event: BranchMutationEvent): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n const hooksDir = this.dotDir.localFile(root, HOOKS_DIR);\n fs.mkdirSync(hooksDir, { recursive: true });\n\n const logPath = path.join(hooksDir, LOG_FILE);\n this.rotateLogFile(logPath, path.join(hooksDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n event.verb,\n event.phase,\n this.oneLine(this.formatDetail(event)),\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // Render only the fields this event actually set, as `key=value` tokens — greppable on one line.\n private formatDetail(event: BranchMutationEvent): string {\n const parts: string[] = [];\n // A rename/move has both ends; a REAP has only the branch it destroyed. Printing `to=?` for\n // the latter reads like a lost destination rather than \"there was never one\".\n if (event.fromBranch !== '' && event.toBranch !== '') parts.push(`from=${event.fromBranch} to=${event.toBranch}`);\n else if (event.fromBranch !== '') parts.push(`branch=${event.fromBranch}`);\n else if (event.toBranch !== '') parts.push(`from=? to=${event.toBranch}`);\n if (event.oldMain !== '' || event.newMain !== '') parts.push(`oldMain=${event.oldMain || '?'} newMain=${event.newMain || '?'}`);\n if (event.conflict) parts.push('conflict=true');\n if (event.conflictFiles.length > 0) parts.push(`conflictFiles=${event.conflictFiles.length}(${event.conflictFiles.join(',')})`);\n if (event.outcome !== '') parts.push(`outcome=${event.outcome}`);\n // Emitted as one unit so the hash is never separated from the command that undoes the delete.\n // Prefer the archive TAG as the recover ref when there is one — it does not expire.\n if (event.worktreePath !== '') {\n parts.push(this.worktreeDetail(event));\n } else if (event.sha !== '' && event.archiveTag !== '') {\n parts.push(\n `sha=${event.sha} archiveTag=${event.archiveTag} ` +\n `recover=git checkout -b ${event.fromBranch || '?'} ${event.archiveTag}`,\n );\n } else if (event.sha !== '') {\n parts.push(`sha=${event.sha} recover=git branch ${event.fromBranch || '?'} ${event.sha}`);\n }\n for (const artifact of event.artifacts) parts.push(`artifact=${artifact}`);\n return parts.join(' ');\n }\n\n /**\n * The worktree flavour of the sha/recover token: path, tip, archive tag and the ONE command that\n * puts the directory AND the branch back together.\n *\n * `git worktree add -b <branch> <path> <ref>` is verified by hand and in worktree-reaper.spec.ts —\n * plain `git worktree add <path> <tag>` would restore the files at a DETACHED HEAD, silently losing\n * the branch name the reap destroyed. Falls back to the sha when nothing was archived (retention\n * 'delete'), and to a bare `git worktree add <path>` when there was no branch at all (detached).\n */\n private worktreeDetail(event: BranchMutationEvent): string {\n const ref = event.archiveTag !== '' ? event.archiveTag : event.sha;\n const tokens = [`worktree=${event.worktreePath}`];\n if (event.sha !== '') tokens.push(`sha=${event.sha}`);\n if (event.archiveTag !== '') tokens.push(`archiveTag=${event.archiveTag}`);\n if (ref === '') return tokens.join(' ');\n const recover = event.fromBranch !== ''\n ? `git worktree add -b ${event.fromBranch} ${event.worktreePath} ${ref}`\n : `git worktree add ${event.worktreePath} ${ref}`;\n tokens.push(`recover=${recover}`);\n return tokens.join(' ');\n }\n\n // Collapse newlines/tabs and cap length so one event is always exactly one log line.\n private oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_DETAIL_LEN ? flat : flat.slice(0, MAX_DETAIL_LEN) + '…';\n }\n\n private rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Temporary migration delegators to BranchMutationLog — removed once consumers inject it.\nconst branchMutationLogSvc = new BranchMutationLog();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function branchMutationLogPath(root: string): string {\n return branchMutationLogSvc.branchMutationLogPath(root);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to BranchMutationLog; removed once consumers inject it\nexport function logBranchMutation(root: string, event: BranchMutationEvent): void {\n branchMutationLogSvc.logBranchMutation(root, event);\n}\n"]}
|