patchwork-os 1.2.0-beta.2.canary.684 → 1.2.0-beta.2.canary.687
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/dist/butler/outcomeShadowLog.d.ts +15 -0
- package/dist/butler/outcomeShadowLog.js +38 -5
- package/dist/butler/outcomeShadowLog.js.map +1 -1
- package/dist/butler/promoteShadowOutcomes.d.ts +122 -0
- package/dist/butler/promoteShadowOutcomes.js +184 -0
- package/dist/butler/promoteShadowOutcomes.js.map +1 -0
- package/dist/index.js +31 -2
- package/dist/index.js.map +1 -1
- package/dist/recipes/cronClaim.d.ts +183 -0
- package/dist/recipes/cronClaim.js +241 -0
- package/dist/recipes/cronClaim.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +61 -4
- package/dist/recipes/scheduler.js +109 -8
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/tools/fanOut.js +23 -1
- package/dist/recipes/tools/fanOut.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +7 -0
- package/dist/recipes/yamlRunner.js +7 -0
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/package.json +1 -1
|
@@ -100,3 +100,18 @@ export interface ShadowSummary {
|
|
|
100
100
|
export declare function summariseShadowLog(opts?: {
|
|
101
101
|
dir?: string;
|
|
102
102
|
}): ShadowSummary;
|
|
103
|
+
/**
|
|
104
|
+
* Parse well-formed rows out of raw ledger text.
|
|
105
|
+
*
|
|
106
|
+
* The single parse rule, shared by the summary and by promotion. Two readers of
|
|
107
|
+
* one append-only file with two copies of "what counts as a row" is how a
|
|
108
|
+
* report and the thing it reports on come to disagree — and here the report is
|
|
109
|
+
* what a person reads before deciding to promote.
|
|
110
|
+
*
|
|
111
|
+
* Malformed lines are SKIPPED and counted in nothing: a half-written row from
|
|
112
|
+
* an interrupted append is not evidence, and must not inflate a number someone
|
|
113
|
+
* is about to make a trust decision on.
|
|
114
|
+
*/
|
|
115
|
+
export declare function parseShadowRows(text: string): ShadowOutcomeRow[];
|
|
116
|
+
/** Every well-formed row in the ledger, oldest first. */
|
|
117
|
+
export declare function readShadowRows(dir?: string): ShadowOutcomeRow[];
|
|
@@ -130,6 +130,28 @@ export function summariseShadowLog(opts = {}) {
|
|
|
130
130
|
return empty;
|
|
131
131
|
}
|
|
132
132
|
const out = { ...empty };
|
|
133
|
+
for (const row of parseShadowRows(text)) {
|
|
134
|
+
out.total++;
|
|
135
|
+
out[row.disposition]++;
|
|
136
|
+
if (row.wouldCountAsEvidence)
|
|
137
|
+
out.wouldCount++;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse well-formed rows out of raw ledger text.
|
|
143
|
+
*
|
|
144
|
+
* The single parse rule, shared by the summary and by promotion. Two readers of
|
|
145
|
+
* one append-only file with two copies of "what counts as a row" is how a
|
|
146
|
+
* report and the thing it reports on come to disagree — and here the report is
|
|
147
|
+
* what a person reads before deciding to promote.
|
|
148
|
+
*
|
|
149
|
+
* Malformed lines are SKIPPED and counted in nothing: a half-written row from
|
|
150
|
+
* an interrupted append is not evidence, and must not inflate a number someone
|
|
151
|
+
* is about to make a trust decision on.
|
|
152
|
+
*/
|
|
153
|
+
export function parseShadowRows(text) {
|
|
154
|
+
const rows = [];
|
|
133
155
|
for (const line of text.split("\n")) {
|
|
134
156
|
if (!line.trim())
|
|
135
157
|
continue;
|
|
@@ -145,11 +167,22 @@ export function summariseShadowLog(opts = {}) {
|
|
|
145
167
|
row.disposition !== "unknown") {
|
|
146
168
|
continue;
|
|
147
169
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
170
|
+
if (typeof row.ref !== "string" || !row.ref)
|
|
171
|
+
continue;
|
|
172
|
+
rows.push(row);
|
|
173
|
+
}
|
|
174
|
+
return rows;
|
|
175
|
+
}
|
|
176
|
+
/** Every well-formed row in the ledger, oldest first. */
|
|
177
|
+
export function readShadowRows(dir) {
|
|
178
|
+
const p = shadowLogPath(dir);
|
|
179
|
+
if (!existsSync(p))
|
|
180
|
+
return [];
|
|
181
|
+
try {
|
|
182
|
+
return parseShadowRows(readFileSync(p, "utf-8"));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
152
186
|
}
|
|
153
|
-
return out;
|
|
154
187
|
}
|
|
155
188
|
//# sourceMappingURL=outcomeShadowLog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outcomeShadowLog.js","sourceRoot":"","sources":["../../src/butler/outcomeShadowLog.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"outcomeShadowLog.js","sourceRoot":"","sources":["../../src/butler/outcomeShadowLog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promote graded Butler shadow rows into the trust ledger — flag-gated, OFF.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a separate module and not a branch in the ingester
|
|
5
|
+
*
|
|
6
|
+
* `outcomeIngester.ts` and `errandOutcomeGrader.ts` each carry a test asserting
|
|
7
|
+
* they cannot reach `OutcomeStore` — not "do not call it", but *do not import
|
|
8
|
+
* anything that could*. That is what makes "shadow-only" a structural fact
|
|
9
|
+
* rather than a promise, and it is why a reviewer can read the grader without
|
|
10
|
+
* checking whether some path in it writes evidence.
|
|
11
|
+
*
|
|
12
|
+
* Adding promotion there would have meant deleting those guards. This module
|
|
13
|
+
* exists so they stay exactly as they are: the grader stays a pure function of
|
|
14
|
+
* observed state, the ingester stays unable to write trust, and the single
|
|
15
|
+
* place that can is this one, which does nothing else.
|
|
16
|
+
*
|
|
17
|
+
* ## Operator path only
|
|
18
|
+
*
|
|
19
|
+
* Not registered as a recipe tool, and a test asserts it. A recipe step runs AS
|
|
20
|
+
* the worker, so a worker able to reach this could promote its own filings —
|
|
21
|
+
* manufacturing the evidence that raises its own dial. Same reasoning as
|
|
22
|
+
* `outcomes confirm|reject`, which is also operator-only for exactly this.
|
|
23
|
+
*
|
|
24
|
+
* ## Flag-gated OFF, and the flag is about EVIDENCE, not about code
|
|
25
|
+
*
|
|
26
|
+
* Promotion is one-way in the way that matters: once a row is folded, the dial
|
|
27
|
+
* it moved cannot be un-moved by deleting the row, because trust replay has
|
|
28
|
+
* already absorbed it into a checkpoint. So the gate is not "is the code
|
|
29
|
+
* finished" — it is "is there enough evidence to start counting this channel".
|
|
30
|
+
*
|
|
31
|
+
* At the time of writing the shadow ledger holds ONE confirmed row. That is a
|
|
32
|
+
* real positive act, correctly attributed, and it is one row. The flag exists so
|
|
33
|
+
* the code can be reviewed, tested and merged now, and the trust decision made
|
|
34
|
+
* later on more of them, by a person, deliberately.
|
|
35
|
+
*
|
|
36
|
+
* ## What is promoted, and what is refused
|
|
37
|
+
*
|
|
38
|
+
* Only `confirmed` and `junk` — the two dispositions that represent a POSITIVE
|
|
39
|
+
* ACT by the operator. `unknown` is never promoted under any flag, because
|
|
40
|
+
* "nobody has acted yet" is not evidence in either direction, and folding it as
|
|
41
|
+
* good is the trust-by-neglect defect closed four times already in this
|
|
42
|
+
* subsystem (#1064, #1318/#1319, #1320, #1322).
|
|
43
|
+
*
|
|
44
|
+
* ## `origin: "ingester"`, never `"manual"`
|
|
45
|
+
*
|
|
46
|
+
* `OutcomeStore` treats a `"manual"` disposition as STICKY against later
|
|
47
|
+
* ingester writes, because it represents a human's explicit judgment. Nothing
|
|
48
|
+
* here is that: it is an automated grade derived from an HTTP response. Marking
|
|
49
|
+
* these `"manual"` would let an automated observation permanently override an
|
|
50
|
+
* operator who had explicitly ruled the other way — precisely inverting the
|
|
51
|
+
* precedence the field exists to protect.
|
|
52
|
+
*/
|
|
53
|
+
/** Feature flag. Absent or anything but a truthy value ⇒ promotion refuses. */
|
|
54
|
+
export declare const BUTLER_PROMOTE_FLAG = "PATCHWORK_FLAG_BUTLER_PROMOTE";
|
|
55
|
+
export interface PromoteOptions {
|
|
56
|
+
/** `~/.patchwork` override (tests). */
|
|
57
|
+
patchworkDir: string;
|
|
58
|
+
/** Clock, injected so a written record is reproducible in tests. */
|
|
59
|
+
now?: number;
|
|
60
|
+
/** Report only; write nothing. Independent of the flag. */
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
/** Flag override, injected rather than read from the environment in tests. */
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface PromoteResult {
|
|
66
|
+
/** Every graded row considered — the DENOMINATOR, always reported. */
|
|
67
|
+
rows: number;
|
|
68
|
+
/** Rows whose disposition is `confirmed` or `junk`. */
|
|
69
|
+
promotable: number;
|
|
70
|
+
/** Rows withheld because the grader said `unknown`. */
|
|
71
|
+
withheld: number;
|
|
72
|
+
/** Rows written to the outcome log. Zero on a dry run or with the flag off. */
|
|
73
|
+
promoted: number;
|
|
74
|
+
/** Rows already carrying the same disposition in the trust ledger. */
|
|
75
|
+
alreadyRecorded: number;
|
|
76
|
+
/** Rows whose stored ref could not be turned back into a key, with why. */
|
|
77
|
+
unkeyable: {
|
|
78
|
+
ref: string;
|
|
79
|
+
reason: string;
|
|
80
|
+
}[];
|
|
81
|
+
/** True when nothing was written because the flag is off. */
|
|
82
|
+
blockedByFlag: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Split a stored `"<tool>:<id>"` key back into its parts.
|
|
86
|
+
*
|
|
87
|
+
* On the FIRST colon, which is the inverse of how `canonicalActionRef` joins
|
|
88
|
+
* them. Tool ids in this repo are dot-separated (`todoist.create_task`) and
|
|
89
|
+
* never contain a colon, while connector ids routinely do — so splitting on the
|
|
90
|
+
* last colon, or on every colon, would silently rekey the action and attach a
|
|
91
|
+
* confirmation to nothing.
|
|
92
|
+
*
|
|
93
|
+
* A URL-shaped key is refused rather than split. Those belong to the legacy
|
|
94
|
+
* `issueUrl` namespace, which `canonicalActionRef` explicitly refuses to
|
|
95
|
+
* produce; treating one as a `ref` here would write a row under a key no reader
|
|
96
|
+
* looks for.
|
|
97
|
+
*/
|
|
98
|
+
export declare function splitStoredRef(stored: string): {
|
|
99
|
+
tool: string;
|
|
100
|
+
id: string;
|
|
101
|
+
} | {
|
|
102
|
+
error: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Fold graded shadow rows into the trust ledger.
|
|
106
|
+
*
|
|
107
|
+
* Idempotent: a row whose disposition already matches what the ledger holds is
|
|
108
|
+
* counted as `alreadyRecorded` and NOT rewritten. `upsert` appends, so
|
|
109
|
+
* re-running without this would grow the file the autonomy gate reads with rows
|
|
110
|
+
* that say nothing new — and that file's byte cap is already what starves trust
|
|
111
|
+
* evidence (#1337).
|
|
112
|
+
*/
|
|
113
|
+
export declare function promoteShadowOutcomes(opts: PromoteOptions): PromoteResult;
|
|
114
|
+
/**
|
|
115
|
+
* Render the result.
|
|
116
|
+
*
|
|
117
|
+
* Leads with the denominator and never prints a bare promoted count, for the
|
|
118
|
+
* same reason the privacy shadow report refuses to: "3 promoted" reads as a
|
|
119
|
+
* measure of the channel's health when it partly measures how little was
|
|
120
|
+
* observed.
|
|
121
|
+
*/
|
|
122
|
+
export declare function formatPromoteResult(r: PromoteResult): string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promote graded Butler shadow rows into the trust ledger — flag-gated, OFF.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a separate module and not a branch in the ingester
|
|
5
|
+
*
|
|
6
|
+
* `outcomeIngester.ts` and `errandOutcomeGrader.ts` each carry a test asserting
|
|
7
|
+
* they cannot reach `OutcomeStore` — not "do not call it", but *do not import
|
|
8
|
+
* anything that could*. That is what makes "shadow-only" a structural fact
|
|
9
|
+
* rather than a promise, and it is why a reviewer can read the grader without
|
|
10
|
+
* checking whether some path in it writes evidence.
|
|
11
|
+
*
|
|
12
|
+
* Adding promotion there would have meant deleting those guards. This module
|
|
13
|
+
* exists so they stay exactly as they are: the grader stays a pure function of
|
|
14
|
+
* observed state, the ingester stays unable to write trust, and the single
|
|
15
|
+
* place that can is this one, which does nothing else.
|
|
16
|
+
*
|
|
17
|
+
* ## Operator path only
|
|
18
|
+
*
|
|
19
|
+
* Not registered as a recipe tool, and a test asserts it. A recipe step runs AS
|
|
20
|
+
* the worker, so a worker able to reach this could promote its own filings —
|
|
21
|
+
* manufacturing the evidence that raises its own dial. Same reasoning as
|
|
22
|
+
* `outcomes confirm|reject`, which is also operator-only for exactly this.
|
|
23
|
+
*
|
|
24
|
+
* ## Flag-gated OFF, and the flag is about EVIDENCE, not about code
|
|
25
|
+
*
|
|
26
|
+
* Promotion is one-way in the way that matters: once a row is folded, the dial
|
|
27
|
+
* it moved cannot be un-moved by deleting the row, because trust replay has
|
|
28
|
+
* already absorbed it into a checkpoint. So the gate is not "is the code
|
|
29
|
+
* finished" — it is "is there enough evidence to start counting this channel".
|
|
30
|
+
*
|
|
31
|
+
* At the time of writing the shadow ledger holds ONE confirmed row. That is a
|
|
32
|
+
* real positive act, correctly attributed, and it is one row. The flag exists so
|
|
33
|
+
* the code can be reviewed, tested and merged now, and the trust decision made
|
|
34
|
+
* later on more of them, by a person, deliberately.
|
|
35
|
+
*
|
|
36
|
+
* ## What is promoted, and what is refused
|
|
37
|
+
*
|
|
38
|
+
* Only `confirmed` and `junk` — the two dispositions that represent a POSITIVE
|
|
39
|
+
* ACT by the operator. `unknown` is never promoted under any flag, because
|
|
40
|
+
* "nobody has acted yet" is not evidence in either direction, and folding it as
|
|
41
|
+
* good is the trust-by-neglect defect closed four times already in this
|
|
42
|
+
* subsystem (#1064, #1318/#1319, #1320, #1322).
|
|
43
|
+
*
|
|
44
|
+
* ## `origin: "ingester"`, never `"manual"`
|
|
45
|
+
*
|
|
46
|
+
* `OutcomeStore` treats a `"manual"` disposition as STICKY against later
|
|
47
|
+
* ingester writes, because it represents a human's explicit judgment. Nothing
|
|
48
|
+
* here is that: it is an automated grade derived from an HTTP response. Marking
|
|
49
|
+
* these `"manual"` would let an automated observation permanently override an
|
|
50
|
+
* operator who had explicitly ruled the other way — precisely inverting the
|
|
51
|
+
* precedence the field exists to protect.
|
|
52
|
+
*/
|
|
53
|
+
import { OutcomeStore } from "../workers/outcomeStore.js";
|
|
54
|
+
import { readShadowRows, wouldCountAsEvidence, } from "./outcomeShadowLog.js";
|
|
55
|
+
/** Feature flag. Absent or anything but a truthy value ⇒ promotion refuses. */
|
|
56
|
+
export const BUTLER_PROMOTE_FLAG = "PATCHWORK_FLAG_BUTLER_PROMOTE";
|
|
57
|
+
function flagEnabled(opts) {
|
|
58
|
+
if (opts.enabled !== undefined)
|
|
59
|
+
return opts.enabled;
|
|
60
|
+
const v = process.env[BUTLER_PROMOTE_FLAG];
|
|
61
|
+
return v === "1" || v?.toLowerCase() === "true";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Split a stored `"<tool>:<id>"` key back into its parts.
|
|
65
|
+
*
|
|
66
|
+
* On the FIRST colon, which is the inverse of how `canonicalActionRef` joins
|
|
67
|
+
* them. Tool ids in this repo are dot-separated (`todoist.create_task`) and
|
|
68
|
+
* never contain a colon, while connector ids routinely do — so splitting on the
|
|
69
|
+
* last colon, or on every colon, would silently rekey the action and attach a
|
|
70
|
+
* confirmation to nothing.
|
|
71
|
+
*
|
|
72
|
+
* A URL-shaped key is refused rather than split. Those belong to the legacy
|
|
73
|
+
* `issueUrl` namespace, which `canonicalActionRef` explicitly refuses to
|
|
74
|
+
* produce; treating one as a `ref` here would write a row under a key no reader
|
|
75
|
+
* looks for.
|
|
76
|
+
*/
|
|
77
|
+
export function splitStoredRef(stored) {
|
|
78
|
+
if (/^https?:\/\//i.test(stored)) {
|
|
79
|
+
return {
|
|
80
|
+
error: "URL-shaped key belongs to the legacy issueUrl namespace, not the tool/id one",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const i = stored.indexOf(":");
|
|
84
|
+
if (i <= 0 || i === stored.length - 1) {
|
|
85
|
+
return { error: "not in '<tool>:<id>' form" };
|
|
86
|
+
}
|
|
87
|
+
return { tool: stored.slice(0, i), id: stored.slice(i + 1) };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fold graded shadow rows into the trust ledger.
|
|
91
|
+
*
|
|
92
|
+
* Idempotent: a row whose disposition already matches what the ledger holds is
|
|
93
|
+
* counted as `alreadyRecorded` and NOT rewritten. `upsert` appends, so
|
|
94
|
+
* re-running without this would grow the file the autonomy gate reads with rows
|
|
95
|
+
* that say nothing new — and that file's byte cap is already what starves trust
|
|
96
|
+
* evidence (#1337).
|
|
97
|
+
*/
|
|
98
|
+
export function promoteShadowOutcomes(opts) {
|
|
99
|
+
const rows = readShadowRows(opts.patchworkDir);
|
|
100
|
+
const enabled = flagEnabled(opts);
|
|
101
|
+
const result = {
|
|
102
|
+
rows: rows.length,
|
|
103
|
+
promotable: 0,
|
|
104
|
+
withheld: 0,
|
|
105
|
+
promoted: 0,
|
|
106
|
+
alreadyRecorded: 0,
|
|
107
|
+
unkeyable: [],
|
|
108
|
+
blockedByFlag: !enabled,
|
|
109
|
+
};
|
|
110
|
+
const store = new OutcomeStore(opts.patchworkDir);
|
|
111
|
+
const now = opts.now ?? Date.now();
|
|
112
|
+
// Last grade wins per ref. The ledger is append-only and an errand is
|
|
113
|
+
// observed repeatedly, so the same ref legitimately appears many times —
|
|
114
|
+
// promoting each one would write the same fact over and over, and an older
|
|
115
|
+
// grade could land after a newer one.
|
|
116
|
+
const latest = new Map();
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
const prev = latest.get(row.ref);
|
|
119
|
+
if (!prev || row.gradedAt >= prev.gradedAt)
|
|
120
|
+
latest.set(row.ref, row);
|
|
121
|
+
}
|
|
122
|
+
for (const row of latest.values()) {
|
|
123
|
+
if (!wouldCountAsEvidence(row.disposition)) {
|
|
124
|
+
result.withheld++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
result.promotable++;
|
|
128
|
+
const parts = splitStoredRef(row.ref);
|
|
129
|
+
if ("error" in parts) {
|
|
130
|
+
// Reported, never dropped. A row that cannot be keyed is a measurement
|
|
131
|
+
// gap, and a run that silently skipped some looks identical to a clean one.
|
|
132
|
+
result.unkeyable.push({ ref: row.ref, reason: parts.error });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (store.getDispositionForRef(parts) === row.disposition) {
|
|
136
|
+
result.alreadyRecorded++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!enabled || opts.dryRun)
|
|
140
|
+
continue;
|
|
141
|
+
store.upsert({
|
|
142
|
+
ref: parts,
|
|
143
|
+
disposition: row.disposition,
|
|
144
|
+
checkedAt: now,
|
|
145
|
+
...(row.recipe ? { recipeName: row.recipe } : {}),
|
|
146
|
+
// NOT "manual" — see the header. This is an automated grade, and marking
|
|
147
|
+
// it manual would make it sticky against a human who ruled otherwise.
|
|
148
|
+
origin: "ingester",
|
|
149
|
+
});
|
|
150
|
+
result.promoted++;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Render the result.
|
|
156
|
+
*
|
|
157
|
+
* Leads with the denominator and never prints a bare promoted count, for the
|
|
158
|
+
* same reason the privacy shadow report refuses to: "3 promoted" reads as a
|
|
159
|
+
* measure of the channel's health when it partly measures how little was
|
|
160
|
+
* observed.
|
|
161
|
+
*/
|
|
162
|
+
export function formatPromoteResult(r) {
|
|
163
|
+
const lines = [];
|
|
164
|
+
lines.push(`[butler-promote] ${r.rows} graded row(s) in the shadow ledger`);
|
|
165
|
+
lines.push(` ${r.promotable} promotable (confirmed or junk) · ${r.withheld} withheld as unknown`);
|
|
166
|
+
if (r.alreadyRecorded > 0) {
|
|
167
|
+
lines.push(` ${r.alreadyRecorded} already recorded with the same verdict`);
|
|
168
|
+
}
|
|
169
|
+
for (const u of r.unkeyable) {
|
|
170
|
+
lines.push(` unkeyable ${u.ref}: ${u.reason}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push("");
|
|
173
|
+
if (r.blockedByFlag) {
|
|
174
|
+
lines.push(` NOTHING WAS WRITTEN — ${BUTLER_PROMOTE_FLAG} is not set, so this run`);
|
|
175
|
+
lines.push(" was a report. Promotion moves the trust dial and a folded");
|
|
176
|
+
lines.push(" row cannot be un-folded by deleting it, so it is off until");
|
|
177
|
+
lines.push(" a person turns it on against evidence they have read.");
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
lines.push(` ${r.promoted} row(s) written to the trust ledger.`);
|
|
181
|
+
}
|
|
182
|
+
return `${lines.join("\n")}\n`;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=promoteShadowOutcomes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promoteShadowOutcomes.js","sourceRoot":"","sources":["../../src/butler/promoteShadowOutcomes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EACL,cAAc,EAEd,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,+BAA+B,CAAC;AA8BnE,SAAS,WAAW,CAAC,IAAoB;IACvC,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC;IACpD,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC3C,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAc;IAEd,IAAI,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACjC,OAAO;YACL,KAAK,EACH,8EAA8E;SACjF,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;AAC/D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAoB;IACxD,MAAM,IAAI,GAAuB,cAAc,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAkB;QAC5B,IAAI,EAAE,IAAI,CAAC,MAAM;QACjB,UAAU,EAAE,CAAC;QACb,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;QACX,eAAe,EAAE,CAAC;QAClB,SAAS,EAAE,EAAE;QACb,aAAa,EAAE,CAAC,OAAO;KACxB,CAAC;IAEF,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAEnC,sEAAsE;IACtE,yEAAyE;IACzE,2EAA2E;IAC3E,sCAAsC;IACtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAC;IACnD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YAC3C,MAAM,CAAC,QAAQ,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QACD,MAAM,CAAC,UAAU,EAAE,CAAC;QAEpB,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YACrB,uEAAuE;YACvE,4EAA4E;YAC5E,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YAC7D,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,WAAW,EAAE,CAAC;YAC1D,MAAM,CAAC,eAAe,EAAE,CAAC;YACzB,SAAS;QACX,CAAC;QAED,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM;YAAE,SAAS;QAEtC,KAAK,CAAC,MAAM,CAAC;YACX,GAAG,EAAE,KAAK;YACV,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,SAAS,EAAE,GAAG;YACd,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,yEAAyE;YACzE,sEAAsE;YACtE,MAAM,EAAE,UAAU;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,QAAQ,EAAE,CAAC;IACpB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAgB;IAClD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,IAAI,qCAAqC,CAAC,CAAC;IAC5E,KAAK,CAAC,IAAI,CACR,KAAK,CAAC,CAAC,UAAU,qCAAqC,CAAC,CAAC,QAAQ,sBAAsB,CACvF,CAAC;IACF,IAAI,CAAC,CAAC,eAAe,GAAG,CAAC,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,eAAe,yCAAyC,CAAC,CAAC;IAC9E,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CACR,2BAA2B,mBAAmB,0BAA0B,CACzE,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,6DAA6D,CAAC,CAAC;QAC1E,KAAK,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;QAC3E,KAAK,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;IACxE,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,sCAAsC,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4672,6 +4672,29 @@ if (process.argv[2] === "butler") {
|
|
|
4672
4672
|
}
|
|
4673
4673
|
process.exit(0);
|
|
4674
4674
|
}
|
|
4675
|
+
if (args[0] === "promote") {
|
|
4676
|
+
// The ONE path that turns a graded Butler row into trust evidence, and
|
|
4677
|
+
// it is off unless PATCHWORK_FLAG_BUTLER_PROMOTE is set. Operator path
|
|
4678
|
+
// only, for the same reason `outcomes confirm` is: a recipe step runs
|
|
4679
|
+
// AS the worker, so a worker able to reach this would manufacture the
|
|
4680
|
+
// evidence that raises its own dial.
|
|
4681
|
+
const { formatPromoteResult, promoteShadowOutcomes } = await import("./butler/promoteShadowOutcomes.js");
|
|
4682
|
+
const { patchworkHome } = await import("./patchworkHome.js");
|
|
4683
|
+
const res = promoteShadowOutcomes({
|
|
4684
|
+
patchworkDir: patchworkHome(),
|
|
4685
|
+
dryRun: args.includes("--dry-run"),
|
|
4686
|
+
});
|
|
4687
|
+
if (args.includes("--json")) {
|
|
4688
|
+
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
4689
|
+
}
|
|
4690
|
+
else {
|
|
4691
|
+
process.stdout.write(formatPromoteResult(res));
|
|
4692
|
+
}
|
|
4693
|
+
// Exit 0 either way: "the flag is off" is the CONFIGURED state, not a
|
|
4694
|
+
// failure, and a non-zero exit would make a cron wrapper report a
|
|
4695
|
+
// problem every time it ran as designed.
|
|
4696
|
+
process.exit(0);
|
|
4697
|
+
}
|
|
4675
4698
|
if (args[0] === "ingest") {
|
|
4676
4699
|
// Observations come from a file or stdin as a JSON array. They are NOT
|
|
4677
4700
|
// gathered here: reading the operator's trackers is a connector
|
|
@@ -4719,13 +4742,19 @@ if (process.argv[2] === "butler") {
|
|
|
4719
4742
|
}
|
|
4720
4743
|
process.exit(0);
|
|
4721
4744
|
}
|
|
4722
|
-
process.stderr.write("Usage: patchwork butler <shadow|ingest|observe> [--json]\n\n" +
|
|
4745
|
+
process.stderr.write("Usage: patchwork butler <shadow|ingest|observe|promote> [--json]\n\n" +
|
|
4723
4746
|
" shadow summarise the graded shadow ledger\n" +
|
|
4724
4747
|
" ingest [--file <path>|-] grade a JSON array of observations\n" +
|
|
4725
4748
|
" observe [--file <path>] discover errands from the run log,\n" +
|
|
4726
4749
|
" look up live Todoist state, then grade\n" +
|
|
4727
4750
|
" [--stale-after-days N]\n\n" +
|
|
4728
|
-
"
|
|
4751
|
+
" promote [--dry-run] fold confirmed/junk grades into the\n" +
|
|
4752
|
+
" trust ledger. Requires\n" +
|
|
4753
|
+
" PATCHWORK_FLAG_BUTLER_PROMOTE=1;\n" +
|
|
4754
|
+
" reports without writing otherwise.\n\n" +
|
|
4755
|
+
" shadow/ingest/observe are shadow-only and write no trust evidence.\n" +
|
|
4756
|
+
" `promote` is the single path that does, and it never promotes an\n" +
|
|
4757
|
+
" `unknown` grade under any flag.\n");
|
|
4729
4758
|
process.exit(2);
|
|
4730
4759
|
}
|
|
4731
4760
|
catch (err) {
|