@afokapu/atdd-bun 0.10.16 → 0.10.18
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/conventions/delivery/delivery.model-allowed.convention.yaml +7 -2
- package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +33 -0
- package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +2 -0
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +2 -1
- package/integrity.json +7 -7
- package/package.json +1 -1
- package/planner-schemas/delivery-evidence.schema.json +10 -3
- package/src/delivery.ts +4 -1
|
@@ -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
|
- >-
|
|
@@ -319,3 +319,36 @@ export function writeReport(violations) {
|
|
|
319
319
|
writeFileSync(rp, JSON.stringify({ violations }, null, 2), "utf8");
|
|
320
320
|
process.exit(0);
|
|
321
321
|
}
|
|
322
|
+
|
|
323
|
+
// A route whose train passes through a wagon with no source yet is pending: the plan promises it, and nothing exists to
|
|
324
|
+
// drive yet. The route rules skip it until every wagon on its train has source, then judge it. Derived from the plan and
|
|
325
|
+
// the source tree, never declared, so it cannot be used to park a route; topology keeps reporting the missing source.
|
|
326
|
+
const pendingCache = new Map();
|
|
327
|
+
export function unbuiltWagons(croot, trainId) {
|
|
328
|
+
if (!trainId) return [];
|
|
329
|
+
let byTrain = pendingCache.get(croot);
|
|
330
|
+
if (!byTrain) {
|
|
331
|
+
byTrain = new Map();
|
|
332
|
+
let sourceRoot = "src/wagons";
|
|
333
|
+
try { sourceRoot = Bun.YAML.parse(readText(join(croot, "atdd-bun.yaml")))?.topology?.source_root || sourceRoot; } catch { /* default layout */ }
|
|
334
|
+
const hasSource = (wagon) => [...walkFiles(join(croot, sourceRoot, wagon), isTs)].some((f) => !/\.(test|spec)\.[cm]?[jt]sx?$/.test(f));
|
|
335
|
+
(function rec(dir) {
|
|
336
|
+
let entries;
|
|
337
|
+
try { entries = readdirSync(dir).sort(); } catch { return; }
|
|
338
|
+
for (const name of entries) {
|
|
339
|
+
const full = join(dir, name);
|
|
340
|
+
let st;
|
|
341
|
+
try { st = statSync(full); } catch { continue; }
|
|
342
|
+
if (st.isDirectory()) { if (name !== "_interlockings") rec(full); continue; }
|
|
343
|
+
if (!/\.ya?ml$/.test(name)) continue;
|
|
344
|
+
let doc;
|
|
345
|
+
try { doc = Bun.YAML.parse(readText(full)); } catch { continue; }
|
|
346
|
+
if (!doc || typeof doc.train_id !== "string" || !Array.isArray(doc.sequence)) continue;
|
|
347
|
+
const wagons = new Set(doc.sequence.flatMap((step) => [step?.from, step?.to]).filter((end) => typeof end === "string" && end.startsWith("wagon:")).map((end) => end.slice(6).trim()));
|
|
348
|
+
byTrain.set(doc.train_id, [...wagons].filter((wagon) => !hasSource(wagon)));
|
|
349
|
+
}
|
|
350
|
+
})(join(croot, PLAN_ROOT));
|
|
351
|
+
pendingCache.set(croot, byTrain);
|
|
352
|
+
}
|
|
353
|
+
return byTrain.get(trainId) ?? [];
|
|
354
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
rel,
|
|
19
19
|
mk,
|
|
20
20
|
writeReport,
|
|
21
|
+
unbuiltWagons,
|
|
21
22
|
} from "../_shared/interlocking.mjs";
|
|
22
23
|
|
|
23
24
|
const RULE = "tester.bun.interlocking-route-coverage";
|
|
@@ -34,6 +35,7 @@ for (const scanRoot of roots) {
|
|
|
34
35
|
for (const { file, rec } of records) {
|
|
35
36
|
for (const route of rec.routes) {
|
|
36
37
|
if (isRouteCovered(route, e2eTexts)) continue;
|
|
38
|
+
if (unbuiltWagons(croot, route.trainId).length) continue; // pending: a wagon on its train has no source yet
|
|
37
39
|
const cat =
|
|
38
40
|
route.category !== null
|
|
39
41
|
? `category "${route.category}"`
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// suite green. The runtime obeyed the new plan; no test looked.
|
|
17
17
|
import {
|
|
18
18
|
parseJsonEnv, readText, findConsumerRoots, interlockingFiles, e2eFiles,
|
|
19
|
-
parseInterlocking, tokenCovered, rel, mk, PLAN_ROOT,
|
|
19
|
+
parseInterlocking, tokenCovered, rel, mk, PLAN_ROOT, unbuiltWagons,
|
|
20
20
|
} from "../_shared/interlocking.mjs";
|
|
21
21
|
import { writeFileSync, readdirSync, statSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
@@ -88,6 +88,7 @@ export function scanExecution(scanRoot) {
|
|
|
88
88
|
const anchor = files.length ? rel(files[0].file, croot) : "e2e/";
|
|
89
89
|
for (const trainId of [...trainsReachableFromRoutes(records)].sort()) {
|
|
90
90
|
if (!declaresASequence(croot, trainId)) continue; // nothing declared to exercise
|
|
91
|
+
if (unbuiltWagons(croot, trainId).length) continue; // pending: a wagon on it has no source yet
|
|
91
92
|
if (trainSequenceCovered(trainId, texts)) continue;
|
|
92
93
|
violations.push(
|
|
93
94
|
mk(RULE_SEQUENCE, anchor, 1, 0,
|
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.18",
|
|
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",
|
|
@@ -379,14 +379,14 @@
|
|
|
379
379
|
"detectors/bun_interlocking_binding/fixtures/dirty_transcribed_runtime/server.ts": "d5bc34c5b1b2b0a511e0c4604ce6f9c77491dca9aee994be720540f3052babc1",
|
|
380
380
|
"detectors/bun_interlocking_binding/fixtures/dirty_transcribed_runtime/src/trains/interlocking.ts": "9bcfdb4ecff6efd1f658b9fd587342615179924e25468c75daa07c2626d4d8ab",
|
|
381
381
|
"detectors/bun_interlocking_binding/fixtures/dirty_transcribed_runtime/src/trains/runner.ts": "0e4b885e50f12cc2a8c8f6740bf83e7b2e7137a28e1e143586e9a8550ea5bd45",
|
|
382
|
-
"detectors/bun_interlocking_coverage/_shared/interlocking.mjs": "
|
|
382
|
+
"detectors/bun_interlocking_coverage/_shared/interlocking.mjs": "f1dcc2a23e1656da7ab5b231ad9cd15466144a13f56aa0e15cbfa97ca098eb8d",
|
|
383
383
|
"detectors/bun_interlocking_coverage/atdd.implementation.yaml": "f160517a8138ba3ca05e3d130e6fbfb796e643376f25743fdef4fc210196525b",
|
|
384
384
|
"detectors/bun_interlocking_coverage/checks/_map.json": "0a218ea319cc6258af7bd721c2154cb9561fe67e80d80ef32d08fc77bc9403c3",
|
|
385
385
|
"detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs": "136eb0955052f2485740adfb888f5ba6f1414dcdb53d505034b8db40420a611a",
|
|
386
|
-
"detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs": "
|
|
386
|
+
"detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs": "b91b7920064faa412056edb8a743dfab4fcf11778fc0af02ac6e1d5db1c88212",
|
|
387
387
|
"detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs": "ea4b0155c4f151a36979d850aa2482901b37eacc0fe01cecc0a736089a3e15ce",
|
|
388
388
|
"detectors/bun_interlocking_coverage/checks/interlocking_trace_binds.mjs": "61dcb8a8d04e9764aab7920f346bd3684fc98e2c294aa5c58eefec555145a1d0",
|
|
389
|
-
"detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs": "
|
|
389
|
+
"detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs": "7a38ce0b8bd5b4342a7bae09776b9638dc3ed51635322055a4fb0f0e6940af77",
|
|
390
390
|
"detectors/bun_interlocking_coverage/detect.mjs": "a124418dc29aaf8fda9cd09427230d25768206b3f563d516774667e8cf75b66e",
|
|
391
391
|
"detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/e2e/interlockings/match-resolution/routes.test.ts": "ba6ea85b3fbe15e3c49332da3ad2921a0ff3436a6fe3b5a5035fb35fdc874720",
|
|
392
392
|
"detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/e2e/interlockings/match-resolution/trace.test.ts": "aee4ea93ceb8636846344e915cd7e66f7e8f99ba08596f83266590fa2b9ff58e",
|
|
@@ -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 };
|
|
@@ -252,6 +253,8 @@ function checkModel(file: string, at: string, stage: Stage, role: "writer" | "re
|
|
|
252
253
|
function checkFallbacks(file: string, at: string, fallbacks: Fallback[], fallback: DeliveryPolicy["fallback"]): PlanFinding[] {
|
|
253
254
|
const out: PlanFinding[] = [];
|
|
254
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;
|
|
255
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)`));
|
|
256
259
|
const span = (Date.parse(entry.window.to) - Date.parse(entry.window.from)) / 60_000;
|
|
257
260
|
if (!(span >= 0)) out.push(finding("delivery.model-allowed", file, `${at}: fallback from '${entry.from}' has a window that ends before it starts`));
|