@haiyangbg/buildbeat 1.21.0 → 2.0.0-beta.2
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/CHANGELOG.md +21 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/bin/buildbeat-v2.js +6 -0
- package/docs/BuildBeat v2/357/274/232AI /345/216/237/347/224/237/350/275/257/344/273/266/344/272/244/344/273/230/346/216/247/345/210/266/345/271/263/351/235/242.md" +2053 -0
- package/docs/CAPABILITY-MATRIX.md +4 -4
- package/docs/CLI.md +4 -4
- package/docs/EXECUTION-PLAN.md +1 -1
- package/docs/RELEASING.md +5 -5
- package/docs/ROADMAP.md +4 -1
- package/docs/V1.21-RELEASE-EVIDENCE-2026-08-25.md +55 -0
- package/docs/V2-D2-DECISION-CARD.md +37 -0
- package/docs/V2-DECISIONS.md +11 -0
- package/docs/V2-ITERATION-01.md +60 -0
- package/docs/V2-ITERATION-02.md +32 -0
- package/docs/V2-ITERATION-03.md +30 -0
- package/docs/V2-ITERATION-04.md +29 -0
- package/docs/V2-ITERATION-05.md +20 -0
- package/docs/V2-ITERATION-06.md +18 -0
- package/docs/V2-ITERATION-07.md +36 -0
- package/docs/V2-PLAN.md +333 -0
- package/docs/V2-PROPOSAL.md +319 -0
- package/docs/V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md +41 -0
- package/docs/v2/M1-ACCEPTANCE-2026-08-28.md +38 -0
- package/docs/v2/M2-DOD-2026-08-28.md +34 -0
- package/docs/v2/M4-CHICKAI-PILOT-2026-08-28.md +44 -0
- package/docs/v2/M4-EXTERNAL-PILOT-2026-08-28.md +46 -0
- package/docs/v2/M4-SELFHOST-2026-08-28.md +53 -0
- package/docs/v2/RFC-0001-product-definition.md +92 -0
- package/docs/v2/RFC-0002-domain-model.md +149 -0
- package/docs/v2/RFC-0003-workflow-policy.md +204 -0
- package/docs/v2/SPEC-0001-events-v1.md +98 -0
- package/docs/v2/guide/01-quickstart.md +92 -0
- package/docs/v2/guide/02-workflow-guide.md +42 -0
- package/docs/v2/guide/03-policy-guide.md +53 -0
- package/docs/v2/guide/04-adapter-guide.md +45 -0
- package/docs/v2/guide/05-worker-contract.md +37 -0
- package/docs/v2/guide/06-evidence-guide.md +38 -0
- package/docs/v2/guide/07-approval-guide.md +34 -0
- package/docs/v2/guide/08-migration-v1.md +68 -0
- package/docs/v2/guide/09-security-boundaries.md +28 -0
- package/docs/v2/guide/10-recovery.md +55 -0
- package/docs/v2/guide/README.md +18 -0
- package/example/.buildbeat/manifest.json +2 -2
- package/example/BUILDBEAT.md +1 -1
- package/package.json +4 -2
- package/src/constants.js +4 -1
- package/src/project.js +6 -1
- package/src/v2/adapters/mock.js +67 -0
- package/src/v2/adapters/shell.js +78 -0
- package/src/v2/cli/run.js +494 -0
- package/src/v2/domain/event-registry.js +100 -0
- package/src/v2/domain/model.js +61 -0
- package/src/v2/engine/reducer.js +253 -0
- package/src/v2/engine/risk-preset.js +48 -0
- package/src/v2/engine/workflow.js +201 -0
- package/src/v2/engine/yaml-subset.js +194 -0
- package/src/v2/evidence/collector.js +63 -0
- package/src/v2/observe/observe-config.js +194 -0
- package/src/v2/observe/observe-reducer.js +117 -0
- package/src/v2/observe/observe.js +420 -0
- package/src/v2/policy/policy.js +302 -0
- package/src/v2/presets/observe.yaml +45 -0
- package/src/v2/presets/policies/ui-render-gate.yaml +13 -0
- package/src/v2/presets/risk/controlled.yaml +39 -0
- package/src/v2/presets/risk/fast.yaml +19 -0
- package/src/v2/presets/risk/legacy-four-gates.yaml +44 -0
- package/src/v2/presets/risk/standard.yaml +28 -0
- package/src/v2/presets/software-delivery.yaml +39 -0
- package/src/v2/runtime/decisions.js +288 -0
- package/src/v2/runtime/metrics.js +140 -0
- package/src/v2/runtime/orchestrator.js +754 -0
- package/src/v2/runtime/run-record.js +55 -0
- package/src/v2/storage/event-ledger.js +154 -0
- package/src/v2/workspace/workspace-manager.js +146 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Policy engine per docs/v2/RFC-0003-workflow-policy.md §4. Policies are
|
|
2
|
+
// declarative YAML documents; rules evaluate under three-valued logic
|
|
3
|
+
// (true / false / "unverified") and UNVERIFIED is never treated as PASS.
|
|
4
|
+
// Parsing is fail-closed: unknown operators, types, or fields are rejected.
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export class PolicyError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "PolicyError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const POLICY_TYPES = ["pre", "post", "transition", "action"];
|
|
18
|
+
const ENFORCEMENTS = ["ADVISORY", "LOCAL_ENFORCED", "SERVER_ENFORCED"];
|
|
19
|
+
const ON_FAIL = ["WAIT_HUMAN", "BLOCK"];
|
|
20
|
+
const POLICY_FIELDS = new Set([
|
|
21
|
+
"kind",
|
|
22
|
+
"version",
|
|
23
|
+
"name",
|
|
24
|
+
"type",
|
|
25
|
+
"appliesTo",
|
|
26
|
+
"enforcement",
|
|
27
|
+
"onFail",
|
|
28
|
+
"rule",
|
|
29
|
+
]);
|
|
30
|
+
const GRADE_RANK = { L0: 0, L1: 1, L2: 2, L3: 3, L4: 4 };
|
|
31
|
+
const SEVERITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
32
|
+
|
|
33
|
+
const OPERATORS = new Set([
|
|
34
|
+
"all",
|
|
35
|
+
"any",
|
|
36
|
+
"not",
|
|
37
|
+
"evidence.exists",
|
|
38
|
+
"artifact.accepted",
|
|
39
|
+
"attempts.lt",
|
|
40
|
+
"budget.remaining",
|
|
41
|
+
"candidate.clean",
|
|
42
|
+
"human.approved",
|
|
43
|
+
"finding.maxSeverity",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function validateRule(rule, path = "rule") {
|
|
47
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
48
|
+
throw new PolicyError(`${path} must be a map with exactly one operator`);
|
|
49
|
+
}
|
|
50
|
+
const keys = Object.keys(rule);
|
|
51
|
+
if (keys.length !== 1) {
|
|
52
|
+
throw new PolicyError(`${path} must have exactly one operator, got: ${keys.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
const op = keys[0];
|
|
55
|
+
if (!OPERATORS.has(op)) {
|
|
56
|
+
throw new PolicyError(`${path}: unknown operator "${op}"`);
|
|
57
|
+
}
|
|
58
|
+
const arg = rule[op];
|
|
59
|
+
if (op === "all" || op === "any") {
|
|
60
|
+
if (!Array.isArray(arg) || arg.length === 0) {
|
|
61
|
+
throw new PolicyError(`${path}.${op} requires a non-empty list of rules`);
|
|
62
|
+
}
|
|
63
|
+
arg.forEach((child, index) => validateRule(child, `${path}.${op}[${index}]`));
|
|
64
|
+
} else if (op === "not") {
|
|
65
|
+
validateRule(arg, `${path}.not`);
|
|
66
|
+
}
|
|
67
|
+
return rule;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parsePolicyDoc(doc) {
|
|
71
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
72
|
+
throw new PolicyError("policy document must be a map");
|
|
73
|
+
}
|
|
74
|
+
for (const key of Object.keys(doc)) {
|
|
75
|
+
if (!POLICY_FIELDS.has(key)) {
|
|
76
|
+
throw new PolicyError(`unknown field "${key}" in policy ${doc.name ?? "(unnamed)"}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (doc.kind !== "policy") {
|
|
80
|
+
throw new PolicyError(`kind must be "policy", got: ${JSON.stringify(doc.kind)}`);
|
|
81
|
+
}
|
|
82
|
+
if (doc.version !== 1) {
|
|
83
|
+
throw new PolicyError(`unsupported policy version: ${JSON.stringify(doc.version)}`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof doc.name !== "string" || doc.name.length === 0) {
|
|
86
|
+
throw new PolicyError("policy needs a name");
|
|
87
|
+
}
|
|
88
|
+
if (!POLICY_TYPES.includes(doc.type)) {
|
|
89
|
+
throw new PolicyError(`policy ${doc.name}: type must be one of ${POLICY_TYPES.join("|")}`);
|
|
90
|
+
}
|
|
91
|
+
if (typeof doc.appliesTo !== "string" || doc.appliesTo.length === 0) {
|
|
92
|
+
throw new PolicyError(`policy ${doc.name}: appliesTo is required`);
|
|
93
|
+
}
|
|
94
|
+
if (!ENFORCEMENTS.includes(doc.enforcement)) {
|
|
95
|
+
throw new PolicyError(
|
|
96
|
+
`policy ${doc.name}: enforcement must be one of ${ENFORCEMENTS.join("|")}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (doc.onFail !== undefined && !ON_FAIL.includes(doc.onFail)) {
|
|
100
|
+
throw new PolicyError(`policy ${doc.name}: onFail must be one of ${ON_FAIL.join("|")}`);
|
|
101
|
+
}
|
|
102
|
+
validateRule(doc.rule, `policy ${doc.name} rule`);
|
|
103
|
+
return {
|
|
104
|
+
name: doc.name,
|
|
105
|
+
type: doc.type,
|
|
106
|
+
appliesTo: doc.appliesTo,
|
|
107
|
+
enforcement: doc.enforcement,
|
|
108
|
+
onFail: doc.onFail ?? "WAIT_HUMAN",
|
|
109
|
+
rule: doc.rule,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function combine(values, mode) {
|
|
114
|
+
// Three-valued logic: false dominates all(), true dominates any(),
|
|
115
|
+
// otherwise any "unverified" makes the result unverified.
|
|
116
|
+
if (mode === "all") {
|
|
117
|
+
if (values.some((value) => value.ok === false)) {
|
|
118
|
+
return values.find((value) => value.ok === false);
|
|
119
|
+
}
|
|
120
|
+
if (values.some((value) => value.ok === "unverified")) {
|
|
121
|
+
return values.find((value) => value.ok === "unverified");
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, why: "all conditions hold" };
|
|
124
|
+
}
|
|
125
|
+
if (values.some((value) => value.ok === true)) {
|
|
126
|
+
return { ok: true, why: "a condition holds" };
|
|
127
|
+
}
|
|
128
|
+
if (values.some((value) => value.ok === "unverified")) {
|
|
129
|
+
return values.find((value) => value.ok === "unverified");
|
|
130
|
+
}
|
|
131
|
+
return { ok: false, why: values.map((value) => value.why).join("; ") };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function sha256Text(text) {
|
|
135
|
+
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ctx: { state, workDir, worktreePath, readWorktree() -> {head, dirty} | null }
|
|
139
|
+
export function evaluateRule(rule, ctx) {
|
|
140
|
+
const op = Object.keys(rule)[0];
|
|
141
|
+
const arg = rule[op];
|
|
142
|
+
switch (op) {
|
|
143
|
+
case "all":
|
|
144
|
+
return combine(arg.map((child) => evaluateRule(child, ctx)), "all");
|
|
145
|
+
case "any":
|
|
146
|
+
return combine(arg.map((child) => evaluateRule(child, ctx)), "any");
|
|
147
|
+
case "not": {
|
|
148
|
+
const inner = evaluateRule(arg, ctx);
|
|
149
|
+
if (inner.ok === "unverified") {
|
|
150
|
+
return inner;
|
|
151
|
+
}
|
|
152
|
+
return { ok: !inner.ok, why: `not(${inner.why})` };
|
|
153
|
+
}
|
|
154
|
+
case "evidence.exists": {
|
|
155
|
+
const minRank = arg.minGrade !== undefined ? GRADE_RANK[arg.minGrade] : 0;
|
|
156
|
+
if (minRank === undefined) {
|
|
157
|
+
return { ok: "unverified", why: `unknown grade ${arg.minGrade}` };
|
|
158
|
+
}
|
|
159
|
+
// When the context names a candidate (e.g. a merge approval), only
|
|
160
|
+
// evidence about that candidate counts — facts about superseded
|
|
161
|
+
// candidates must not satisfy a gate for the current one.
|
|
162
|
+
const scoped = ctx.candidate
|
|
163
|
+
? ctx.state.evidence.filter((item) => item.subject === ctx.candidate)
|
|
164
|
+
: ctx.state.evidence;
|
|
165
|
+
const hit = scoped.some(
|
|
166
|
+
(item) =>
|
|
167
|
+
item.kind === arg.kind &&
|
|
168
|
+
item.status === "passed" &&
|
|
169
|
+
(GRADE_RANK[item.grade] ?? -1) >= minRank,
|
|
170
|
+
);
|
|
171
|
+
return hit
|
|
172
|
+
? { ok: true, why: `evidence ${arg.kind} present` }
|
|
173
|
+
: {
|
|
174
|
+
ok: false,
|
|
175
|
+
why: `no passed evidence of kind ${arg.kind}${arg.minGrade ? ` at grade >= ${arg.minGrade}` : ""}${ctx.candidate ? " for the current candidate" : ""}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
case "artifact.accepted": {
|
|
179
|
+
const artifact = arg.artifact;
|
|
180
|
+
const filePath = join(ctx.workDir, `${artifact}.md`);
|
|
181
|
+
if (!existsSync(filePath)) {
|
|
182
|
+
return { ok: "unverified", why: `artifact file missing: ${artifact}.md` };
|
|
183
|
+
}
|
|
184
|
+
const decisionsPath = join(ctx.workDir, "decisions.jsonl");
|
|
185
|
+
if (!existsSync(decisionsPath)) {
|
|
186
|
+
return { ok: false, why: `artifact ${artifact} has never been accepted` };
|
|
187
|
+
}
|
|
188
|
+
const accepts = readFileSync(decisionsPath, "utf8")
|
|
189
|
+
.split("\n")
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.map((line) => JSON.parse(line))
|
|
192
|
+
.filter(
|
|
193
|
+
(line) => line.transition === `accept-${artifact}` && line.decision === "approved",
|
|
194
|
+
);
|
|
195
|
+
if (accepts.length === 0) {
|
|
196
|
+
return { ok: false, why: `artifact ${artifact} has never been accepted` };
|
|
197
|
+
}
|
|
198
|
+
const latest = accepts[accepts.length - 1];
|
|
199
|
+
const currentDigest = sha256Text(readFileSync(filePath, "utf8"));
|
|
200
|
+
if (latest.subject?.digest !== currentDigest) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
why: `artifact ${artifact} changed after its acceptance (stale acceptance)`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, why: `artifact ${artifact} accepted at current digest` };
|
|
207
|
+
}
|
|
208
|
+
case "attempts.lt": {
|
|
209
|
+
const attempts = ctx.state.steps[arg.step]?.attempts ?? 0;
|
|
210
|
+
return attempts < arg.max
|
|
211
|
+
? { ok: true, why: `${arg.step} attempts ${attempts} < ${arg.max}` }
|
|
212
|
+
: { ok: false, why: `${arg.step} attempts ${attempts} reached ${arg.max}` };
|
|
213
|
+
}
|
|
214
|
+
case "budget.remaining": {
|
|
215
|
+
const budget = ctx.state.budgets[arg.kind];
|
|
216
|
+
if (!budget) {
|
|
217
|
+
return { ok: true, why: `budget ${arg.kind} untouched` };
|
|
218
|
+
}
|
|
219
|
+
return budget.remaining > 0
|
|
220
|
+
? { ok: true, why: `budget ${arg.kind} remaining ${budget.remaining}` }
|
|
221
|
+
: { ok: false, why: `budget ${arg.kind} exhausted` };
|
|
222
|
+
}
|
|
223
|
+
case "candidate.clean": {
|
|
224
|
+
const tree = ctx.readWorktree();
|
|
225
|
+
if (!tree) {
|
|
226
|
+
return { ok: "unverified", why: "worktree not available for readback" };
|
|
227
|
+
}
|
|
228
|
+
return tree.dirty
|
|
229
|
+
? { ok: false, why: "worktree is dirty" }
|
|
230
|
+
: { ok: true, why: "worktree is clean" };
|
|
231
|
+
}
|
|
232
|
+
case "human.approved": {
|
|
233
|
+
const hit = ctx.state.approvals.some(
|
|
234
|
+
(approval) => !approval.stale && approval.transition === arg.transition,
|
|
235
|
+
);
|
|
236
|
+
return hit
|
|
237
|
+
? { ok: true, why: `transition ${arg.transition} approved` }
|
|
238
|
+
: { ok: false, why: `no active approval for transition ${arg.transition}` };
|
|
239
|
+
}
|
|
240
|
+
case "finding.maxSeverity": {
|
|
241
|
+
const atMostRank = SEVERITY_RANK[arg.atMost];
|
|
242
|
+
if (atMostRank === undefined) {
|
|
243
|
+
return { ok: "unverified", why: `unknown severity ${arg.atMost}` };
|
|
244
|
+
}
|
|
245
|
+
// Findings against superseded candidates are history, not verdicts:
|
|
246
|
+
// a blocked round-1 review that the fix loop already addressed must
|
|
247
|
+
// not poison the gate for the final candidate. When the context
|
|
248
|
+
// names a candidate, judge only reviews of that candidate.
|
|
249
|
+
const reviews = ctx.state.evidence.filter(
|
|
250
|
+
(item) =>
|
|
251
|
+
item.kind === "review" && (!ctx.candidate || item.subject === ctx.candidate),
|
|
252
|
+
);
|
|
253
|
+
if (reviews.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
ok: "unverified",
|
|
256
|
+
why: ctx.candidate
|
|
257
|
+
? "no review evidence for the current candidate"
|
|
258
|
+
: "no review evidence to judge findings",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const findings = reviews.flatMap((item) => item.findings ?? []);
|
|
262
|
+
const tooSevere = findings.filter(
|
|
263
|
+
(finding) => (SEVERITY_RANK[finding.severity] ?? 0) < atMostRank,
|
|
264
|
+
);
|
|
265
|
+
return tooSevere.length === 0
|
|
266
|
+
? { ok: true, why: `no findings above ${arg.atMost}` }
|
|
267
|
+
: {
|
|
268
|
+
ok: false,
|
|
269
|
+
why: `${tooSevere.length} finding(s) more severe than ${arg.atMost}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
default:
|
|
273
|
+
return { ok: "unverified", why: `unknown operator ${op}` };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Evaluates every policy of `type` whose appliesTo matches; returns rows of
|
|
278
|
+
// { policy, result, reason, enforcement } where result is a GateResult.
|
|
279
|
+
export function evaluatePolicies(policies, { type, appliesTo }, ctx) {
|
|
280
|
+
const rows = [];
|
|
281
|
+
for (const policy of policies ?? []) {
|
|
282
|
+
if (policy.type !== type || policy.appliesTo !== appliesTo) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const verdict = evaluateRule(policy.rule, ctx);
|
|
286
|
+
let result;
|
|
287
|
+
if (verdict.ok === true) {
|
|
288
|
+
result = "PASS";
|
|
289
|
+
} else if (verdict.ok === "unverified") {
|
|
290
|
+
result = "UNVERIFIED";
|
|
291
|
+
} else {
|
|
292
|
+
result = policy.onFail;
|
|
293
|
+
}
|
|
294
|
+
rows.push({
|
|
295
|
+
policy: policy.name,
|
|
296
|
+
result,
|
|
297
|
+
reason: verdict.why,
|
|
298
|
+
enforcement: policy.enforcement,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return rows;
|
|
302
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# observe workflow preset per docs/v2/RFC-0003-workflow-policy.md §8 (frozen
|
|
2
|
+
# shape; block form — the strict YAML subset has no inline {}/[]).
|
|
3
|
+
# One `observe run` invocation = one cycle; scheduling is host-level in v0
|
|
4
|
+
# (cron/CI invoking the CLI). Copy to <repo>/.buildbeat/observe.yaml and
|
|
5
|
+
# point commands at the project's real probes.
|
|
6
|
+
kind: workflow
|
|
7
|
+
version: 1
|
|
8
|
+
name: observe
|
|
9
|
+
providers:
|
|
10
|
+
- id: drift-check
|
|
11
|
+
command: bash
|
|
12
|
+
args:
|
|
13
|
+
- scripts/drift-check.sh
|
|
14
|
+
schedule: manual
|
|
15
|
+
evidence:
|
|
16
|
+
kind: drift
|
|
17
|
+
subject: deploy-unit
|
|
18
|
+
severity:
|
|
19
|
+
failed: error
|
|
20
|
+
unverified: warn
|
|
21
|
+
diagnose:
|
|
22
|
+
command: bash
|
|
23
|
+
args:
|
|
24
|
+
- scripts/live-status.sh
|
|
25
|
+
- id: live-status
|
|
26
|
+
command: bash
|
|
27
|
+
args:
|
|
28
|
+
- scripts/live-status.sh
|
|
29
|
+
schedule: manual
|
|
30
|
+
evidence:
|
|
31
|
+
kind: runtime-health
|
|
32
|
+
subject: deploy-unit
|
|
33
|
+
bands:
|
|
34
|
+
- level: log
|
|
35
|
+
when: "severity >= info"
|
|
36
|
+
- level: diagnose
|
|
37
|
+
when: "severity >= error"
|
|
38
|
+
- level: intent
|
|
39
|
+
when: "severity >= error"
|
|
40
|
+
triage:
|
|
41
|
+
actions:
|
|
42
|
+
- fix_now
|
|
43
|
+
- schedule
|
|
44
|
+
- dismiss
|
|
45
|
+
dismissFeedback: bands
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Invariant 22 (v1 lessons #3, machine form): a UI delivery's spec approval
|
|
2
|
+
# subject must include renderable proof — a screenshot with a digest —
|
|
3
|
+
# before a human may stamp it. Static prose is not an approvable design.
|
|
4
|
+
kind: policy
|
|
5
|
+
version: 1
|
|
6
|
+
name: ui-render-gate
|
|
7
|
+
type: transition
|
|
8
|
+
appliesTo: enter-spec
|
|
9
|
+
enforcement: LOCAL_ENFORCED
|
|
10
|
+
rule:
|
|
11
|
+
evidence.exists:
|
|
12
|
+
kind: screenshot
|
|
13
|
+
minGrade: L2
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Controlled lane: intent AND plan must be accepted artifacts before build;
|
|
2
|
+
# merge stays human behind a stricter evidence floor. The release gate from
|
|
3
|
+
# the controlled track is out of MVP scope (no deploy capability exists) and
|
|
4
|
+
# returns with observe/release work in M5.
|
|
5
|
+
kind: risk-preset
|
|
6
|
+
version: 1
|
|
7
|
+
name: controlled
|
|
8
|
+
policies:
|
|
9
|
+
- kind: policy
|
|
10
|
+
version: 1
|
|
11
|
+
name: intent-accepted
|
|
12
|
+
type: pre
|
|
13
|
+
appliesTo: build
|
|
14
|
+
enforcement: LOCAL_ENFORCED
|
|
15
|
+
rule:
|
|
16
|
+
artifact.accepted:
|
|
17
|
+
artifact: intent
|
|
18
|
+
- kind: policy
|
|
19
|
+
version: 1
|
|
20
|
+
name: plan-accepted
|
|
21
|
+
type: pre
|
|
22
|
+
appliesTo: build
|
|
23
|
+
enforcement: LOCAL_ENFORCED
|
|
24
|
+
rule:
|
|
25
|
+
artifact.accepted:
|
|
26
|
+
artifact: plan
|
|
27
|
+
- kind: policy
|
|
28
|
+
version: 1
|
|
29
|
+
name: merge-evidence-floor
|
|
30
|
+
type: transition
|
|
31
|
+
appliesTo: enter-wait-merge
|
|
32
|
+
enforcement: LOCAL_ENFORCED
|
|
33
|
+
rule:
|
|
34
|
+
all:
|
|
35
|
+
- evidence.exists:
|
|
36
|
+
kind: command
|
|
37
|
+
minGrade: L2
|
|
38
|
+
- finding.maxSeverity:
|
|
39
|
+
atMost: P3
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Fast lane: automatic plan/build/verify/review, human merge decision only.
|
|
2
|
+
# The merge gate still demands read-back evidence and a clean review.
|
|
3
|
+
kind: risk-preset
|
|
4
|
+
version: 1
|
|
5
|
+
name: fast
|
|
6
|
+
policies:
|
|
7
|
+
- kind: policy
|
|
8
|
+
version: 1
|
|
9
|
+
name: merge-evidence-floor
|
|
10
|
+
type: transition
|
|
11
|
+
appliesTo: enter-wait-merge
|
|
12
|
+
enforcement: LOCAL_ENFORCED
|
|
13
|
+
rule:
|
|
14
|
+
all:
|
|
15
|
+
- evidence.exists:
|
|
16
|
+
kind: command
|
|
17
|
+
minGrade: L2
|
|
18
|
+
- finding.maxSeverity:
|
|
19
|
+
atMost: P2
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Migration preset mapping BuildBeat v1's four fixed gates:
|
|
2
|
+
# Gate1 (requirements) -> intent acceptance (digest-bound)
|
|
3
|
+
# Gate2 (design) -> plan acceptance + human boundary before build
|
|
4
|
+
# Gate3 (merge) -> the built-in final merge decision
|
|
5
|
+
# Gate4 (release) -> out of MVP scope (no deploy capability exists)
|
|
6
|
+
# The extra human boundary before review preserves v1's late review gate.
|
|
7
|
+
kind: risk-preset
|
|
8
|
+
version: 1
|
|
9
|
+
name: legacy-four-gates
|
|
10
|
+
stopAt:
|
|
11
|
+
- build
|
|
12
|
+
- review
|
|
13
|
+
policies:
|
|
14
|
+
- kind: policy
|
|
15
|
+
version: 1
|
|
16
|
+
name: intent-accepted
|
|
17
|
+
type: pre
|
|
18
|
+
appliesTo: build
|
|
19
|
+
enforcement: LOCAL_ENFORCED
|
|
20
|
+
rule:
|
|
21
|
+
artifact.accepted:
|
|
22
|
+
artifact: intent
|
|
23
|
+
- kind: policy
|
|
24
|
+
version: 1
|
|
25
|
+
name: plan-accepted
|
|
26
|
+
type: pre
|
|
27
|
+
appliesTo: build
|
|
28
|
+
enforcement: LOCAL_ENFORCED
|
|
29
|
+
rule:
|
|
30
|
+
artifact.accepted:
|
|
31
|
+
artifact: plan
|
|
32
|
+
- kind: policy
|
|
33
|
+
version: 1
|
|
34
|
+
name: merge-evidence-floor
|
|
35
|
+
type: transition
|
|
36
|
+
appliesTo: enter-wait-merge
|
|
37
|
+
enforcement: LOCAL_ENFORCED
|
|
38
|
+
rule:
|
|
39
|
+
all:
|
|
40
|
+
- evidence.exists:
|
|
41
|
+
kind: command
|
|
42
|
+
minGrade: L2
|
|
43
|
+
- finding.maxSeverity:
|
|
44
|
+
atMost: P2
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Standard lane: the plan must be an accepted artifact (digest-bound) before
|
|
2
|
+
# build starts; merge stays a human decision behind the evidence floor.
|
|
3
|
+
kind: risk-preset
|
|
4
|
+
version: 1
|
|
5
|
+
name: standard
|
|
6
|
+
policies:
|
|
7
|
+
- kind: policy
|
|
8
|
+
version: 1
|
|
9
|
+
name: plan-accepted
|
|
10
|
+
type: pre
|
|
11
|
+
appliesTo: build
|
|
12
|
+
enforcement: LOCAL_ENFORCED
|
|
13
|
+
rule:
|
|
14
|
+
artifact.accepted:
|
|
15
|
+
artifact: plan
|
|
16
|
+
- kind: policy
|
|
17
|
+
version: 1
|
|
18
|
+
name: merge-evidence-floor
|
|
19
|
+
type: transition
|
|
20
|
+
appliesTo: enter-wait-merge
|
|
21
|
+
enforcement: LOCAL_ENFORCED
|
|
22
|
+
rule:
|
|
23
|
+
all:
|
|
24
|
+
- evidence.exists:
|
|
25
|
+
kind: command
|
|
26
|
+
minGrade: L2
|
|
27
|
+
- finding.maxSeverity:
|
|
28
|
+
atMost: P2
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Official software-delivery preset per docs/v2/RFC-0003-workflow-policy.md §2.
|
|
2
|
+
# Step order defines the happy path (each step chains to the next on
|
|
3
|
+
# "succeeded"); fix sits off-path at the end and is reached only via the
|
|
4
|
+
# explicit failure/findings transitions.
|
|
5
|
+
kind: workflow
|
|
6
|
+
version: 1
|
|
7
|
+
name: software-delivery
|
|
8
|
+
entry: intent
|
|
9
|
+
steps:
|
|
10
|
+
- id: intent
|
|
11
|
+
worker: planner
|
|
12
|
+
- id: spec
|
|
13
|
+
worker: planner
|
|
14
|
+
optional: true
|
|
15
|
+
requiredWhen: ui-delivery
|
|
16
|
+
- id: plan
|
|
17
|
+
worker: planner
|
|
18
|
+
- id: build
|
|
19
|
+
worker: builder
|
|
20
|
+
- id: verify
|
|
21
|
+
worker: verifier
|
|
22
|
+
- id: review
|
|
23
|
+
worker: reviewer
|
|
24
|
+
readonly: true
|
|
25
|
+
- id: wait-merge
|
|
26
|
+
- id: fix
|
|
27
|
+
worker: fixer
|
|
28
|
+
transitions:
|
|
29
|
+
- from: verify
|
|
30
|
+
on: failed
|
|
31
|
+
to: fix
|
|
32
|
+
- from: fix
|
|
33
|
+
on: succeeded
|
|
34
|
+
to: verify
|
|
35
|
+
- from: review
|
|
36
|
+
on: findings-blocking
|
|
37
|
+
to: fix
|
|
38
|
+
terminal:
|
|
39
|
+
- wait-merge
|