@afokapu/atdd-bun 0.10.15 → 0.10.17
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.
|
@@ -4,22 +4,27 @@ kind: rule
|
|
|
4
4
|
status: active
|
|
5
5
|
name: Authors and reviewers are allowed models, and every fallback says why
|
|
6
6
|
statement: >-
|
|
7
|
-
Each work entry's writer appears in that stage's writer list and each review's reviewer in its reviewer list; a model after the first is used only with a recorded fallback from each model before it, stating the kind of unavailability (outage, rate_limit, no_report, timeout), the failures observed (at least delivery.fallback.after_failures) within a window no longer than delivery.fallback.within_minutes, and the reason (REQUIRED).
|
|
7
|
+
Each work entry's writer appears in that stage's writer list and each review's reviewer in its reviewer list; a model after the first is used only with a recorded fallback from each model before it, stating the kind of unavailability (outage, rate_limit, no_report, timeout), the failures observed (at least delivery.fallback.after_failures) within a window no longer than delivery.fallback.within_minutes, and the reason; or, where the operator ordered the substitution, kind operator with who decided, when, the ruling it is recorded in, and the reason (REQUIRED).
|
|
8
8
|
terms:
|
|
9
9
|
- term_id: fallback
|
|
10
10
|
text: >-
|
|
11
|
-
a recorded switch from a preferred model to
|
|
11
|
+
a recorded switch from a preferred model to a later one in the stage's list, with role (writer or reviewer) and a reason: an outage, a rate limit, or no auditable report, counted by failures; or the operator's order, referenced by its ruling. A REQUEST CHANGES verdict is never a reason.
|
|
12
12
|
content:
|
|
13
13
|
summary: >-
|
|
14
14
|
The lists are the operator's choice of who may do the work. A silent switch to another model hides an outage and can hide a reviewer chosen for being lenient.
|
|
15
15
|
normative_text: |
|
|
16
16
|
The first model in a list is used normally. When it is unavailable, the driver falls back to the next one and records it. A fallback is never triggered by a verdict: shopping for a reviewer until one approves is exactly what this rule exists to expose.
|
|
17
|
+
An operator can order a substitution faster than a policy change lands, as a standing ruling while a model is rate-limited. The record then states that ruling rather than failures nobody counted. The substitute must still be a model the stage lists in the policy the record is judged by, so the policy file keeps the final word; and only the operator rules: a driver never writes an operator fallback on its own authority.
|
|
17
18
|
fix_hint: |
|
|
18
19
|
Record the fallback on the review:
|
|
19
20
|
|
|
20
21
|
fallback:
|
|
21
22
|
- { role: reviewer, from: codex, kind: rate_limit, failures: 3, window: { from: "2026-09-25T09:00:00Z", to: "2026-09-25T09:08:00Z" }, reason: "429 on 3 attempts in 10 minutes" }
|
|
22
23
|
|
|
24
|
+
or, for a substitution the operator ordered:
|
|
25
|
+
|
|
26
|
+
- { role: writer, from: claude-opus, kind: operator, decided_by: "the owner", decided_at: "2026-09-27T23:00:00Z", ruling: "atdd-frg-workstation #abc123", reason: "Standing substitution while Claude is rate-limited: kimi for Opus-tier work" }
|
|
27
|
+
|
|
23
28
|
or use a model the stage lists.
|
|
24
29
|
exceptions:
|
|
25
30
|
- >-
|
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.17",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"conventions/delivery/delivery.evidence-schema.convention.yaml": "9c6a22495dfb3e2fcbca700392866c3b5660084b1f5594230eb320b499d5e5cb",
|
|
99
99
|
"conventions/delivery/delivery.findings-resolved.convention.yaml": "f6574e93a9a2d4bdd6d6f5645720b1a647fc0048d95f7675fe783d2ddb62c05b",
|
|
100
100
|
"conventions/delivery/delivery.merge-gate.convention.yaml": "48745ec23625b68be9b8fe2c6dcdcba88b2b38924f6593abf6987391ff0ee243",
|
|
101
|
-
"conventions/delivery/delivery.model-allowed.convention.yaml": "
|
|
101
|
+
"conventions/delivery/delivery.model-allowed.convention.yaml": "7bf69b1803ec388a77b2fbc8aa727ff4ac6fd0cf4e5d5736519117bf86ac8688",
|
|
102
102
|
"conventions/delivery/delivery.operating-model.convention.yaml": "7982157d957938ade878a114d3f5c2bdaa8f02e52ac21614634d9af4be9692f1",
|
|
103
103
|
"conventions/delivery/delivery.review.convention.yaml": "b033579c2d90fcb098d325dcc07c7b084e048cca43fbe8e28159ec08a412018a",
|
|
104
104
|
"conventions/delivery/delivery.reviewer-independent.convention.yaml": "8c436d66ef5b1f6d6bc5dd55a235caca2490020e88665eecadb2ca41cd4149c5",
|
|
@@ -1178,7 +1178,7 @@
|
|
|
1178
1178
|
"planner-schemas/component.schema.json": "d20d3502c155627a120cb10d5e9bdec4ba2c73af9c3e9a9d10e3c351f167c72a",
|
|
1179
1179
|
"planner-schemas/contract-registry.schema.json": "b9dbaf6c87d4f4d676e3b4251344b9eada523b1c5e739c0423cabe267012c12e",
|
|
1180
1180
|
"planner-schemas/delivery-config.schema.json": "16a9b66a7d36edf8a49c68d9786f2516cd2ff5392be6dd60b766a09ffc95a9c6",
|
|
1181
|
-
"planner-schemas/delivery-evidence.schema.json": "
|
|
1181
|
+
"planner-schemas/delivery-evidence.schema.json": "72ba1450abe5482c43f2069e9ee7d121398008a0ddef36c6b269d38a16872e0e",
|
|
1182
1182
|
"planner-schemas/feature.schema.json": "eb4aa40d89067ae5c86a079bb3d8e4f45c8727f9e15db2b86c08dacf8e4ae56a",
|
|
1183
1183
|
"planner-schemas/journey.schema.json": "1ff705bcf89a2823cf60c9e1597e254f97844e7a74dc58299d53192ad2c9f8c3",
|
|
1184
1184
|
"planner-schemas/registry-index.schema.json": "ffd5643da662ced4052707eeb270eb02908f2e48365103ff0638ec7ba146f430",
|
|
@@ -1197,7 +1197,7 @@
|
|
|
1197
1197
|
"src/board.ts": "a3a8efe923858b9b5cf5ca3682dacfceb5557f8303d818687eb1325a15fa1782",
|
|
1198
1198
|
"src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
|
|
1199
1199
|
"src/cli.ts": "e45f2666334503a91eb65b1b5ee4dd8acab6b99f62d333e2ee2b65963db6358b",
|
|
1200
|
-
"src/delivery.ts": "
|
|
1200
|
+
"src/delivery.ts": "f6bb6a7c42693d795b964c77b6186e1ca3a1556fd43932911a0ad42e72e7e441",
|
|
1201
1201
|
"src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
|
|
1202
1202
|
"src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
|
|
1203
1203
|
"src/hooks.ts": "2feeec2674c1462c94256de02ce59ccc6e3ff671ec4dca1bf9903c32888b600b",
|
package/package.json
CHANGED
|
@@ -30,12 +30,16 @@
|
|
|
30
30
|
},
|
|
31
31
|
"fallback": {
|
|
32
32
|
"type": "object",
|
|
33
|
-
"required": ["from", "kind", "
|
|
33
|
+
"required": ["from", "kind", "reason"],
|
|
34
34
|
"additionalProperties": false,
|
|
35
|
+
"description": "Why a later model in the stage's list was used: counted failures of the preferred one, or the operator's order (kind operator), which references its ruling instead and is accepted only for a model the stage lists.",
|
|
36
|
+
"if": { "required": ["kind"], "properties": { "kind": { "const": "operator" } } },
|
|
37
|
+
"then": { "required": ["decided_by", "decided_at", "ruling"], "not": { "anyOf": [{ "required": ["failures"] }, { "required": ["window"] }] } },
|
|
38
|
+
"else": { "required": ["failures", "window"] },
|
|
35
39
|
"properties": {
|
|
36
40
|
"role": { "enum": ["writer", "author", "reviewer"], "description": "Which actor fell back; author is the 0.9 spelling of writer. Default: writer on a work entry, reviewer on a review." },
|
|
37
41
|
"from": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
38
|
-
"kind": { "enum": ["outage", "rate_limit", "no_report", "timeout"], "description": "Why the model counts as unavailable. A verdict is never one of these." },
|
|
42
|
+
"kind": { "enum": ["outage", "rate_limit", "no_report", "timeout", "operator"], "description": "Why the model counts as unavailable, or operator when the operator ordered the substitution. A verdict is never one of these." },
|
|
39
43
|
"failures": { "type": "integer", "minimum": 1, "description": "Failures observed before falling back; at least delivery.fallback.after_failures." },
|
|
40
44
|
"window": {
|
|
41
45
|
"type": "object",
|
|
@@ -44,7 +48,10 @@
|
|
|
44
48
|
"description": "When the first and last counted failures happened; the span is at most delivery.fallback.within_minutes.",
|
|
45
49
|
"properties": { "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time" } }
|
|
46
50
|
},
|
|
47
|
-
"
|
|
51
|
+
"decided_by": { "type": "string", "minLength": 2, "description": "operator only: who ruled, by name or board identity. Never the driver on its own authority." },
|
|
52
|
+
"decided_at": { "type": "string", "format": "date-time", "description": "operator only: when the ruling was given." },
|
|
53
|
+
"ruling": { "type": "string", "minLength": 3, "description": "operator only: where the ruling is recorded (a board message id, an ADR or a PR)." },
|
|
54
|
+
"reason": { "type": "string", "minLength": 10, "description": "Why the preferred model was not used: an outage, a rate limit, no auditable report, or the operator's order. Never a verdict." }
|
|
48
55
|
}
|
|
49
56
|
},
|
|
50
57
|
"finding": {
|
package/src/delivery.ts
CHANGED
|
@@ -177,7 +177,8 @@ const git = async (cwd: string, args: string[]) => {
|
|
|
177
177
|
};
|
|
178
178
|
|
|
179
179
|
type Actor = { model: string; run: string };
|
|
180
|
-
|
|
180
|
+
// A failure fallback counts observed failures; an operator fallback references the operator's ruling instead.
|
|
181
|
+
type Fallback = { role?: "author" | "writer" | "reviewer"; from: string; kind: string; failures?: number; window?: { from: string; to: string }; decided_by?: string; decided_at?: string; ruling?: string; reason: string };
|
|
181
182
|
type Work = { stage: Stage; sha: string; writer: Actor; fallback?: Fallback[] };
|
|
182
183
|
type Review = { stage: Stage; sha: string; author?: Actor; reviewer: Actor; fallback?: Fallback[]; verdict: "approve" | "request_changes"; findings?: Finding[]; report?: string };
|
|
183
184
|
type Finding = { id: string; severity: string; rebuttal?: string; outcome?: "fixed" | "withdrawn" | "human"; decision?: string };
|
|
@@ -234,14 +235,16 @@ export async function loadEvidence(root: string, policy: DeliveryPolicy): Promis
|
|
|
234
235
|
return out;
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
/** A model must come from the stage's list; a model after the first needs a recorded fallback from each one before it
|
|
238
|
-
|
|
238
|
+
/** A model must come from the stage's list; a model after the first needs a recorded fallback from each one before it,
|
|
239
|
+
* except one the stage could not use: under different-model independence a reviewer skips every model that wrote the
|
|
240
|
+
* work it reviews (delivery.reviewer-independent's fix hint). Ineligible is not unavailable, so no fallback records it. */
|
|
241
|
+
function checkModel(file: string, at: string, stage: Stage, role: "writer" | "reviewer", actor: Actor, list: string[], fallbacks: Fallback[], defaultRole: "writer" | "reviewer", fallback: DeliveryPolicy["fallback"], ineligible: string[] = []): PlanFinding[] {
|
|
239
242
|
const out: PlanFinding[] = [];
|
|
240
243
|
const index = list.indexOf(actor.model);
|
|
241
244
|
if (index < 0) return [finding("delivery.model-allowed", file, `${at}: ${role} model '${actor.model}' is not in ${stage}.${role} [${list.join(", ")}]`)];
|
|
242
245
|
// `author` is the 0.9 spelling of `writer`.
|
|
243
246
|
const recorded = fallbacks.filter(entry => ((entry.role === "author" ? "writer" : entry.role) ?? defaultRole) === role).map(entry => entry.from);
|
|
244
|
-
for (const skipped of list.slice(0, index)) if (!recorded.includes(skipped)) out.push(finding("delivery.model-allowed", file, `${at}: ${role} '${actor.model}' is a fallback, but no fallback from '${skipped}' records why it was unavailable`));
|
|
247
|
+
for (const skipped of list.slice(0, index)) if (!recorded.includes(skipped) && !ineligible.includes(skipped)) out.push(finding("delivery.model-allowed", file, `${at}: ${role} '${actor.model}' is a fallback, but no fallback from '${skipped}' records why it was unavailable`));
|
|
245
248
|
for (const from of recorded) if (!list.slice(0, index).includes(from)) out.push(finding("delivery.model-allowed", file, `${at}: ${role} fallback from '${from}' does not precede '${actor.model}' in [${list.join(", ")}]`));
|
|
246
249
|
return out;
|
|
247
250
|
}
|
|
@@ -250,6 +253,8 @@ function checkModel(file: string, at: string, stage: Stage, role: "writer" | "re
|
|
|
250
253
|
function checkFallbacks(file: string, at: string, fallbacks: Fallback[], fallback: DeliveryPolicy["fallback"]): PlanFinding[] {
|
|
251
254
|
const out: PlanFinding[] = [];
|
|
252
255
|
for (const entry of fallbacks) {
|
|
256
|
+
// The operator ordered the substitution: nothing was counted, and the schema has required the ruling it names.
|
|
257
|
+
if (entry.kind === "operator" || !entry.window || entry.failures === undefined) continue;
|
|
253
258
|
if (entry.failures < fallback.after_failures) out.push(finding("delivery.model-allowed", file, `${at}: fallback from '${entry.from}' after ${entry.failures} failure(s); the policy requires ${fallback.after_failures} (delivery.fallback.after_failures)`));
|
|
254
259
|
const span = (Date.parse(entry.window.to) - Date.parse(entry.window.from)) / 60_000;
|
|
255
260
|
if (!(span >= 0)) out.push(finding("delivery.model-allowed", file, `${at}: fallback from '${entry.from}' has a window that ends before it starts`));
|
|
@@ -305,10 +310,11 @@ function checkEvidence(file: string, evidence: Evidence, policy: DeliveryPolicy)
|
|
|
305
310
|
const at = `reviews[${index}] (${review.stage} @ ${review.sha})`, stage = policy.stages[review.stage];
|
|
306
311
|
if (!stage?.reviewer.length) { out.push(finding("delivery.evidence-schema", file, `${at}: ${review.stage} has no reviewer in delivery.stages`)); return; }
|
|
307
312
|
const fallbacks = review.fallback ?? [];
|
|
308
|
-
out.push(...checkFallbacks(file, at, fallbacks, policy.fallback), ...checkModel(file, at, review.stage, "reviewer", review.reviewer, stage.reviewer, fallbacks, "reviewer", policy.fallback));
|
|
309
|
-
if (review.author && stage.writer.length) out.push(...checkModel(file, at, review.stage, "writer", review.author, stage.writer, fallbacks, "reviewer", policy.fallback));
|
|
310
313
|
// Independence is judged against everyone who wrote what this review answers for.
|
|
311
314
|
const covered = coveredStages(review.stage, policy, work), writers = [...work.filter(entry => covered.includes(entry.stage)).map(entry => entry.writer), ...(review.author ? [review.author] : [])];
|
|
315
|
+
const ineligible = stage.independence === "different-model" ? writers.map(writer => writer.model) : [];
|
|
316
|
+
out.push(...checkFallbacks(file, at, fallbacks, policy.fallback), ...checkModel(file, at, review.stage, "reviewer", review.reviewer, stage.reviewer, fallbacks, "reviewer", policy.fallback, ineligible));
|
|
317
|
+
if (review.author && stage.writer.length) out.push(...checkModel(file, at, review.stage, "writer", review.author, stage.writer, fallbacks, "reviewer", policy.fallback));
|
|
312
318
|
if (!writers.length) out.push(finding("delivery.evidence-schema", file, `${at}: names no writer of the work it reviews (${covered.join(", ")}); record each stage's work, or the review's author, so independence can be judged`));
|
|
313
319
|
if (writerRuns.has(review.reviewer.run)) out.push(finding("delivery.reviewer-independent", file, `${at}: reviewer run '${review.reviewer.run}' also wrote in this tranche; a reviewer that edits becomes a writer`));
|
|
314
320
|
if (reviewerRuns.has(review.reviewer.run)) out.push(finding("delivery.reviewer-independent", file, `${at}: reviewer run '${review.reviewer.run}' already reviewed reviews[${reviewerRuns.get(review.reviewer.run)}]; every review is a fresh process`));
|