@vercel/factory 0.0.8 → 0.0.10
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/{approval-qkxww0wJ.d.mts → approval-CDiBco2V.d.mts} +22 -14
- package/dist/approval-DdvEKLiE.mjs +132 -0
- package/dist/approval-DdvEKLiE.mjs.map +1 -0
- package/dist/build-factory.d.mts +12 -0
- package/dist/build-factory.mjs +16 -0
- package/dist/build-factory.mjs.map +1 -0
- package/dist/{task-BxTU3MJb.mjs → change-BPYhWKuL.mjs} +113 -220
- package/dist/change-BPYhWKuL.mjs.map +1 -0
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/eve/index.d.mts +99 -29
- package/dist/eve/index.mjs +84 -3
- package/dist/eve/index.mjs.map +1 -1
- package/dist/factory-config-Bu4mklm9.d.mts +39 -0
- package/dist/factory-config-ifd3JxiU.mjs +49 -0
- package/dist/factory-config-ifd3JxiU.mjs.map +1 -0
- package/dist/{github-BdWWueJP.d.mts → github-wThBfbm-.d.mts} +100 -3
- package/dist/github.d.mts +2 -2
- package/dist/github.mjs +195 -1
- package/dist/github.mjs.map +1 -1
- package/dist/{index-DUb-U5cy.d.mts → index-Cy4jfXh5.d.mts} +9 -9
- package/dist/index.d.mts +72 -47
- package/dist/index.mjs +203 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -1
- package/dist/task-BxTU3MJb.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as StoredEntry, i as StoreDriver, n as RecordExistsError, o as StoredRecord, r as RecordNotFoundError, s as VersionConflictError, t as CollectionName } from "./driver-CrZsy7sm.mjs";
|
|
2
|
+
import { a as factoryConfigSchema, i as factoryConfigRepositorySchema, n as FactoryConfigInput, r as defineFactory, t as FactoryConfig } from "./factory-config-Bu4mklm9.mjs";
|
|
2
3
|
import { a as signalSchema, c as ReplyTo, d as replyToSchema, f as schemaVersionSchema, i as signalOutcomeSchema, l as SCHEMA_VERSION, m as vocabularySchema, n as SignalOutcome, o as signalSourceSchema, p as usdSchema, r as SignalSource, s as standardSignalSources, t as Signal, u as isoDateTimeSchema } from "./signal-CHVEmzvI.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
+
import { $ as Cost, A as FactoryStores, At as repositoryIdsSchema, B as standardTaskKinds, C as ChangePatch, Ct as RepositoryId, D as CreateStoresOptions, Dt as factoryIdSchema, E as CreateSignalInput, Et as changeIdSchema, F as TaskApproval, G as taskOriginSchema, H as taskDevboxSchema, I as TaskKind, J as taskStateSchema, K as taskQuestionSchema, L as TaskState, M as TaskTransitionOptions, Mt as taskIdSchema, N as createStores, O as CreateTaskInput, Ot as receiptIdSchema, P as Task, Q as repositorySlugSchema, R as coordinationRepositoryIdsSchema, S as ChangeAssociationError, St as ReceiptId, T as CreateChangeInput, Tt as TaskId, U as taskExecutionRepositoryIdsSchema, V as taskApprovalSchema, W as taskKindSchema, X as RepositorySlug, Y as Repository, Z as repositorySchema, _ as findingVerificationMethodSchema, _t as changeSchema, a as approvalRuleSchema, at as receiptKindSchema, b as ApprovalDecision, bt as ChangeId, c as Finding, ct as receiptStateSchema, d as FindingVerification, dt as standardReceiptStates, et as Receipt, f as findingCategorySchema, ft as Change, g as findingVerificationLevelSchema, gt as changeEvidenceSchema, h as findingSchema, ht as Level, i as approvalPolicySchema, it as costSchema, j as TaskPatch, jt as signalIdSchema, k as DecideSignalOptions, kt as repositoryIdSchema, l as FindingCategory, lt as standardReceiptKinds, m as findingPrioritySchema, mt as ChangeState, n as ApprovalRule, nt as ReceiptPhase, o as defaultApprovalPolicy, ot as receiptPhaseSchema, p as findingFingerprint, pt as ChangeEvidence, q as taskSchema, r as ApprovalSubject, rt as ReceiptState, s as requiresApproval, st as receiptSchema, t as ApprovalPolicy, tt as ReceiptKind, u as FindingPriority, ut as standardReceiptPhases, v as findingVerificationSchema, vt as changeStateSchema, w as ChangeTransitionOptions, wt as SignalId, x as ApprovalError, xt as FactoryId, y as AppendReceiptInput, yt as levelSchema, z as harnessSchema } from "./approval-CDiBco2V.mjs";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
import { EveNextConfig, EveNextConfigFunction, EveNextConfigInput } from "eve/next";
|
|
6
6
|
//#region src/briefs.d.ts
|
|
7
7
|
/** The finding behind a task, when its signal carries one. */
|
|
8
8
|
declare function findingForTask(stores: FactoryStores, task: Task): Promise<Finding | null>;
|
|
@@ -14,50 +14,6 @@ declare function fixBrief(task: Task, repository: Repository, finding: Finding |
|
|
|
14
14
|
/** The scanner's starting message for a scan task. */
|
|
15
15
|
declare function scanBrief(task: Task, repository: Repository): string;
|
|
16
16
|
//#endregion
|
|
17
|
-
//#region src/schema/factory-config.d.ts
|
|
18
|
-
declare const factoryConfigRepositorySchema: z.ZodObject<{
|
|
19
|
-
slug: z.ZodString;
|
|
20
|
-
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
21
|
-
}, z.core.$strict>;
|
|
22
|
-
declare const factoryConfigSchema: z.ZodObject<{
|
|
23
|
-
connectors: z.ZodDefault<z.ZodObject<{
|
|
24
|
-
github: z.ZodArray<z.ZodObject<{
|
|
25
|
-
owner: z.ZodString;
|
|
26
|
-
connector: z.ZodString;
|
|
27
|
-
}, z.core.$strict>>;
|
|
28
|
-
slack: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
29
|
-
connector: z.ZodString;
|
|
30
|
-
}, z.core.$strict>>>;
|
|
31
|
-
}, z.core.$strict>>;
|
|
32
|
-
repositories: z.ZodArray<z.ZodObject<{
|
|
33
|
-
slug: z.ZodString;
|
|
34
|
-
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
35
|
-
}, z.core.$strict>>;
|
|
36
|
-
operators: z.ZodArray<z.ZodString>;
|
|
37
|
-
approval: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
38
|
-
source: z.ZodOptional<z.ZodString>;
|
|
39
|
-
kind: z.ZodOptional<z.ZodString>;
|
|
40
|
-
priority: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
41
|
-
P0: "P0";
|
|
42
|
-
P1: "P1";
|
|
43
|
-
P2: "P2";
|
|
44
|
-
P3: "P3";
|
|
45
|
-
}>>>;
|
|
46
|
-
require: z.ZodBoolean;
|
|
47
|
-
}, z.core.$strict>>>;
|
|
48
|
-
}, z.core.$strict>;
|
|
49
|
-
type FactoryConfig = z.infer<typeof factoryConfigSchema>;
|
|
50
|
-
type FactoryConfigInput = z.input<typeof factoryConfigSchema>;
|
|
51
|
-
declare function defineFactory(config: FactoryConfigInput): FactoryConfig;
|
|
52
|
-
//#endregion
|
|
53
|
-
//#region src/build-factory.d.ts
|
|
54
|
-
interface FactoryArgs<TConfig extends EveNextConfig = EveNextConfig> {
|
|
55
|
-
factoryConfig: FactoryConfigInput;
|
|
56
|
-
nextConfig: EveNextConfigInput<TConfig>;
|
|
57
|
-
root?: string;
|
|
58
|
-
}
|
|
59
|
-
declare function buildFactory<TConfig extends EveNextConfig>({ factoryConfig, nextConfig, root }: FactoryArgs<TConfig>): EveNextConfigFunction<TConfig>;
|
|
60
|
-
//#endregion
|
|
61
17
|
//#region src/changes.d.ts
|
|
62
18
|
interface AdoptPullRequestInput {
|
|
63
19
|
repositoryId: RepositoryId;
|
|
@@ -94,6 +50,75 @@ declare const DEFAULT_MAX_LAUNCH_ATTEMPTS = 45;
|
|
|
94
50
|
*/
|
|
95
51
|
declare function dispatchTask(stores: FactoryStores, taskId: TaskId, options: DispatchOptions): Promise<Task>;
|
|
96
52
|
//#endregion
|
|
53
|
+
//#region src/metrics.d.ts
|
|
54
|
+
/**
|
|
55
|
+
* The derived review bucket from the default risk x confidence table. Derived
|
|
56
|
+
* at read time, never stored; "blocked" is a bucket, not a change state.
|
|
57
|
+
*/
|
|
58
|
+
type ReviewBucket = "stamp" | "review" | "blocked";
|
|
59
|
+
declare function reviewBucket(risk: Level, confidence: Level): ReviewBucket;
|
|
60
|
+
interface LedgerRows {
|
|
61
|
+
changes: readonly Change[];
|
|
62
|
+
signals: readonly Signal[];
|
|
63
|
+
receipts: readonly Receipt[];
|
|
64
|
+
tasks: readonly Task[];
|
|
65
|
+
}
|
|
66
|
+
interface MetricsOptions {
|
|
67
|
+
/** Evaluation time; defaults to the current time. */
|
|
68
|
+
now?: Date;
|
|
69
|
+
/** Days a landed change must hold without a rollback. Default 7. */
|
|
70
|
+
holdDays?: number;
|
|
71
|
+
}
|
|
72
|
+
interface LedgerMetrics {
|
|
73
|
+
/**
|
|
74
|
+
* Changes a human merged that reached deployed (rolled_out for high risk)
|
|
75
|
+
* and did not roll back within the hold window. Includes provisional ones.
|
|
76
|
+
*/
|
|
77
|
+
landed: number;
|
|
78
|
+
/** Landed changes still inside the hold window; the number may drop. */
|
|
79
|
+
provisional: number;
|
|
80
|
+
/** landed / (landed + rolled back + closed after ready). Null when nothing decided. */
|
|
81
|
+
stickRate: number | null;
|
|
82
|
+
/**
|
|
83
|
+
* Not computable from ledger rows: needs a record of human commits pushed
|
|
84
|
+
* to the pull request after ready, which nothing writes today.
|
|
85
|
+
*/
|
|
86
|
+
stampedClean: null;
|
|
87
|
+
/** Median ms from the ready receipt to the merged receipt, per review bucket. */
|
|
88
|
+
humanTimeMs: {
|
|
89
|
+
stamp: number | null;
|
|
90
|
+
review: number | null;
|
|
91
|
+
};
|
|
92
|
+
/** Sum of settled task cost over landed changes / landed. Null until budgets settle. */
|
|
93
|
+
costPerLandedUsd: number | null;
|
|
94
|
+
/** Signals triage turned away plus changes closed in verification. */
|
|
95
|
+
filtered: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The six product metrics from ledger rows alone. One rule governs them: a
|
|
99
|
+
* change counts only after a human merged it and it stayed. Activity that
|
|
100
|
+
* never landed is not a result. Compute over rows already filtered to the
|
|
101
|
+
* period and repositories you are reporting on.
|
|
102
|
+
*/
|
|
103
|
+
declare function computeLedgerMetrics(rows: LedgerRows, options?: MetricsOptions): LedgerMetrics;
|
|
104
|
+
type HealthColor = "green" | "yellow" | "red";
|
|
105
|
+
interface HealthOptions {
|
|
106
|
+
now?: Date;
|
|
107
|
+
/** Open-change cap from factory configuration; omitted disables that check. */
|
|
108
|
+
maxOpenChanges?: number;
|
|
109
|
+
}
|
|
110
|
+
interface RepositoryHealth {
|
|
111
|
+
color: HealthColor;
|
|
112
|
+
/** Every rule that fired, so the color is explainable from rows. */
|
|
113
|
+
reasons: string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Green, yellow, or red from rows, never from a model. Budget and onboarding
|
|
117
|
+
* signals are not evaluated yet: budgets do not settle and onboarding
|
|
118
|
+
* investigations are not recorded, so those spec rules have no rows to read.
|
|
119
|
+
*/
|
|
120
|
+
declare function computeRepositoryHealth(rows: LedgerRows, options?: HealthOptions): RepositoryHealth;
|
|
121
|
+
//#endregion
|
|
97
122
|
//#region src/schema/budget.d.ts
|
|
98
123
|
declare const budgetMonthSchema: z.ZodString;
|
|
99
124
|
declare const budgetSchema: z.ZodObject<{
|
|
@@ -183,5 +208,5 @@ interface SweepResult {
|
|
|
183
208
|
*/
|
|
184
209
|
declare function sweepQueued(stores: FactoryStores, options: SweepOptions): Promise<SweepResult>;
|
|
185
210
|
//#endregion
|
|
186
|
-
export { AdoptPullRequestInput, AppendReceiptInput, ApprovalDecision, ApprovalError, ApprovalPolicy, ApprovalRule, ApprovalSubject, Budget, Change, ChangeEvidence, ChangeId, ChangePatch, ChangeState, ChangeTransitionOptions, CollectionName, Cost, CreateChangeInput, CreateSignalInput, CreateStoresOptions, CreateTaskInput, DEFAULT_MAX_LAUNCH_ATTEMPTS, DecideSignalOptions, DispatchOptions, DispatchRefusedError,
|
|
211
|
+
export { AdoptPullRequestInput, AppendReceiptInput, ApprovalDecision, ApprovalError, ApprovalPolicy, ApprovalRule, ApprovalSubject, Budget, Change, ChangeAssociationError, ChangeEvidence, ChangeId, ChangePatch, ChangeState, ChangeTransitionOptions, CollectionName, Cost, CreateChangeInput, CreateSignalInput, CreateStoresOptions, CreateTaskInput, DEFAULT_MAX_LAUNCH_ATTEMPTS, DecideSignalOptions, DispatchOptions, DispatchRefusedError, FactoryConfig, FactoryConfigInput, FactoryId, FactoryStores, Finding, FindingCategory, FindingPriority, FindingVerification, HealthColor, HealthOptions, InvalidTransitionError, LedgerMetrics, LedgerRows, Level, MetricsOptions, Receipt, ReceiptId, ReceiptKind, ReceiptPhase, ReceiptState, RecordExistsError, RecordNotFoundError, ReplyTo, Repository, RepositoryHealth, RepositoryId, RepositorySlug, ReviewBucket, SCHEMA_VERSION, Signal, SignalCreator, SignalId, SignalOutcome, SignalSource, StoreDriver, StoredEntry, StoredRecord, SweepOptions, SweepResult, Task, TaskApproval, TaskId, TaskKind, TaskLauncher, TaskPatch, TaskState, TaskTransitionOptions, Transition, TransitionActor, VersionConflictError, adoptPullRequest, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetMonthSchema, budgetSchema, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, computeLedgerMetrics, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defineFactory, dispatchTask, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepQueued, taskApprovalSchema, taskDevboxSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, terminalStates, usdSchema, vocabularySchema };
|
|
187
212
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { a as repositoryIdsSchema, c as SCHEMA_VERSION, d as schemaVersionSchema, f as usdSchema, i as repositoryIdSchema, l as isoDateTimeSchema, n as factoryIdSchema, o as signalIdSchema, p as vocabularySchema, r as receiptIdSchema, s as taskIdSchema, t as changeIdSchema, u as replyToSchema } from "./id-DwCihQr4.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as findingCategorySchema, c as findingSchema, d as findingVerificationSchema, i as requiresApproval, l as findingVerificationLevelSchema, n as approvalRuleSchema, o as findingFingerprint, r as defaultApprovalPolicy, s as findingPrioritySchema, t as approvalPolicySchema, u as findingVerificationMethodSchema } from "./approval-DdvEKLiE.mjs";
|
|
3
|
+
import { _ as taskSchema, a as DEFAULT_MAX_LAUNCH_ATTEMPTS, c as coordinationRepositoryIdsSchema, d as taskApprovalSchema, f as taskDevboxSchema, g as taskQuestionSchema, h as taskOriginSchema, i as levelSchema, l as harnessSchema, m as taskKindSchema, n as changeSchema, o as DispatchRefusedError, p as taskExecutionRepositoryIdsSchema, r as changeStateSchema, s as dispatchTask, t as changeEvidenceSchema, u as standardTaskKinds, v as taskStateSchema } from "./change-BPYhWKuL.mjs";
|
|
3
4
|
import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "./driver-Bbd25rNM.mjs";
|
|
5
|
+
import { a as repositorySlugSchema, i as repositorySchema, n as factoryConfigRepositorySchema, r as factoryConfigSchema, t as defineFactory } from "./factory-config-ifd3JxiU.mjs";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { readdirSync } from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
8
|
//#region src/briefs.ts
|
|
9
9
|
/** The finding behind a task, when its signal carries one. */
|
|
10
10
|
async function findingForTask(stores, task) {
|
|
@@ -38,59 +38,6 @@ function scanBrief(task, repository) {
|
|
|
38
38
|
].join("\n");
|
|
39
39
|
}
|
|
40
40
|
//#endregion
|
|
41
|
-
//#region src/schema/repository.ts
|
|
42
|
-
const repositorySlugSchema = z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?\/[a-z0-9._-]+$/).describe("owner/name, lowercase, e.g. acme/web.");
|
|
43
|
-
const repositorySchema = z.strictObject({
|
|
44
|
-
schemaVersion: schemaVersionSchema,
|
|
45
|
-
id: repositoryIdSchema,
|
|
46
|
-
slug: repositorySlugSchema,
|
|
47
|
-
defaultBranch: z.string().min(1).describe("Branch changes land on, usually main."),
|
|
48
|
-
enabled: z.boolean().describe("Whether the factory may work on this repository."),
|
|
49
|
-
dependsOn: z.array(repositorySlugSchema).describe("Slugs of repositories this one consumes, for multi-repo work ordering."),
|
|
50
|
-
devbox: z.strictObject({
|
|
51
|
-
projectId: z.string().min(1),
|
|
52
|
-
teamId: z.string().min(1),
|
|
53
|
-
snapshot: z.string().min(1).optional().describe("Prebuilt snapshot to boot from.")
|
|
54
|
-
}).optional().describe("Devbox execution config. Absent when this repository never uses devboxes."),
|
|
55
|
-
connectedAt: isoDateTimeSchema
|
|
56
|
-
});
|
|
57
|
-
//#endregion
|
|
58
|
-
//#region src/schema/factory-config.ts
|
|
59
|
-
const factoryConfigRepositorySchema = z.strictObject({
|
|
60
|
-
slug: repositorySlugSchema.describe("Repository this factory may work on."),
|
|
61
|
-
dependsOn: z.array(repositorySlugSchema).default([]).describe("Repositories that must land changes before this repository.")
|
|
62
|
-
});
|
|
63
|
-
const githubConnectorSchema = z.strictObject({
|
|
64
|
-
owner: z.string().regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/).describe("GitHub user or organization that owns this connector installation."),
|
|
65
|
-
connector: z.string().regex(/^github\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/).describe("Vercel Connect UID installed for this GitHub owner.")
|
|
66
|
-
});
|
|
67
|
-
const slackConnectorSchema = z.strictObject({ connector: z.string().regex(/^slack\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/).describe("Vercel Connect UID installed for this Slack app.") });
|
|
68
|
-
const factoryConfigSchema = z.strictObject({
|
|
69
|
-
connectors: z.strictObject({
|
|
70
|
-
github: z.array(githubConnectorSchema).refine((connectors) => new Set(connectors.map(({ owner }) => owner)).size === connectors.length, "GitHub connector owners must be unique."),
|
|
71
|
-
slack: z.array(slackConnectorSchema).max(1, "A factory may configure one Slack connector.").default([])
|
|
72
|
-
}).default({
|
|
73
|
-
github: [],
|
|
74
|
-
slack: []
|
|
75
|
-
}).describe("Provider connections available to this factory."),
|
|
76
|
-
repositories: z.array(factoryConfigRepositorySchema).min(1).describe("Repositories this factory is allowed to inspect and change."),
|
|
77
|
-
operators: z.array(z.string().min(1)).min(1).describe("Provider-qualified identities allowed to operate this factory."),
|
|
78
|
-
approval: approvalPolicySchema.default(defaultApprovalPolicy).describe("Which tasks wait for a human before they run. First match wins.")
|
|
79
|
-
});
|
|
80
|
-
function defineFactory(config) {
|
|
81
|
-
return factoryConfigSchema.parse(config);
|
|
82
|
-
}
|
|
83
|
-
//#endregion
|
|
84
|
-
//#region src/build-factory.ts
|
|
85
|
-
function buildFactory({ factoryConfig, nextConfig, root = process.cwd() }) {
|
|
86
|
-
defineFactory(factoryConfig);
|
|
87
|
-
const agentDefinitions = Object.fromEntries(readdirSync(path.join(root, "agents"), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => [entry.name, path.join(root, "agents", entry.name)]));
|
|
88
|
-
return async (phase, context) => {
|
|
89
|
-
const { withEve } = await import("eve/next");
|
|
90
|
-
return withEve(nextConfig, { agents: agentDefinitions })(phase, context);
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
41
|
//#region src/changes.ts
|
|
95
42
|
async function adoptPullRequest(stores, input) {
|
|
96
43
|
const existing = (await stores.changes.list()).find((change) => change.repositoryId === input.repositoryId && change.prNumber === input.prNumber && (change.state === "verifying" || change.state === "ready"));
|
|
@@ -109,6 +56,168 @@ async function adoptPullRequest(stores, input) {
|
|
|
109
56
|
});
|
|
110
57
|
}
|
|
111
58
|
//#endregion
|
|
59
|
+
//#region src/metrics.ts
|
|
60
|
+
const bucketTable = {
|
|
61
|
+
low: {
|
|
62
|
+
low: "review",
|
|
63
|
+
medium: "stamp",
|
|
64
|
+
high: "stamp"
|
|
65
|
+
},
|
|
66
|
+
medium: {
|
|
67
|
+
low: "review",
|
|
68
|
+
medium: "review",
|
|
69
|
+
high: "stamp"
|
|
70
|
+
},
|
|
71
|
+
high: {
|
|
72
|
+
low: "blocked",
|
|
73
|
+
medium: "review",
|
|
74
|
+
high: "review"
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function reviewBucket(risk, confidence) {
|
|
78
|
+
return bucketTable[risk][confidence];
|
|
79
|
+
}
|
|
80
|
+
function collectFacts(rows) {
|
|
81
|
+
const byChange = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const receipt of rows.receipts) {
|
|
83
|
+
if (receipt.kind !== "change" || receipt.changeId === void 0) continue;
|
|
84
|
+
const list = byChange.get(receipt.changeId) ?? [];
|
|
85
|
+
list.push(receipt);
|
|
86
|
+
byChange.set(receipt.changeId, list);
|
|
87
|
+
}
|
|
88
|
+
const at = (receipts, state) => {
|
|
89
|
+
const found = receipts?.find((receipt) => receipt.state === state);
|
|
90
|
+
return found === void 0 ? null : Date.parse(found.recordedAt);
|
|
91
|
+
};
|
|
92
|
+
return rows.changes.map((change) => {
|
|
93
|
+
const receipts = byChange.get(change.id);
|
|
94
|
+
const requiredState = change.risk === "high" ? "rolled_out" : "deployed";
|
|
95
|
+
return {
|
|
96
|
+
change,
|
|
97
|
+
readyAt: at(receipts, "ready"),
|
|
98
|
+
mergedAt: at(receipts, "merged"),
|
|
99
|
+
reachedAt: at(receipts, requiredState),
|
|
100
|
+
rolledBackAt: at(receipts, "rolled_back")
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** True for a change a human saw as ready and then closed unmerged. */
|
|
105
|
+
function closedAfterReady(facts) {
|
|
106
|
+
return facts.change.state === "closed" && (facts.readyAt !== null || facts.change.decidedBy !== void 0);
|
|
107
|
+
}
|
|
108
|
+
function median(values) {
|
|
109
|
+
if (values.length === 0) return null;
|
|
110
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
111
|
+
const mid = Math.floor(sorted.length / 2);
|
|
112
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The six product metrics from ledger rows alone. One rule governs them: a
|
|
116
|
+
* change counts only after a human merged it and it stayed. Activity that
|
|
117
|
+
* never landed is not a result. Compute over rows already filtered to the
|
|
118
|
+
* period and repositories you are reporting on.
|
|
119
|
+
*/
|
|
120
|
+
function computeLedgerMetrics(rows, options = {}) {
|
|
121
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
122
|
+
const holdMs = (options.holdDays ?? 7) * 24 * 60 * 60 * 1e3;
|
|
123
|
+
const facts = collectFacts(rows);
|
|
124
|
+
let landed = 0;
|
|
125
|
+
let provisional = 0;
|
|
126
|
+
let rolledBack = 0;
|
|
127
|
+
let closedAfterReadyCount = 0;
|
|
128
|
+
let closedInVerification = 0;
|
|
129
|
+
const landedChangeIds = /* @__PURE__ */ new Set();
|
|
130
|
+
for (const fact of facts) {
|
|
131
|
+
const { change } = fact;
|
|
132
|
+
if (fact.rolledBackAt !== null) {
|
|
133
|
+
rolledBack += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (change.state === "closed") {
|
|
137
|
+
if (closedAfterReady(fact)) closedAfterReadyCount += 1;
|
|
138
|
+
else closedInVerification += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (change.decidedBy !== void 0 && fact.reachedAt !== null) {
|
|
142
|
+
landed += 1;
|
|
143
|
+
landedChangeIds.add(change.id);
|
|
144
|
+
if (now - fact.reachedAt < holdMs) provisional += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const stickDenominator = landed + rolledBack + closedAfterReadyCount;
|
|
148
|
+
const humanTimes = {
|
|
149
|
+
stamp: [],
|
|
150
|
+
review: []
|
|
151
|
+
};
|
|
152
|
+
for (const fact of facts) {
|
|
153
|
+
if (fact.readyAt === null || fact.mergedAt === null) continue;
|
|
154
|
+
const bucket = reviewBucket(fact.change.risk, fact.change.confidence);
|
|
155
|
+
if (bucket === "blocked") continue;
|
|
156
|
+
humanTimes[bucket].push(fact.mergedAt - fact.readyAt);
|
|
157
|
+
}
|
|
158
|
+
let settledOverLanded = 0;
|
|
159
|
+
let sawSettledCost = false;
|
|
160
|
+
for (const task of rows.tasks) {
|
|
161
|
+
if (task.settledUsd === void 0) continue;
|
|
162
|
+
if (task.kind === "code_change" && task.changeId !== void 0 && landedChangeIds.has(task.changeId)) {
|
|
163
|
+
settledOverLanded += task.settledUsd;
|
|
164
|
+
sawSettledCost = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const rejectedSignals = rows.signals.filter((signal) => signal.outcome !== "pending" && signal.outcome !== "accepted").length;
|
|
168
|
+
return {
|
|
169
|
+
landed,
|
|
170
|
+
provisional,
|
|
171
|
+
stickRate: stickDenominator === 0 ? null : landed / stickDenominator,
|
|
172
|
+
stampedClean: null,
|
|
173
|
+
humanTimeMs: {
|
|
174
|
+
stamp: median(humanTimes.stamp),
|
|
175
|
+
review: median(humanTimes.review)
|
|
176
|
+
},
|
|
177
|
+
costPerLandedUsd: sawSettledCost && landed > 0 ? settledOverLanded / landed : null,
|
|
178
|
+
filtered: rejectedSignals + closedInVerification
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
|
|
182
|
+
/**
|
|
183
|
+
* Green, yellow, or red from rows, never from a model. Budget and onboarding
|
|
184
|
+
* signals are not evaluated yet: budgets do not settle and onboarding
|
|
185
|
+
* investigations are not recorded, so those spec rules have no rows to read.
|
|
186
|
+
*/
|
|
187
|
+
function computeRepositoryHealth(rows, options = {}) {
|
|
188
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
189
|
+
const dayMs = 864e5;
|
|
190
|
+
const red = [];
|
|
191
|
+
const yellow = [];
|
|
192
|
+
for (const receipt of rows.receipts) {
|
|
193
|
+
if (now - Date.parse(receipt.recordedAt) >= dayMs) continue;
|
|
194
|
+
if (receipt.state === "rolled_back") red.push(`rollback in the last 24h (${receipt.receiptId})`);
|
|
195
|
+
if (receipt.state === "cancelled" && receipt.reason?.toLowerCase().includes("kill")) red.push(`kill in the last 24h (${receipt.receiptId})`);
|
|
196
|
+
}
|
|
197
|
+
const openQuestions = rows.tasks.filter((task) => task.kind === "question" && task.state === "needs_human").length;
|
|
198
|
+
if (openQuestions > 0) yellow.push(`${openQuestions} open question(s)`);
|
|
199
|
+
const needsHuman = rows.tasks.filter((task) => task.kind !== "question" && task.state === "needs_human").length;
|
|
200
|
+
if (needsHuman > 0) yellow.push(`${needsHuman} task(s) waiting on a human`);
|
|
201
|
+
const blocked = rows.changes.filter((change) => openChangeStates.has(change.state) && reviewBucket(change.risk, change.confidence) === "blocked").length;
|
|
202
|
+
if (blocked > 0) yellow.push(`${blocked} change(s) in the blocked bucket`);
|
|
203
|
+
if (options.maxOpenChanges !== void 0) {
|
|
204
|
+
const open = rows.changes.filter((change) => openChangeStates.has(change.state)).length;
|
|
205
|
+
if (open >= options.maxOpenChanges) yellow.push(`open changes at the cap (${open}/${options.maxOpenChanges})`);
|
|
206
|
+
}
|
|
207
|
+
if (red.length > 0) return {
|
|
208
|
+
color: "red",
|
|
209
|
+
reasons: red
|
|
210
|
+
};
|
|
211
|
+
if (yellow.length > 0) return {
|
|
212
|
+
color: "yellow",
|
|
213
|
+
reasons: yellow
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
color: "green",
|
|
217
|
+
reasons: []
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
112
221
|
//#region src/schema/budget.ts
|
|
113
222
|
const budgetMonthSchema = z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/);
|
|
114
223
|
const budgetSchema = z.strictObject({
|
|
@@ -139,6 +248,7 @@ const standardReceiptStates = [
|
|
|
139
248
|
"needs_human",
|
|
140
249
|
"cancelled",
|
|
141
250
|
"settled",
|
|
251
|
+
"ready",
|
|
142
252
|
"merged",
|
|
143
253
|
"deployed",
|
|
144
254
|
"rolled_out",
|
|
@@ -491,6 +601,18 @@ var ApprovalError = class extends Error {
|
|
|
491
601
|
this.reason = reason;
|
|
492
602
|
}
|
|
493
603
|
};
|
|
604
|
+
var ChangeAssociationError = class extends Error {
|
|
605
|
+
taskId;
|
|
606
|
+
repositoryId;
|
|
607
|
+
reason;
|
|
608
|
+
constructor(taskId, repositoryId, reason) {
|
|
609
|
+
super(`change for task "${taskId}" refused: ${reason}`);
|
|
610
|
+
this.name = "ChangeAssociationError";
|
|
611
|
+
this.taskId = taskId;
|
|
612
|
+
this.repositoryId = repositoryId;
|
|
613
|
+
this.reason = reason;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
494
616
|
const taskReceiptStates = {
|
|
495
617
|
running: "dispatched",
|
|
496
618
|
succeeded: "succeeded",
|
|
@@ -499,6 +621,7 @@ const taskReceiptStates = {
|
|
|
499
621
|
cancelled: "cancelled"
|
|
500
622
|
};
|
|
501
623
|
const changeReceiptStates = {
|
|
624
|
+
ready: "ready",
|
|
502
625
|
merged: "merged",
|
|
503
626
|
deployed: "deployed",
|
|
504
627
|
rolled_out: "rolled_out",
|
|
@@ -741,6 +864,13 @@ function createStores(options) {
|
|
|
741
864
|
createdAt: timestamp,
|
|
742
865
|
updatedAt: timestamp
|
|
743
866
|
});
|
|
867
|
+
await mustRead("repositories", change.repositoryId, repositorySchema);
|
|
868
|
+
if (change.taskId !== void 0) {
|
|
869
|
+
const task = (await mustRead("tasks", change.taskId, taskSchema)).value;
|
|
870
|
+
if (task.kind !== "code_change") throw new ChangeAssociationError(change.taskId, change.repositoryId, "changes can only be produced by code_change tasks");
|
|
871
|
+
const taskRepositoryId = task.repositoryIds[0];
|
|
872
|
+
if (taskRepositoryId !== change.repositoryId) throw new ChangeAssociationError(change.taskId, change.repositoryId, `repository "${change.repositoryId}" does not match task repository "${taskRepositoryId}"`);
|
|
873
|
+
}
|
|
744
874
|
await driver.insert("changes", change.id, change);
|
|
745
875
|
return change;
|
|
746
876
|
},
|
|
@@ -858,6 +988,21 @@ async function sweepQueued(stores, options) {
|
|
|
858
988
|
};
|
|
859
989
|
const queued = (await stores.tasks.list()).filter((task) => task.state === "queued" && task.kind !== "question").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
860
990
|
for (const task of queued) {
|
|
991
|
+
if (task.kind === "coordination") {
|
|
992
|
+
result.skipped.push({
|
|
993
|
+
taskId: task.id,
|
|
994
|
+
reason: "coordination tasks are reconciled, not dispatched"
|
|
995
|
+
});
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
const executionRepositoryIds = taskExecutionRepositoryIdsSchema.safeParse(task.repositoryIds);
|
|
999
|
+
if (!executionRepositoryIds.success) {
|
|
1000
|
+
result.skipped.push({
|
|
1001
|
+
taskId: task.id,
|
|
1002
|
+
reason: "task execution requires exactly one repository"
|
|
1003
|
+
});
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
861
1006
|
if (task.approval === "required") {
|
|
862
1007
|
result.skipped.push({
|
|
863
1008
|
taskId: task.id,
|
|
@@ -873,8 +1018,7 @@ async function sweepQueued(stores, options) {
|
|
|
873
1018
|
});
|
|
874
1019
|
continue;
|
|
875
1020
|
}
|
|
876
|
-
const
|
|
877
|
-
const repository = repositoryId === void 0 ? null : await stores.repositories.get(repositoryId);
|
|
1021
|
+
const repository = await stores.repositories.get(executionRepositoryIds.data[0]);
|
|
878
1022
|
if (repository === null) {
|
|
879
1023
|
result.skipped.push({
|
|
880
1024
|
taskId: task.id,
|
|
@@ -902,6 +1046,6 @@ async function sweepQueued(stores, options) {
|
|
|
902
1046
|
return result;
|
|
903
1047
|
}
|
|
904
1048
|
//#endregion
|
|
905
|
-
export { ApprovalError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, VersionConflictError, adoptPullRequest, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetMonthSchema, budgetSchema,
|
|
1049
|
+
export { ApprovalError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, VersionConflictError, adoptPullRequest, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetMonthSchema, budgetSchema, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, computeLedgerMetrics, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defineFactory, dispatchTask, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepQueued, taskApprovalSchema, taskDevboxSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, terminalStates, usdSchema, vocabularySchema };
|
|
906
1050
|
|
|
907
1051
|
//# sourceMappingURL=index.mjs.map
|