@webpieces/rules-config 0.4.686 → 0.4.688
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 +1 -1
- package/src/builds-log.d.ts +193 -0
- package/src/builds-log.js +508 -0
- package/src/builds-log.js.map +1 -0
- package/src/cli-args.d.ts +5 -2
- package/src/cli-args.js +6 -3
- package/src/cli-args.js.map +1 -1
- package/src/home-config.d.ts +61 -5
- package/src/home-config.js +86 -12
- package/src/home-config.js.map +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.js +26 -6
- package/src/index.js.map +1 -1
- package/src/run-main.js +11 -1
- package/src/run-main.js.map +1 -1
- package/templates/webpieces.buildlog.md +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.688",
|
|
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",
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { DotWebpieces } from './state-dir';
|
|
2
|
+
/**
|
|
3
|
+
* `~/.webpieces/builds.log` — the MACHINE-WIDE, append-only ledger of every build this box has started.
|
|
4
|
+
*
|
|
5
|
+
* ─── WHY THIS ONE FILE LIVES OUTSIDE THE REPO ─────────────────────────────────────────────────────────
|
|
6
|
+
* `no-machine-global-state.spec.ts` records the standing rule: webpieces writes state under
|
|
7
|
+
* `{repo}/.webpieces` and nowhere else. This is the ONE carve-out, and the argument is written out in
|
|
8
|
+
* `decisions/0006-the-build-ledger-is-machine-global.md`. In short:
|
|
9
|
+
*
|
|
10
|
+
* • The FACT is machine-scoped. "How many builds are burning this box's CPU right now" is not a
|
|
11
|
+
* property of any repo; it is a property of the machine. A per-repo ledger cannot answer it — every
|
|
12
|
+
* linked worktree has its OWN `.webpieces/`, so it would be blind to the sibling worktree it is
|
|
13
|
+
* actually contending with, never mind the four other repos on the disk.
|
|
14
|
+
* • It is NOT A CACHE. The retired `PrBodyStore` that the no-machine-global rule was written for was a
|
|
15
|
+
* local copy of a fact GitHub owned, so it could be stale, missing, or on the wrong computer. There
|
|
16
|
+
* is no remote copy of this. The file IS the fact.
|
|
17
|
+
* • Its key is an ABSOLUTE LOCAL PATH, which is stable precisely because it never leaves the machine —
|
|
18
|
+
* the instability that killed `PrBodyStore`'s `<host>/<owner>/<repo>` key cannot arise here.
|
|
19
|
+
*
|
|
20
|
+
* ─── WHY IT IS SAFE TO WRITE CONCURRENTLY ─────────────────────────────────────────────────────────────
|
|
21
|
+
* Rows are deliberately kept under `MAX_ROW_BYTES` (512, macOS `PIPE_BUF`). A single `O_APPEND`
|
|
22
|
+
* `write(2)` at or below that size is indivisible, so two builds appending at the same instant cannot
|
|
23
|
+
* interleave halves of a line. The lock is therefore belt-and-braces for the APPEND and genuinely
|
|
24
|
+
* load-bearing for ROTATION, where a rename-and-reopen really does race.
|
|
25
|
+
*
|
|
26
|
+
* ─── IT MAY NEVER FAIL A BUILD ────────────────────────────────────────────────────────────────────────
|
|
27
|
+
* Every method here is best-effort and swallows its own errors. A build must never die because a log
|
|
28
|
+
* file was busy, unwritable, or on a full disk. Lock acquisition retries and then gives up and appends
|
|
29
|
+
* anyway — which the row-size invariant above makes safe.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BUILDS_LOG_FILE = "builds.log";
|
|
32
|
+
export declare const BUILDS_LOCK_FILE = "builds.log.lock";
|
|
33
|
+
/** START, and the two terminal kinds. `DONE-` is the greppable prefix that pairs with a START. */
|
|
34
|
+
export declare const BUILD_START = "START";
|
|
35
|
+
export declare const BUILD_DONE_SUCCESS = "DONE-SUCCESS";
|
|
36
|
+
export declare const BUILD_DONE_FAIL = "DONE-FAIL";
|
|
37
|
+
/** Rotate at 1 MB, keeping five generations (`.1` … `.5`); the old `.5` is dropped. */
|
|
38
|
+
export declare const MAX_BUILDS_LOG_BYTES: number;
|
|
39
|
+
export declare const BUILDS_LOG_GENERATIONS = 5;
|
|
40
|
+
/**
|
|
41
|
+
* macOS `PIPE_BUF`. A row at or under this size is written by ONE indivisible `write(2)`, which is what
|
|
42
|
+
* makes a lost lock a non-event rather than a corrupted file. Long paths are clipped to hold the line
|
|
43
|
+
* under it — see `clip`.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_ROW_BYTES = 512;
|
|
46
|
+
/**
|
|
47
|
+
* The handle a START row hands back, and the ONLY thing `finish()` accepts. Data-only (a class, per
|
|
48
|
+
* CLAUDE.md), carrying exactly the fields the DONE row needs to pair itself with its START: the uuid,
|
|
49
|
+
* the caller, the repo, and when it began (so `took=` is computed from one clock, not two).
|
|
50
|
+
*/
|
|
51
|
+
export declare class BuildTicket {
|
|
52
|
+
id: string;
|
|
53
|
+
by: string;
|
|
54
|
+
repo: string;
|
|
55
|
+
startedMs: number;
|
|
56
|
+
constructor(id: string, by: string, repo: string, startedMs: number);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One build that is STILL RUNNING — a START row with no matching `DONE-`, whose pid is still alive.
|
|
60
|
+
* Data-only. This is what the refusal message renders, so it carries the three things a reader needs to
|
|
61
|
+
* recognise the build in question: where it is, which tree, and how old it is.
|
|
62
|
+
*/
|
|
63
|
+
export declare class RunningBuild {
|
|
64
|
+
id: string;
|
|
65
|
+
by: string;
|
|
66
|
+
repo: string;
|
|
67
|
+
tree: string;
|
|
68
|
+
cwd: string;
|
|
69
|
+
branch: string;
|
|
70
|
+
pid: number;
|
|
71
|
+
startedMs: number;
|
|
72
|
+
constructor(id: string, by: string, repo: string, tree: string, cwd: string, branch: string, pid: number, startedMs: number);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The ledger. See the file docblock for why it is machine-global and why every operation swallows its
|
|
76
|
+
* own errors.
|
|
77
|
+
*
|
|
78
|
+
* `homeDir` is a parameter on every public method, defaulted to `os.homedir()`, for exactly the reason
|
|
79
|
+
* `HomeConfigService.configPath` takes one: a spec must be able to exercise the real code against a temp
|
|
80
|
+
* directory and must never touch the developer's actual `~/.webpieces`.
|
|
81
|
+
*/
|
|
82
|
+
export declare class BuildsLog {
|
|
83
|
+
private readonly dotDir;
|
|
84
|
+
constructor(dotDir: DotWebpieces);
|
|
85
|
+
/** `~/.webpieces/builds.log`. */
|
|
86
|
+
logPath(homeDir?: string): string;
|
|
87
|
+
/** `~/.webpieces/builds.log.lock` — `{"pid":N,"started":<epochMs>}`. */
|
|
88
|
+
lockPath(homeDir?: string): string;
|
|
89
|
+
/** `~/.webpieces/builds.log.<n>` — generation `n`, 1 being the most recent. */
|
|
90
|
+
rotatedPath(generation: number, homeDir?: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Record that a build is starting, and hand back the ticket its DONE row will need.
|
|
93
|
+
*
|
|
94
|
+
* `by` is the CALLER — `BuildGateOptions.stage`, i.e. `build` | `review` | `finish`. There is no
|
|
95
|
+
* second "caller" concept anywhere: the stage id already is one, and a second spelling of it would
|
|
96
|
+
* be the shim CLAUDE.md rejects.
|
|
97
|
+
*
|
|
98
|
+
* Returns a ticket even when the append failed. A build whose START row never landed still has to be
|
|
99
|
+
* able to call `finish()`; the alternative is a nullable return that every call site must branch on
|
|
100
|
+
* for a logging failure that is, by policy, not an error.
|
|
101
|
+
*/
|
|
102
|
+
start(by: string, startDir: string, homeDir?: string): BuildTicket;
|
|
103
|
+
/**
|
|
104
|
+
* Record that the build behind `ticket` has ended. `exitCode` 0 writes `DONE-SUCCESS`; anything else
|
|
105
|
+
* writes `DONE-FAIL` carrying the code, so `grep DONE-FAIL` lists every red build on the machine.
|
|
106
|
+
*/
|
|
107
|
+
finish(ticket: BuildTicket, exitCode: number, homeDir?: string): void;
|
|
108
|
+
/**
|
|
109
|
+
* Every build that is still live: a `START` with no matching `DONE-` row, whose pid is still alive.
|
|
110
|
+
*
|
|
111
|
+
* The pid filter is not an optimisation, it is what keeps the ledger from wedging the machine. A
|
|
112
|
+
* build killed with SIGKILL — an agent cancelled mid-run, a terminal closed — writes no DONE row, so
|
|
113
|
+
* without the liveness test its START would count forever and the fourth build would be refused for
|
|
114
|
+
* the rest of the machine's life. The uuid answers "which build"; the pid answers "is it still real".
|
|
115
|
+
*
|
|
116
|
+
* Only the CURRENT generation is read. A rotated-away START is by definition at least 1 MB of rows
|
|
117
|
+
* old and is not a build anyone is contending with.
|
|
118
|
+
*/
|
|
119
|
+
running(homeDir?: string): RunningBuild[];
|
|
120
|
+
/**
|
|
121
|
+
* The `@webpieces` release ACTUALLY EXECUTING — found by walking UP from this module's own directory
|
|
122
|
+
* to the nearest enclosing `node_modules/@webpieces/<pkg>/package.json`. `''` when this code is
|
|
123
|
+
* running from source rather than from an installed package (which is the state in this repo's own
|
|
124
|
+
* specs, and a perfectly ordinary answer).
|
|
125
|
+
*
|
|
126
|
+
* ─── WHY THIS IS NOT `WebpiecesVersions.readInstalled(root)` ──────────────────────────────────────
|
|
127
|
+
* They answer DIFFERENT QUESTIONS and merging them would break the older one. `readInstalled` joins
|
|
128
|
+
* `<root>/node_modules/@webpieces/...` at a FIXED tree root ON PURPOSE: its whole job is to detect
|
|
129
|
+
* DRIFT between what a tree PINS and what some other tree pins, and a walk-up would silently resolve
|
|
130
|
+
* a worktree with no install of its own to the primary clone's copy — hiding exactly the skew that
|
|
131
|
+
* guard exists to catch. This question is the opposite one: "whichever copy is running, name it", and
|
|
132
|
+
* for that the walk-up is the only correct answer. Do not fold them together.
|
|
133
|
+
*/
|
|
134
|
+
executingVersion(): string;
|
|
135
|
+
private versionOfEnclosingPackage;
|
|
136
|
+
private startRow;
|
|
137
|
+
private doneRow;
|
|
138
|
+
/**
|
|
139
|
+
* Hold a long path down to `max` characters by keeping its TAIL, which is the half that identifies
|
|
140
|
+
* the tree; a clipped value is marked with a leading `…` so nobody mistakes it for a real path.
|
|
141
|
+
*
|
|
142
|
+
* This is what keeps a row under `MAX_ROW_BYTES` — see the file docblock. `append` re-checks the
|
|
143
|
+
* assembled line as a backstop, because three clipped fields plus a long branch name can still add up.
|
|
144
|
+
*/
|
|
145
|
+
private clip;
|
|
146
|
+
/** The value of `<name>=` on a TSV row, or '' when the row does not carry it. */
|
|
147
|
+
private field;
|
|
148
|
+
private toRunningBuild;
|
|
149
|
+
/**
|
|
150
|
+
* Is `pid` still addressable? `process.kill(pid, 0)` sends no signal — it only asks the kernel. ESRCH
|
|
151
|
+
* is the ONE answer that proves death; EPERM proves the opposite (it exists, it is somebody else's).
|
|
152
|
+
* Same test, same reasoning, as `AgentWorktreeLockReader.isRunning`, including the accepted
|
|
153
|
+
* imprecision of pid reuse: being wrong in the "still running" direction costs one extra refusal,
|
|
154
|
+
* being wrong the other way lets a fourth build start.
|
|
155
|
+
*/
|
|
156
|
+
private isAlive;
|
|
157
|
+
/**
|
|
158
|
+
* Append one row, best-effort. Takes the lock so rotation cannot race, and appends ANYWAY when the
|
|
159
|
+
* lock cannot be had within `LOCK_TIMEOUT_MS` — the row is under `PIPE_BUF`, so an unlocked
|
|
160
|
+
* `O_APPEND` write is still indivisible, and a build must never die because a log file was busy.
|
|
161
|
+
*/
|
|
162
|
+
private append;
|
|
163
|
+
private truncateToRowLimit;
|
|
164
|
+
/**
|
|
165
|
+
* Take the ledger lock, retrying every `LOCK_RETRY_MS` until `LOCK_TIMEOUT_MS`. False means "carry on
|
|
166
|
+
* without it" — never an error, and never a reason to skip the append.
|
|
167
|
+
*
|
|
168
|
+
* The mechanism is `MainSyncStatusService.tryAcquireMainSyncLock`'s, proven and deliberately copied
|
|
169
|
+
* rather than re-invented: an `wx` (O_CREAT|O_EXCL) create so exactly one of N racers wins, a payload
|
|
170
|
+
* carrying pid + started so a dead holder is identifiable, stale reclaim gated on pid liveness, and a
|
|
171
|
+
* re-read afterwards to confirm the entry on disk is OURS (a simultaneous reclaimer could have
|
|
172
|
+
* unlinked ours and written its own between the two calls).
|
|
173
|
+
*/
|
|
174
|
+
private tryAcquireLock;
|
|
175
|
+
private releaseLock;
|
|
176
|
+
private createExclusive;
|
|
177
|
+
private lockHolderPid;
|
|
178
|
+
private isHolderAlive;
|
|
179
|
+
private holderIsUs;
|
|
180
|
+
/**
|
|
181
|
+
* `.4→.5, .3→.4, … .log→.1`, dropping the old `.5`. Runs INSIDE the lock, which is the one place the
|
|
182
|
+
* lock is genuinely load-bearing: a rename-and-reopen really does race, and a writer that opened the
|
|
183
|
+
* old inode mid-shift would append into a file nobody reads again.
|
|
184
|
+
*/
|
|
185
|
+
private rotateIfLarge;
|
|
186
|
+
private ensureDir;
|
|
187
|
+
private unlinkQuietly;
|
|
188
|
+
private renameQuietly;
|
|
189
|
+
private readTextOrEmpty;
|
|
190
|
+
private readLines;
|
|
191
|
+
private sleep;
|
|
192
|
+
private gitBranch;
|
|
193
|
+
}
|