@tyhld/conductor 0.12.89 → 0.12.90
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/headless.js +56 -10
- package/dist/relay.js +129 -16
- package/package.json +1 -1
- package/sales-template/setup.sh +3 -1
- package/scripts/hooks/finish_gate.py +171 -33
- package/scripts/hooks/report-shape.json +243 -0
- package/scripts/hooks/stop_hook.py +1 -1
- package/scripts/hooks/test_finish_gate.py +14 -1
- package/scripts/hooks/test_report_shape.py +122 -0
- package/scripts/update-guard.sh +5 -1
package/dist/headless.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createInterface } from 'node:readline';
|
|
|
19
19
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import os from 'node:os';
|
|
22
|
-
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliverJobEndFacts, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, flushPendingJobEndFacts, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs,
|
|
22
|
+
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliverJobEndFacts, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, flushPendingJobEndFacts, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, FINISH_GATE_KINDS, reportDelivery, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
23
23
|
import { claudePinPath, readClaudePinLine, } from './autoupdate-flags.js';
|
|
24
24
|
const log = (msg) => {
|
|
25
25
|
console.log(`[headless] ${msg}`);
|
|
@@ -558,13 +558,16 @@ export function shouldAbortForBodyHandoff(j) {
|
|
|
558
558
|
return j.bodyHandoff === 'failed' && !j.stopped && !j.timedOut;
|
|
559
559
|
}
|
|
560
560
|
/**
|
|
561
|
-
* 便の終わりの関門が残した「途中で終わった」理由の控え(便 7aa63a97
|
|
561
|
+
* 便の終わりの関門が残した「途中で終わった」理由の控え(便 7aa63a97・便 461be339 で欄を足した)。
|
|
562
562
|
*
|
|
563
|
-
* ★番人側(`scripts/hooks/finish_gate.py
|
|
564
|
-
*
|
|
563
|
+
* ★番人側(`scripts/hooks/finish_gate.py`)が、同じ理由で止める回数の上限に達したときだけ書く。
|
|
564
|
+
* 中継はそれを【読んで消すだけ】で、判定はしない(判定は関門が持つ)。
|
|
565
565
|
* これが要るのは「管制には『途中で終わった』と分かる形で届く」ため。新しい状態は作らず、
|
|
566
|
-
*
|
|
566
|
+
* 既にある「終了の事実」に相乗りする(管制の口を変えない)。
|
|
567
|
+
* ・人向けの1行 … `jobEndDetail`(今までのまま)
|
|
568
|
+
* ・機械で読む形 … `finishGate`(便 461be339 で足した。管制が知らないうちは黙って捨てられる)
|
|
567
569
|
* 控えが無い便=関門が止めずに済んだ便なので、今までどおり null。
|
|
570
|
+
* ★古い版の番人が書いた控え(JSON ではない1行)も読める=入れ替えの途中でも落とさない。
|
|
568
571
|
*/
|
|
569
572
|
export function takeFinishGateResidue(commandId, env = process.env) {
|
|
570
573
|
const xdg = (env.XDG_STATE_HOME ?? '').trim();
|
|
@@ -573,9 +576,9 @@ export function takeFinishGateResidue(commandId, env = process.env) {
|
|
|
573
576
|
if (!safe)
|
|
574
577
|
return null;
|
|
575
578
|
const file = path.join(root, 'conductor', 'finish-gate', `${safe}.residue`);
|
|
576
|
-
let
|
|
579
|
+
let raw;
|
|
577
580
|
try {
|
|
578
|
-
|
|
581
|
+
raw = readFileSync(file, 'utf8').trim();
|
|
579
582
|
}
|
|
580
583
|
catch {
|
|
581
584
|
return null;
|
|
@@ -585,7 +588,41 @@ export function takeFinishGateResidue(commandId, env = process.env) {
|
|
|
585
588
|
unlinkSync(file);
|
|
586
589
|
}
|
|
587
590
|
catch { /* 消せなくても害は無い(同じ便の控えは1つだけ) */ }
|
|
588
|
-
|
|
591
|
+
if (raw.length === 0)
|
|
592
|
+
return null;
|
|
593
|
+
return parseFinishGateResidue(raw);
|
|
594
|
+
}
|
|
595
|
+
/** 控えの字面を読む(★純粋関数。JSON でなければ人向けの1行として扱う)。 */
|
|
596
|
+
export function parseFinishGateResidue(raw) {
|
|
597
|
+
const text = String(raw ?? '').trim();
|
|
598
|
+
if (text.length === 0)
|
|
599
|
+
return null;
|
|
600
|
+
let got = null;
|
|
601
|
+
try {
|
|
602
|
+
got = JSON.parse(text);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return { text: text.slice(0, 500), facts: null }; // 古い形(1行だけ)
|
|
606
|
+
}
|
|
607
|
+
if (got === null || typeof got !== 'object' || Array.isArray(got)) {
|
|
608
|
+
return { text: text.slice(0, 500), facts: null };
|
|
609
|
+
}
|
|
610
|
+
const rec = got;
|
|
611
|
+
const line = typeof rec.text === 'string' ? rec.text.trim() : '';
|
|
612
|
+
if (line.length === 0)
|
|
613
|
+
return null;
|
|
614
|
+
const reasons = Array.isArray(rec.reasons)
|
|
615
|
+
? FINISH_GATE_KINDS.filter((k) => rec.reasons.includes(k))
|
|
616
|
+
: [];
|
|
617
|
+
const countsRaw = (rec.counts ?? null);
|
|
618
|
+
const counts = {};
|
|
619
|
+
for (const k of FINISH_GATE_KINDS) {
|
|
620
|
+
const n = Number(countsRaw?.[k] ?? 0);
|
|
621
|
+
counts[k] = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
622
|
+
}
|
|
623
|
+
// ★「何が残っていたか」が読めない控えは、機械で読む形を作らない(作り話の事実を送らない)。
|
|
624
|
+
const facts = reasons.length > 0 ? { exhausted: true, reasons: reasons, counts } : null;
|
|
625
|
+
return { text: line.slice(0, 500), facts };
|
|
589
626
|
}
|
|
590
627
|
/**
|
|
591
628
|
* いま画面なし方式が追っている便の指示ID(★回収の道から除く物・便 6ebba0d0 の b)。
|
|
@@ -1281,6 +1318,11 @@ async function checkRunningJob(cfg, state) {
|
|
|
1281
1318
|
authError: isAuthError(job),
|
|
1282
1319
|
exitCode: job.exitCode,
|
|
1283
1320
|
});
|
|
1321
|
+
// ★「報告を出せたか」はファイルの有無【と形】で見る(便 461be339)。
|
|
1322
|
+
// 形が整っていない=書きかけの報告は「届いた」と数えず、理由を添えて管制へ届ける。
|
|
1323
|
+
// ★本文の言葉は1文字も読まない(2026-09-23 の確定のまま。下書きの印は関門の側だけ)。
|
|
1324
|
+
const delivery = reportDelivery(job.commandId);
|
|
1325
|
+
const residue = takeFinishGateResidue(job.commandId);
|
|
1284
1326
|
await deliverJobEndFacts(cfg, {
|
|
1285
1327
|
commandId: job.commandId,
|
|
1286
1328
|
site: cfg.site,
|
|
@@ -1288,16 +1330,20 @@ async function checkRunningJob(cfg, state) {
|
|
|
1288
1330
|
endKind,
|
|
1289
1331
|
exitCode: job.exitCode,
|
|
1290
1332
|
exitSignal: job.exitSignal,
|
|
1291
|
-
reportDelivered:
|
|
1333
|
+
reportDelivered: delivery.delivered,
|
|
1292
1334
|
// ★画面なし方式は中継が自分で立ち上げた処理なので、終わりを直接つかんでいる。
|
|
1293
1335
|
// つかめていないときは false を送る(1-3 の「つかめていない」と同じ形)。
|
|
1294
1336
|
processEndObserved: job.finishedAt !== null,
|
|
1295
1337
|
startedAt: new Date(job.startedAt).toISOString(),
|
|
1296
1338
|
// ★終わりの関門が「途中で終わった」と残していれば、その理由を先に載せる(便 7aa63a97)。
|
|
1297
1339
|
// 本文を渡せなかった理由がいちばん強い(職人は何も受け取っていない)ので、それが先。
|
|
1340
|
+
// そのどちらも無く、報告の形が整っていないときは、その理由を載せる(便 461be339)。
|
|
1298
1341
|
detail: job.bodyHandoffError
|
|
1299
|
-
??
|
|
1342
|
+
?? residue?.text
|
|
1343
|
+
?? delivery.note
|
|
1300
1344
|
?? `attempt ${job.attempt}/${HEADLESS_MAX_ATTEMPTS}`,
|
|
1345
|
+
// ★上限に達して「途中で終わった」便だけに付ける(管制が機械で見分けられる形)。
|
|
1346
|
+
...(residue?.facts != null ? { finishGate: residue.facts } : {}),
|
|
1301
1347
|
}, { logLine: log });
|
|
1302
1348
|
}
|
|
1303
1349
|
if (job.timedOut) {
|
package/dist/relay.js
CHANGED
|
@@ -1475,6 +1475,81 @@ export function reportPhase(commandId, env) {
|
|
|
1475
1475
|
}
|
|
1476
1476
|
return 'none';
|
|
1477
1477
|
}
|
|
1478
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1479
|
+
// ★報告を「届いた」と数える条件=ファイルが在る【かつ】形が整っている(便 461be339)
|
|
1480
|
+
//
|
|
1481
|
+
// 【なぜ要るか(実地 915694ac・前便 7aa63a97 で残った穴)】従来は reportPhase() が
|
|
1482
|
+
// 【ファイルの有無だけ】を見ていた。だから書きかけの報告でも「届いた」と数えられ、
|
|
1483
|
+
// `processed/` へ移され、管制は「終わりました・✅」になった。合流も公開もしていなかった。
|
|
1484
|
+
// 便の終わりの関門(`scripts/hooks/finish_gate.py`)は職人が【きれいに手番を閉じたとき】に
|
|
1485
|
+
// しか鳴らないので、処理が落ちた・上限時間で止められた便では書きかけが完了の材料になる道が
|
|
1486
|
+
// 残っていた。そこを、中継の側でも閉じる。
|
|
1487
|
+
//
|
|
1488
|
+
// 【★見るのは「形」だけ。本文の言葉は1文字も読まない】報告の中身の言葉で完了を判定しない
|
|
1489
|
+
// (えふさん確定 2026-09-23)。見るのは parseReport() が返す `missing` だけ=
|
|
1490
|
+
// 囲みが閉じている/5つの欄が在る/outcome が決まった語/evidence が1件以上/本文が空でない。
|
|
1491
|
+
// 「作成中」「TODO」などの下書きの印を見るのは関門の側だけ(正本は finish_gate.py)。
|
|
1492
|
+
//
|
|
1493
|
+
// 【★形の決まりの正本は1か所】`scripts/hooks/report-shape.json`。
|
|
1494
|
+
// REQUIRED_REPORT_FIELDS / REPORT_OUTCOMES がその値と同じであることを
|
|
1495
|
+
// `test/report-shape-gate.test.ts` が固定し、同じ `cases` を関門の試験とこちらで流す
|
|
1496
|
+
// =2か所に同じ決まりを書かない。
|
|
1497
|
+
//
|
|
1498
|
+
// 【★形が整っていないときに何をするか】
|
|
1499
|
+
// ・「届いた」と数えない(`reportDelivered: false` + 理由を添えて管制へ届ける)
|
|
1500
|
+
// ・ファイルは【消さない・processed/ へも移さない】=職人が書き直せる・人が読める
|
|
1501
|
+
// ・管制へ本文を渡さない(書きかけを完了の材料にしない)
|
|
1502
|
+
//
|
|
1503
|
+
// 【★例外は1つだけ】「作業なし・本文未着」の申告(isInstructionMissingReport・便 7d810bbb)。
|
|
1504
|
+
// 職人は本文を受け取っていないので囲みを書く材料が無い。実地 af960f79 の報告は囲みが無い。
|
|
1505
|
+
// これを止めると「本文未着が管制に届かない」という直した障害が戻るので、形の確かめから外す。
|
|
1506
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1507
|
+
/** 報告の「形」で足りない物(空なら整っている)。★本文の言葉は読まない。 */
|
|
1508
|
+
export function reportShapeMissing(raw) {
|
|
1509
|
+
return parseReport(raw).missing;
|
|
1510
|
+
}
|
|
1511
|
+
/** 形が整っていないことを人へ伝える一言(★報告の中身は1文字も写さない)。 */
|
|
1512
|
+
export function reportShapeNote(missing) {
|
|
1513
|
+
return `報告は在りますが形が整っていません(足りない: ${missing.join(', ')})`;
|
|
1514
|
+
}
|
|
1515
|
+
/** その指示の報告の段と形を1回で見る(★未処理 → 処理済みの順・readParsedReport と同じ順)。 */
|
|
1516
|
+
export function reportDelivery(commandId, env) {
|
|
1517
|
+
const groups = [
|
|
1518
|
+
[pendingCandidates(commandId, env), 'pending'],
|
|
1519
|
+
[processedCandidates(commandId, env), 'processed'],
|
|
1520
|
+
];
|
|
1521
|
+
for (const [files, phase] of groups) {
|
|
1522
|
+
for (const file of files) {
|
|
1523
|
+
const raw = readIfFilled(file);
|
|
1524
|
+
if (raw === null)
|
|
1525
|
+
continue;
|
|
1526
|
+
const instructionMissing = isInstructionMissingReport(raw);
|
|
1527
|
+
const missing = instructionMissing ? [] : reportShapeMissing(raw);
|
|
1528
|
+
const delivered = missing.length === 0;
|
|
1529
|
+
return {
|
|
1530
|
+
phase,
|
|
1531
|
+
raw,
|
|
1532
|
+
delivered,
|
|
1533
|
+
missing,
|
|
1534
|
+
instructionMissing,
|
|
1535
|
+
note: delivered ? null : reportShapeNote(missing),
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
phase: 'none',
|
|
1541
|
+
raw: null,
|
|
1542
|
+
delivered: false,
|
|
1543
|
+
missing: [],
|
|
1544
|
+
instructionMissing: false,
|
|
1545
|
+
note: null,
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
/** 管制へ渡してよい報告の本文(形が整っていなければ null=渡さない)。 */
|
|
1549
|
+
export function readDeliverableReport(commandId, env) {
|
|
1550
|
+
const d = reportDelivery(commandId, env);
|
|
1551
|
+
return d.delivered && d.raw !== null ? parseReport(d.raw).body : null;
|
|
1552
|
+
}
|
|
1478
1553
|
/**
|
|
1479
1554
|
* 管制へ渡し終えた報告を【処理済み】にする(★消さない・削除しない)。
|
|
1480
1555
|
* 同じ置き場の `processed/` へ移すだけ。移した時刻を更新時刻に刻む(30日の数え始め)。
|
|
@@ -1611,6 +1686,7 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
|
1611
1686
|
const exclude = new Set(opts.exclude ?? []);
|
|
1612
1687
|
const rejectCounts = opts.rejectCounts ?? new Map();
|
|
1613
1688
|
const items = [];
|
|
1689
|
+
let malformed = 0;
|
|
1614
1690
|
for (const id of pendingReportIds(env, REPORT_FILES_MAX_PER_ROUND).ids) {
|
|
1615
1691
|
for (const file of pendingCandidates(id, env)) {
|
|
1616
1692
|
const raw = readIfFilled(file);
|
|
@@ -1619,6 +1695,17 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
|
1619
1695
|
// (便 7d810bbb)。完了の道はその便を完了にしないので、ここで送らないと管制に届かない。
|
|
1620
1696
|
if (exclude.has(id) && !isInstructionMissingReport(raw))
|
|
1621
1697
|
break;
|
|
1698
|
+
// ★形が整っていない報告は送らない(便 461be339)。未処理のまま残すので、職人が書き直せば
|
|
1699
|
+
// 次の周でそのまま届く。★本文の言葉は1文字も読まない(見るのは形だけ)。
|
|
1700
|
+
if (!isInstructionMissingReport(raw)) {
|
|
1701
|
+
const missing = reportShapeMissing(raw);
|
|
1702
|
+
if (missing.length > 0) {
|
|
1703
|
+
malformed++;
|
|
1704
|
+
logLine(`site=${cfg.site} id=${id} ${reportShapeNote(missing)}` +
|
|
1705
|
+
'→管制へ送らず未処理のまま残します(職人が書き直せます)');
|
|
1706
|
+
break;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1622
1709
|
items.push({ id, raw });
|
|
1623
1710
|
break;
|
|
1624
1711
|
}
|
|
@@ -1690,10 +1777,12 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
|
1690
1777
|
}
|
|
1691
1778
|
}
|
|
1692
1779
|
}
|
|
1693
|
-
if (items.length > 0) {
|
|
1694
|
-
logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept}
|
|
1780
|
+
if (items.length > 0 || malformed > 0) {
|
|
1781
|
+
logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件` +
|
|
1782
|
+
`${rejected > 0 ? `・退けた ${rejected} 件` : ''}` +
|
|
1783
|
+
`${malformed > 0 ? `・形が整っていない ${malformed} 件(未処理のまま残す)` : ''}`);
|
|
1695
1784
|
}
|
|
1696
|
-
return { sent, processed, kept, rejected };
|
|
1785
|
+
return { sent, processed, kept, rejected, malformed };
|
|
1697
1786
|
}
|
|
1698
1787
|
/**
|
|
1699
1788
|
* 古い控えを片付ける。★消すのは `processed/` の中だけ(未処理は何日たっても消さない)。
|
|
@@ -2620,17 +2709,22 @@ export async function patchStatus(cfg, id, status, report) {
|
|
|
2620
2709
|
* 返り: 追跡を畳んでよいか(true = 便の終了状態が確定した/false = 書けていない=次の周でやり直す)。
|
|
2621
2710
|
*/
|
|
2622
2711
|
export async function completeWithReport(cfg, id, logLine = log) {
|
|
2623
|
-
//
|
|
2624
|
-
|
|
2712
|
+
// ★①報告がどの段にあるか+形が整っているか(送る工程の話)。②③の実行可否には使わない。
|
|
2713
|
+
// ★形が整っていない報告は「届いた」と数えない(便 461be339)。ファイルは消さず・移さず残す。
|
|
2714
|
+
const delivery = reportDelivery(id);
|
|
2715
|
+
const phase = delivery.phase;
|
|
2625
2716
|
// ★②便が終わっているか(確定の工程の話)。管制の真実だけを見る。
|
|
2626
2717
|
const already = await fetchCommandStatus(cfg, id);
|
|
2627
2718
|
if (isCentralFinished(already)) {
|
|
2628
2719
|
// 既に終端(職人の conductor_complete・人の操作・failed 等)。★何も書かない。
|
|
2629
2720
|
// 控えは消さずに処理済みとして残す(MCP 経由でも同じ控えが残る・えふさん確定 2026-09-21)。
|
|
2630
|
-
|
|
2721
|
+
// ★形が整っていない報告は未処理のまま置く(職人が書き直せる・人が読める)。
|
|
2722
|
+
if (phase === 'pending' && delivery.delivered)
|
|
2631
2723
|
markReportProcessed(id);
|
|
2632
2724
|
logLine(`site=${cfg.site} id=${id} は管制で既に終わっています(status=${already})` +
|
|
2633
|
-
'
|
|
2725
|
+
(phase === 'pending' && !delivery.delivered
|
|
2726
|
+
? `→relayからは送りません(${delivery.note}=未処理のまま残します)`
|
|
2727
|
+
: '→relayからは送りません(報告は控えとして処理済みにしました)'));
|
|
2634
2728
|
return true;
|
|
2635
2729
|
}
|
|
2636
2730
|
if (phase === 'processed') {
|
|
@@ -2641,13 +2735,19 @@ export async function completeWithReport(cfg, id, logLine = log) {
|
|
|
2641
2735
|
logLine(`site=${cfg.site} id=${id} done(報告は回収の道で渡し済み=本文は添えない)`);
|
|
2642
2736
|
return true;
|
|
2643
2737
|
}
|
|
2644
|
-
|
|
2738
|
+
// ★形が整っていない報告は添えない(書きかけを完了の材料にしない)。便の確定は今までどおり行う
|
|
2739
|
+
// =現場を占有したままにしない。「報告が届いていない」ことは終了の事実で管制へ届く。
|
|
2740
|
+
const report = delivery.delivered && delivery.raw !== null ? parseReport(delivery.raw).body : null;
|
|
2645
2741
|
if (!(await patchStatus(cfg, id, 'done', report)))
|
|
2646
2742
|
return false;
|
|
2647
2743
|
if (report !== null)
|
|
2648
2744
|
markReportProcessed(id); // ★消さずに処理済みへ(同じ報告を二度送らない)
|
|
2649
2745
|
logLine(`site=${cfg.site} id=${id} done` +
|
|
2650
|
-
(report !== null
|
|
2746
|
+
(report !== null
|
|
2747
|
+
? `(報告添付・${report.length}文字)`
|
|
2748
|
+
: phase === 'none'
|
|
2749
|
+
? '(報告ファイルなし=完了のみ)'
|
|
2750
|
+
: `(${delivery.note}=添えずに未処理のまま残します)`));
|
|
2651
2751
|
return true;
|
|
2652
2752
|
}
|
|
2653
2753
|
/**
|
|
@@ -2957,6 +3057,8 @@ export async function sendHeartbeat(cfg, id, busy = false, liveness) {
|
|
|
2957
3057
|
log(`site=${cfg.site} id=${id} heartbeat -> 失敗: ${err instanceof Error ? err.message : String(err)}(縮退で安全)`);
|
|
2958
3058
|
}
|
|
2959
3059
|
}
|
|
3060
|
+
/** 便の終わりの関門の種類(控えの数え方の見出し。★`finish_gate.py` の GATE_KINDS と同じ語)。 */
|
|
3061
|
+
export const FINISH_GATE_KINDS = ['report', 'pr', 'background'];
|
|
2960
3062
|
/** 届けられなかった「終了の事実」の置き場。 */
|
|
2961
3063
|
export function jobEndDir(env = process.env) {
|
|
2962
3064
|
const base = (env.CONDUCTOR_STATE_DIR ?? '').trim();
|
|
@@ -2993,6 +3095,10 @@ export function jobEndFactsBody(f) {
|
|
|
2993
3095
|
body.startedAt = f.startedAt;
|
|
2994
3096
|
if (f.detail !== undefined && f.detail !== '')
|
|
2995
3097
|
body.jobEndDetail = f.detail.slice(0, JOB_END_DETAIL_MAX);
|
|
3098
|
+
// ★上限に達して「途中で終わった」便だけに付ける(付いていること自体が印)。
|
|
3099
|
+
// jobEndDetail の人向けの文は今までのまま残す(管制が知らないうちは黙って捨てられる)。
|
|
3100
|
+
if (f.finishGate !== undefined)
|
|
3101
|
+
body.finishGate = f.finishGate;
|
|
2996
3102
|
return body;
|
|
2997
3103
|
}
|
|
2998
3104
|
/** 控えを手元に書く(★届くまで消さない)。書けたか返す。 */
|
|
@@ -3310,12 +3416,16 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
3310
3416
|
return;
|
|
3311
3417
|
}
|
|
3312
3418
|
// 完了は従来の経路(画面の DONE 行)にそのまま乗せる。★報告は職人が書いたファイルから読む。
|
|
3313
|
-
|
|
3419
|
+
// ★形が整っていない報告は添えない・移さない(便 461be339)。
|
|
3420
|
+
const kaDelivery = reportDelivery(ka.id);
|
|
3421
|
+
const report = kaDelivery.delivered && kaDelivery.raw !== null
|
|
3422
|
+
? parseReport(kaDelivery.raw).body
|
|
3423
|
+
: null;
|
|
3314
3424
|
if (await complete(cfg, ka.id, report)) {
|
|
3315
3425
|
if (report !== null)
|
|
3316
3426
|
markReportProcessed(ka.id); // ★消さずに処理済みへ(二度送らない)
|
|
3317
3427
|
logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 画面のDONEを検出し完了を報告` +
|
|
3318
|
-
`${report != null ? '(報告添付)' : ''}`);
|
|
3428
|
+
`${report != null ? '(報告添付)' : kaDelivery.phase === 'none' ? '' : `(${kaDelivery.note})`}`);
|
|
3319
3429
|
// ★終わったら必ず手を下ろす(挙げっぱなしにすると現場が長くふさがる)。
|
|
3320
3430
|
noteBusyEdge(state, cfg.site, ka.id, false, logLine);
|
|
3321
3431
|
state.keepalive = null;
|
|
@@ -3501,7 +3611,8 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3501
3611
|
endKind: 'not_started',
|
|
3502
3612
|
exitCode: null,
|
|
3503
3613
|
exitSignal: null,
|
|
3504
|
-
|
|
3614
|
+
// ★「作業なし・本文未着」の申告は形の確かめから外してある(便 461be339)。
|
|
3615
|
+
reportDelivered: reportDelivery(state.pending.id).delivered,
|
|
3505
3616
|
processEndObserved: false,
|
|
3506
3617
|
detail: '配り役が本文未着と申告した便(職人の処理は中継が持っていない)',
|
|
3507
3618
|
}, { logLine: log });
|
|
@@ -3543,7 +3654,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3543
3654
|
endKind: 'completed',
|
|
3544
3655
|
exitCode: null,
|
|
3545
3656
|
exitSignal: null,
|
|
3546
|
-
reportDelivered: report != null ||
|
|
3657
|
+
reportDelivered: report != null || reportDelivery(state.pending.id).delivered,
|
|
3547
3658
|
processEndObserved: false,
|
|
3548
3659
|
detail: '配り役が完了の知らせで確かめた(職人の処理は中継が持っていない)',
|
|
3549
3660
|
}, { logLine: log });
|
|
@@ -3556,7 +3667,8 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3556
3667
|
if (below !== null && hasStandaloneDone(below, cfg.site)) {
|
|
3557
3668
|
// ★報告は画面から読まない。職人が書いたファイルを読む(relayReportDirs / readReport)。
|
|
3558
3669
|
// 画面を読む方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
|
|
3559
|
-
|
|
3670
|
+
// ★「届いた」は形まで見る(便 461be339)。整っていなければ理由も一緒に届ける。
|
|
3671
|
+
const doneDelivery = reportDelivery(state.pending.id);
|
|
3560
3672
|
const ok = await completeWithReport(cfg, state.pending.id, log);
|
|
3561
3673
|
if (ok) {
|
|
3562
3674
|
// ★終了の事実を同じ項目で届ける(便 915694ac・1-3)。画面へ貼る方式では実プロセスを
|
|
@@ -3568,9 +3680,10 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3568
3680
|
endKind: 'completed',
|
|
3569
3681
|
exitCode: null,
|
|
3570
3682
|
exitSignal: null,
|
|
3571
|
-
reportDelivered:
|
|
3683
|
+
reportDelivered: doneDelivery.delivered,
|
|
3572
3684
|
processEndObserved: false,
|
|
3573
|
-
detail: '配り役が画面の完了の合図で確かめた(職人の処理は中継が持っていない)'
|
|
3685
|
+
detail: '配り役が画面の完了の合図で確かめた(職人の処理は中継が持っていない)'
|
|
3686
|
+
+ (doneDelivery.note !== null ? `/${doneDelivery.note}` : ''),
|
|
3574
3687
|
}, { logLine: log });
|
|
3575
3688
|
state.pending = null;
|
|
3576
3689
|
}
|
package/package.json
CHANGED
package/sales-template/setup.sh
CHANGED
|
@@ -862,7 +862,9 @@ GUARD_FILES=(
|
|
|
862
862
|
finish_gate.py # stop_hook が使う「便の終わりの関門」の判定(便 7aa63a97)
|
|
863
863
|
)
|
|
864
864
|
GUARD_ENTRYPOINTS=(cc2_guard.py permission_request_hook.py stop_hook.py notification_hook.py)
|
|
865
|
-
|
|
865
|
+
# ★この一覧は scripts/update-guard.sh の INSTALL_CONFIGS と【同じ集合】にすること。
|
|
866
|
+
# report-shape.json … 報告の「形」の決まりの正本(finish_gate.py が実行時に読む・便 461be339)。
|
|
867
|
+
GUARD_CONFIGS=(safe_verbs.json network_allow.json report-shape.json) # 任意・あれば同居させる
|
|
866
868
|
|
|
867
869
|
# --- ⑤-0 見本と「配る中身」がズレていないか機械照合する(★足し忘れの再発防止) ---
|
|
868
870
|
# 見本が呼ぶ入口スクリプトが (a) 正本にあるか (b) 上の GUARD_FILES に入っているか を確かめる。
|
|
@@ -124,44 +124,153 @@ def read_report_text(command_id, env=None):
|
|
|
124
124
|
|
|
125
125
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
126
126
|
# (2) 報告が無い・下書きのまま
|
|
127
|
+
#
|
|
128
|
+
# 【★形の決まりの正本は1か所(便 461be339)】
|
|
129
|
+
# 「囲みが閉じている/5つの欄が在る/outcome が決まった語/evidence が1件以上/本文が空でない」は
|
|
130
|
+
# 中継(src/relay.ts の parseReport / missingOf)も同じ判定をする。同じ決まりを2か所に書くと、
|
|
131
|
+
# 片方だけ直して食い違う(=関門は差し戻すのに中継は届いたと数える、またはその逆)。
|
|
132
|
+
# だから決まりは `report-shape.json`(この python と同じフォルダに配られる)だけに置き、
|
|
133
|
+
# ここは【読み手】にする。中継の側は定数がこの JSON と同じであることを試験で固定する。
|
|
134
|
+
# ★読めなかったときは形の判定をしない(fail open=関門が足される前と同じ動き)。
|
|
135
|
+
#
|
|
136
|
+
# 【★本文の言葉だけは別】下書きの印(作成中・TODO など)は関門だけの決まり。
|
|
137
|
+
# 報告の中身の言葉で完了を判定しない(えふさん確定 2026-09-23)ので、中継からは手が届かない
|
|
138
|
+
# ところ(この下の DRAFT_MARKERS)に置く。正本はここ1か所。
|
|
127
139
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
-
#: 囲み(front matter)に要る欄。鉄則「便の終え方」②のとおり。
|
|
129
|
-
REQUIRED_KEYS = ('instruction_id', 'site', 'outcome', 'created_at', 'evidence')
|
|
130
|
-
#: `outcome` に書ける値。
|
|
131
|
-
VALID_OUTCOMES = ('pushed', 'no_change', 'blocked')
|
|
132
140
|
#: 「最終版で差し替える前」の印。★決まった字面の一覧だけを見る(文意は読まない)。
|
|
133
141
|
DRAFT_MARKERS = ('作成中', '下書き', '差し替える', 'あとで書く', '(未記入)', '(未記入)', 'TODO', 'TBD')
|
|
134
142
|
|
|
143
|
+
#: 形の決まりの正本(★この python と同じフォルダに配られる)。
|
|
144
|
+
SHAPE_SPEC_NAME = 'report-shape.json'
|
|
145
|
+
|
|
146
|
+
#: 足りない物の名前 → 職人へ見せる日本語。
|
|
147
|
+
_SHAPE_JA = {
|
|
148
|
+
'instruction_id': 'instruction_id の欄',
|
|
149
|
+
'site': 'site の欄',
|
|
150
|
+
'outcome': 'outcome(pushed / no_change / blocked のどれか1つ)',
|
|
151
|
+
'created_at': 'created_at の欄',
|
|
152
|
+
'evidence': 'evidence(1件以上)',
|
|
153
|
+
'body': '囲みの下の本文',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def shape_spec_path():
|
|
158
|
+
"""形の決まりの正本の置き場(自分と同じフォルダ)。"""
|
|
159
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), SHAPE_SPEC_NAME)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def load_shape_spec(path=None):
|
|
163
|
+
"""形の決まりを読む。読めない・形が違うなら None(=形の判定をしない)。"""
|
|
164
|
+
try:
|
|
165
|
+
with open(path or shape_spec_path(), encoding='utf-8') as f:
|
|
166
|
+
got = json.load(f)
|
|
167
|
+
except Exception: # noqa: BLE001
|
|
168
|
+
return None
|
|
169
|
+
if not isinstance(got, dict):
|
|
170
|
+
return None
|
|
171
|
+
fields = got.get('requiredFields')
|
|
172
|
+
outcomes = got.get('validOutcomes')
|
|
173
|
+
if not isinstance(fields, list) or not fields or not all(isinstance(k, str) for k in fields):
|
|
174
|
+
return None
|
|
175
|
+
if not isinstance(outcomes, list) or not outcomes or not all(isinstance(k, str) for k in outcomes):
|
|
176
|
+
return None
|
|
177
|
+
return got
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
#: 読み込んだ正本(無ければ None)。
|
|
181
|
+
SHAPE_SPEC = load_shape_spec()
|
|
135
182
|
|
|
136
|
-
|
|
137
|
-
|
|
183
|
+
_KV_RE = re.compile(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$')
|
|
184
|
+
_BULLET_RE = re.compile(r'^\s*-\s+(.*)$')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _parse_report(text):
|
|
188
|
+
"""報告を欄と本文に分ける(★中継の parseReport と同じ読み方)。
|
|
189
|
+
|
|
190
|
+
返り: (欄の辞書, evidence の一覧, 本文)。囲みが無い・閉じていないときは
|
|
191
|
+
欄も evidence も空で、全文が本文になる(中継と同じ倒れ方)。
|
|
192
|
+
"""
|
|
193
|
+
raw = '' if text is None else str(text)
|
|
194
|
+
if raw.startswith(''):
|
|
195
|
+
raw = raw[1:]
|
|
196
|
+
lines = raw.split('\n')
|
|
197
|
+
i = 0
|
|
198
|
+
while i < len(lines) and lines[i].strip() == '':
|
|
199
|
+
i += 1
|
|
200
|
+
if i >= len(lines) or lines[i].strip() != '---':
|
|
201
|
+
return {}, [], raw.strip()
|
|
202
|
+
fields, evidence, last_key = {}, [], ''
|
|
203
|
+
j = i + 1
|
|
204
|
+
closed = False
|
|
205
|
+
while j < len(lines):
|
|
206
|
+
line = lines[j]
|
|
207
|
+
if line.strip() == '---':
|
|
208
|
+
closed = True
|
|
209
|
+
break
|
|
210
|
+
bullet = _BULLET_RE.match(line)
|
|
211
|
+
if bullet is not None:
|
|
212
|
+
if last_key == 'evidence' and bullet.group(1).strip() != '':
|
|
213
|
+
evidence.append(bullet.group(1).strip())
|
|
214
|
+
j += 1
|
|
215
|
+
continue
|
|
216
|
+
kv = _KV_RE.match(line)
|
|
217
|
+
if kv is None:
|
|
218
|
+
j += 1
|
|
219
|
+
continue
|
|
220
|
+
last_key = kv.group(1).lower()
|
|
221
|
+
value = kv.group(2).strip()
|
|
222
|
+
if last_key == 'evidence':
|
|
223
|
+
if value != '':
|
|
224
|
+
evidence.append(value)
|
|
225
|
+
j += 1
|
|
226
|
+
continue
|
|
227
|
+
fields[last_key] = value
|
|
228
|
+
j += 1
|
|
229
|
+
if not closed:
|
|
230
|
+
return {}, [], raw.strip()
|
|
231
|
+
return fields, evidence, '\n'.join(lines[j + 1:]).strip()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def report_shape_missing(text, spec=None):
|
|
235
|
+
"""報告の「形」で足りない物を並べる(空なら整っている)。★純粋関数。
|
|
236
|
+
|
|
237
|
+
★正本が読めないときは [] を返す(形の判定をしない=退行しない)。
|
|
238
|
+
"""
|
|
239
|
+
rules = SHAPE_SPEC if spec is None else spec
|
|
240
|
+
if not rules:
|
|
241
|
+
return []
|
|
242
|
+
fields, evidence, body = _parse_report(text)
|
|
243
|
+
outcomes = tuple(rules['validOutcomes'])
|
|
244
|
+
out = []
|
|
245
|
+
for key in rules['requiredFields']:
|
|
246
|
+
if key == 'body':
|
|
247
|
+
if body.strip() == '':
|
|
248
|
+
out.append(key)
|
|
249
|
+
elif key == 'evidence':
|
|
250
|
+
if not evidence:
|
|
251
|
+
out.append(key)
|
|
252
|
+
elif key == 'outcome':
|
|
253
|
+
if (fields.get('outcome') or '').strip() not in outcomes:
|
|
254
|
+
out.append(key)
|
|
255
|
+
elif not (fields.get(key) or '').strip():
|
|
256
|
+
out.append(key)
|
|
257
|
+
return out
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def report_body(text):
|
|
261
|
+
"""報告の本文(囲みの下)。囲みが無い・閉じていないときは全文。★純粋関数。"""
|
|
262
|
+
return _parse_report(text)[2]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def report_residue(text, spec=None):
|
|
266
|
+
"""報告が「無い・形が整っていない・下書きのまま」なら理由、出来上がっていれば None。"""
|
|
138
267
|
if text is None or not text.strip():
|
|
139
268
|
return '報告ファイルが無い(または空)'
|
|
140
|
-
|
|
141
|
-
if lines[0].strip() != '---':
|
|
142
|
-
return '報告の1行目に囲み(---)が無い'
|
|
143
|
-
try:
|
|
144
|
-
end = next(i for i in range(1, len(lines)) if lines[i].strip() == '---')
|
|
145
|
-
except StopIteration:
|
|
146
|
-
return '報告の囲みが閉じていない(2つめの --- が無い)'
|
|
147
|
-
head = lines[1:end]
|
|
148
|
-
body = '\n'.join(lines[end + 1:]).strip()
|
|
149
|
-
keys = {}
|
|
150
|
-
for ln in head:
|
|
151
|
-
m = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$', ln)
|
|
152
|
-
if m:
|
|
153
|
-
keys[m.group(1)] = m.group(2).strip()
|
|
154
|
-
missing = [k for k in REQUIRED_KEYS if k not in keys]
|
|
269
|
+
missing = report_shape_missing(text, spec)
|
|
155
270
|
if missing:
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# evidence は「- 」で始まる行が1つ以上要る。
|
|
160
|
-
if not any(ln.strip().startswith('- ') for ln in head):
|
|
161
|
-
return '報告の evidence が1件も無い'
|
|
162
|
-
if not body:
|
|
163
|
-
return '報告の本文が空(囲みだけ)'
|
|
164
|
-
hit = next((m for m in DRAFT_MARKERS if m in body), None)
|
|
271
|
+
return ('報告の形が整っていない(足りない: '
|
|
272
|
+
+ '・'.join(_SHAPE_JA.get(k, k) for k in missing) + ')')
|
|
273
|
+
hit = next((m for m in DRAFT_MARKERS if m in report_body(text)), None)
|
|
165
274
|
if hit is not None:
|
|
166
275
|
return f'報告が下書きのまま(本文に「{hit}」が残っている)'
|
|
167
276
|
return None
|
|
@@ -306,13 +415,25 @@ def save_counts(command_id, counts, env=None):
|
|
|
306
415
|
pass
|
|
307
416
|
|
|
308
417
|
|
|
309
|
-
def save_residue(command_id,
|
|
310
|
-
"""上限に達して終わらせた理由を残す(中継が終了の事実へ載せる)。
|
|
418
|
+
def save_residue(command_id, record, env=None):
|
|
419
|
+
"""上限に達して終わらせた理由を残す(中継が終了の事実へ載せる)。
|
|
420
|
+
|
|
421
|
+
record は `build_residue_record()` の辞書(人向けの1行+機械が読む形)。
|
|
422
|
+
★文字列を渡したときは人向けの1行だけとして扱う(古い呼び方も通る)。
|
|
423
|
+
"""
|
|
424
|
+
if isinstance(record, str):
|
|
425
|
+
record = {'text': record}
|
|
426
|
+
text = str(record.get('text') or '').strip()[:500]
|
|
427
|
+
out = {'text': text}
|
|
428
|
+
if isinstance(record.get('reasons'), list):
|
|
429
|
+
out['reasons'] = [k for k in record['reasons'] if k in GATE_KINDS]
|
|
430
|
+
if isinstance(record.get('counts'), dict):
|
|
431
|
+
out['counts'] = {k: int(record['counts'].get(k, 0)) for k in GATE_KINDS}
|
|
311
432
|
p = residue_path(command_id, env)
|
|
312
433
|
try:
|
|
313
434
|
os.makedirs(os.path.dirname(p), mode=0o700, exist_ok=True)
|
|
314
435
|
with open(p, 'w', encoding='utf-8') as f:
|
|
315
|
-
|
|
436
|
+
json.dump(out, f, ensure_ascii=False)
|
|
316
437
|
except Exception: # noqa: BLE001
|
|
317
438
|
return False
|
|
318
439
|
return True
|
|
@@ -396,3 +517,20 @@ def build_reason(result):
|
|
|
396
517
|
def build_residue_text(result):
|
|
397
518
|
"""上限に達して終わらせたときに控えへ残す1行(管制へ「途中で終わった」として届く)。"""
|
|
398
519
|
return '途中で終わった(便の終わりの関門の上限): ' + ' / '.join(result['reasons'])
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def build_residue_record(result):
|
|
523
|
+
"""控えに残す中身(人向けの1行+★管制が機械で読む形)。★純粋関数。
|
|
524
|
+
|
|
525
|
+
【なぜ機械で読む形も要るか(便 461be339)】上限に達して終わらせた便は、これまで
|
|
526
|
+
人向けの自由な文(`jobEndDetail`)にしか理由が乗らなかった。管制では「終わった」と
|
|
527
|
+
「途中で終わった」を機械で見分けられない(実地 7aa63a97 の残った穴)。
|
|
528
|
+
★新しい状態は作らず、終了の事実に `finishGate` の欄を足して届ける。
|
|
529
|
+
|
|
530
|
+
返り: {'text': 人向けの1行, 'reasons': 残っていた種類, 'counts': 止めた回数}
|
|
531
|
+
"""
|
|
532
|
+
return {
|
|
533
|
+
'text': build_residue_text(result),
|
|
534
|
+
'reasons': [k for k in GATE_KINDS if k in result['kinds']],
|
|
535
|
+
'counts': {k: int(result['counts'].get(k, 0)) for k in GATE_KINDS},
|
|
536
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_note": [
|
|
3
|
+
"★報告の「形」の決まりの正本。ここが1か所で、他に同じ決まりを書かない(便 461be339)。",
|
|
4
|
+
" 読む側は2つ。",
|
|
5
|
+
" ① 中継(src/relay.ts の parseReport / missingOf)… 報告を「届いた」と数える条件。",
|
|
6
|
+
" ★relay.ts の REQUIRED_REPORT_FIELDS / REPORT_OUTCOMES がこの値と同じであることを",
|
|
7
|
+
" test/report-shape-gate.test.ts が固定する(食い違えば CI が赤くなる)。",
|
|
8
|
+
" ② 便の終わりの関門(scripts/hooks/finish_gate.py)… 手番を閉じさせるかの判定。",
|
|
9
|
+
" ★この JSON を実行時に読む(同じフォルダに配られる)。読めなければ形の判定はしない。",
|
|
10
|
+
" ★cases は「同じ字面に同じ答えを出す」ことの答え合わせの正本。①②の両方の試験が流す。",
|
|
11
|
+
"★本文の言葉の印(「作成中」「TODO」など下書きの印)はここに置かない。",
|
|
12
|
+
" 報告の中身の言葉で完了を判定しない(えふさん確定 2026-09-23)ので、中継からは",
|
|
13
|
+
" 言葉の決まりに手が届かない形にしておく。下書きの印は関門だけの決まりで、",
|
|
14
|
+
" 正本は scripts/hooks/finish_gate.py の DRAFT_MARKERS。"
|
|
15
|
+
],
|
|
16
|
+
"version": 1,
|
|
17
|
+
"_requiredFields_note": "★並び順まで中継の missingOf と同じにする(答え合わせを順序ごと比べる)。",
|
|
18
|
+
"requiredFields": [
|
|
19
|
+
"instruction_id",
|
|
20
|
+
"site",
|
|
21
|
+
"outcome",
|
|
22
|
+
"created_at",
|
|
23
|
+
"evidence",
|
|
24
|
+
"body"
|
|
25
|
+
],
|
|
26
|
+
"validOutcomes": [
|
|
27
|
+
"pushed",
|
|
28
|
+
"no_change",
|
|
29
|
+
"blocked"
|
|
30
|
+
],
|
|
31
|
+
"_cases_note": "text をこの決まりで読んだとき、足りない物が missing と1つも違わないこと。",
|
|
32
|
+
"cases": [
|
|
33
|
+
{
|
|
34
|
+
"name": "★最終版(これが基本の形)",
|
|
35
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
36
|
+
"missing": []
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "no_change でも通る",
|
|
40
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: no_change\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
41
|
+
"missing": []
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "blocked でも通る",
|
|
45
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: blocked\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
46
|
+
"missing": []
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "空",
|
|
50
|
+
"text": "",
|
|
51
|
+
"missing": [
|
|
52
|
+
"instruction_id",
|
|
53
|
+
"site",
|
|
54
|
+
"outcome",
|
|
55
|
+
"created_at",
|
|
56
|
+
"evidence",
|
|
57
|
+
"body"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "空白と改行だけ",
|
|
62
|
+
"text": " \n \n",
|
|
63
|
+
"missing": [
|
|
64
|
+
"instruction_id",
|
|
65
|
+
"site",
|
|
66
|
+
"outcome",
|
|
67
|
+
"created_at",
|
|
68
|
+
"evidence",
|
|
69
|
+
"body"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "囲みが無い(古い自由形式)",
|
|
74
|
+
"text": "作業しました。\n直したファイル: src/relay.ts\n",
|
|
75
|
+
"missing": [
|
|
76
|
+
"instruction_id",
|
|
77
|
+
"site",
|
|
78
|
+
"outcome",
|
|
79
|
+
"created_at",
|
|
80
|
+
"evidence"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "★囲みが閉じていない(書きかけで落ちた形)",
|
|
85
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n\nやりました。",
|
|
86
|
+
"missing": [
|
|
87
|
+
"instruction_id",
|
|
88
|
+
"site",
|
|
89
|
+
"outcome",
|
|
90
|
+
"created_at",
|
|
91
|
+
"evidence"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "★囲みだけ・本文が空(実地 47bdfe00 の形)",
|
|
96
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n",
|
|
97
|
+
"missing": [
|
|
98
|
+
"body"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "本文が空白だけ",
|
|
103
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n \n ",
|
|
104
|
+
"missing": [
|
|
105
|
+
"body"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "instruction_id の欄が無い",
|
|
110
|
+
"text": "---\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
111
|
+
"missing": [
|
|
112
|
+
"instruction_id"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "site が空",
|
|
117
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: \noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
118
|
+
"missing": [
|
|
119
|
+
"site"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "outcome が決まった語でない",
|
|
124
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: やりました\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
125
|
+
"missing": [
|
|
126
|
+
"outcome"
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "outcome が空",
|
|
131
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: \ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
132
|
+
"missing": [
|
|
133
|
+
"outcome"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "created_at の欄が無い",
|
|
138
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
139
|
+
"missing": [
|
|
140
|
+
"created_at"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "evidence の欄が在るが1件も無い",
|
|
145
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n---\n\nやりました。",
|
|
146
|
+
"missing": [
|
|
147
|
+
"evidence"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "evidence の欄そのものが無い",
|
|
152
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\n---\n\nやりました。",
|
|
153
|
+
"missing": [
|
|
154
|
+
"evidence"
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "evidence が同じ行に書かれている(1件と数える)",
|
|
159
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence: PR https://example.invalid/1\n---\n\nやりました。",
|
|
160
|
+
"missing": []
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "囲みの中も本文も空",
|
|
164
|
+
"text": "---\n---\n",
|
|
165
|
+
"missing": [
|
|
166
|
+
"instruction_id",
|
|
167
|
+
"site",
|
|
168
|
+
"outcome",
|
|
169
|
+
"created_at",
|
|
170
|
+
"evidence",
|
|
171
|
+
"body"
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "★箇条書きが evidence の下でなければ evidence と数えない",
|
|
176
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nnote:\n - これは証拠ではない\n---\n\nやりました。",
|
|
177
|
+
"missing": [
|
|
178
|
+
"evidence"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "知らない欄が混じっていても通る",
|
|
183
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\nbranch: fix/x\npr_number: 256\n---\n\nやりました。",
|
|
184
|
+
"missing": []
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "欄の名前が大文字でも通る",
|
|
188
|
+
"text": "---\nInstruction_Id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
189
|
+
"missing": []
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "囲みの前に空行があっても通る",
|
|
193
|
+
"text": "\n\n---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
194
|
+
"missing": []
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"name": "先頭に BOM があっても通る",
|
|
198
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
199
|
+
"missing": []
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "★本文に下書きの言葉が残っていても形としては通る(中継は本文の言葉を読まない)",
|
|
203
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n# 便 915694ac 報告(作成中)\n\nこの見出しは最終版で差し替える。",
|
|
204
|
+
"missing": []
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "★続きの報告(詳しい中身を別の記録へ分けた形)も通る",
|
|
208
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n結論だけ書きます。詳しい中身は開発履歴の「7aa63a97 報告 1/3」〜「3/3」。",
|
|
209
|
+
"missing": []
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "読めない行が混じっていても欄の解釈は壊れない",
|
|
213
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n(この行は欄ではない)\n====\n---\n\nやりました。",
|
|
214
|
+
"missing": []
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"name": "「- 」だけの箇条書きは evidence と数えない",
|
|
218
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - \n---\n\nやりました。",
|
|
219
|
+
"missing": [
|
|
220
|
+
"evidence"
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "evidence の箇条書きがタブ区切りでも1件と数える",
|
|
225
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n\t-\tPR https://example.invalid/2\n---\n\nやりました。",
|
|
226
|
+
"missing": []
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"name": "同じ欄が2回あれば後が勝つ",
|
|
230
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: やりました\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\noutcome: pushed\n---\n\nやりました。",
|
|
231
|
+
"missing": []
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "欄が足りず本文も空(複数同時)",
|
|
235
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\noutcome: pushed\nevidence:\n - PR https://example.invalid/1\n---\n\n",
|
|
236
|
+
"missing": [
|
|
237
|
+
"site",
|
|
238
|
+
"created_at",
|
|
239
|
+
"body"
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
@@ -307,7 +307,7 @@ def finish_gate_block(data, env=None):
|
|
|
307
307
|
return finish_gate.build_reason(result)
|
|
308
308
|
if result['kinds']:
|
|
309
309
|
# 上限に達した=これ以上止めない。理由だけ残して終わらせる。
|
|
310
|
-
finish_gate.save_residue(command_id, finish_gate.
|
|
310
|
+
finish_gate.save_residue(command_id, finish_gate.build_residue_record(result), env)
|
|
311
311
|
return None
|
|
312
312
|
|
|
313
313
|
|
|
@@ -194,6 +194,16 @@ check('★上限に達したら止めない(便が終わる)', d['block'], F
|
|
|
194
194
|
check('★ただし「途中で終わった」と分かる(種類が残る)', d['kinds'], ['report'])
|
|
195
195
|
truthy('控えの文に理由が入る', '途中で終わった' in finish_gate.build_residue_text(d))
|
|
196
196
|
|
|
197
|
+
print('--- ★控えは「管制が機械で読める形」でも残す(便 461be339) ---')
|
|
198
|
+
rec = finish_gate.build_residue_record(d)
|
|
199
|
+
truthy('人向けの1行は今までどおり', '途中で終わった' in rec['text'])
|
|
200
|
+
check('★残っていた種類が入る', rec['reasons'], ['report'])
|
|
201
|
+
check('★止めた回数が3種類ぶん入る', rec['counts'], {'report': 3, 'pr': 0, 'background': 0})
|
|
202
|
+
rec3 = finish_gate.build_residue_record(
|
|
203
|
+
finish_gate.decide({'report': 'a', 'pr': 'b', 'background': 'c'},
|
|
204
|
+
{'report': 3, 'pr': 3, 'background': 3}, cap=3))
|
|
205
|
+
check('3つ残ったまま終わったら3つとも入る', rec3['reasons'], ['report', 'pr', 'background'])
|
|
206
|
+
|
|
197
207
|
check('残りが無ければ止めない', finish_gate.decide({}, {}, cap=3)['block'], False)
|
|
198
208
|
check('残りが無ければ種類も空', finish_gate.decide({}, {'report': 1}, cap=3)['kinds'], [])
|
|
199
209
|
|
|
@@ -291,7 +301,10 @@ check('その後は止めない',
|
|
|
291
301
|
rp = finish_gate.residue_path(JOB, ENV)
|
|
292
302
|
truthy('理由の控えが残る', os.path.isfile(rp))
|
|
293
303
|
with open(rp, encoding='utf-8') as f:
|
|
294
|
-
|
|
304
|
+
got = json.load(f)
|
|
305
|
+
truthy('控えに「途中で終わった」と入っている', '途中で終わった' in got['text'])
|
|
306
|
+
check('★控えの種類を中継が機械で読める', got.get('reasons'), ['report'])
|
|
307
|
+
check('★控えの回数は上限ちょうど', (got.get('counts') or {}).get('report'), finish_gate.CAP_PER_KIND)
|
|
295
308
|
|
|
296
309
|
print('--- ★便の本文が「合流は人が行う」なら PR を見ない ---')
|
|
297
310
|
clear_counts()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""報告の「形」の判定が、決まりの正本どおりであることの検査(便 461be339)。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_report_shape.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【なぜ要るか】形の決まりは2か所から読まれる。
|
|
8
|
+
① 中継(`src/relay.ts`)… 報告を「届いた」と数える条件
|
|
9
|
+
② 便の終わりの関門(`finish_gate.py`)… 手番を閉じさせるかの判定
|
|
10
|
+
同じ決まりを2か所に書くと、片方だけ直して食い違う(=関門は差し戻すのに中継は届いたと数える、
|
|
11
|
+
またはその逆)。だから決まりは `report-shape.json` の1か所だけに置き、
|
|
12
|
+
**同じ字面に同じ答えを出すこと**を両方の試験で固定する。
|
|
13
|
+
★中継の側は `test/report-shape-gate.test.ts` が、この同じ `cases` を流して突き合わせる。
|
|
14
|
+
|
|
15
|
+
【ここで固定すること】
|
|
16
|
+
(A) 正本が読めること(配り忘れ・壊れた JSON に気づける)
|
|
17
|
+
(B) 正本の `cases` の答えと、関門の判定が1件も違わないこと
|
|
18
|
+
(C) 正本が読めないときは形の判定をしない(fail open=関門が足される前と同じ動き)
|
|
19
|
+
(D) ★下書きの印(本文の言葉)は正本に載せない
|
|
20
|
+
=中継からは言葉の決まりに手が届かない(えふさん確定 2026-09-23)
|
|
21
|
+
(E) 形と下書きの印は別の理由として返る
|
|
22
|
+
|
|
23
|
+
★空振り確認(隔離した worktree の中だけ):
|
|
24
|
+
・`report-shape.json` の `requiredFields` から1つ消す → (B) が落ちる
|
|
25
|
+
・`report_shape_missing` の outcome の確かめを外す → (B) が落ちる
|
|
26
|
+
"""
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
32
|
+
|
|
33
|
+
import finish_gate # noqa: E402
|
|
34
|
+
|
|
35
|
+
failures = []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check(label, got, want):
|
|
39
|
+
ok = got == want
|
|
40
|
+
if not ok:
|
|
41
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
42
|
+
print(f'{"OK " if ok else "NG "} {label:70} {str(got)[:40]}')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def truthy(label, got):
|
|
46
|
+
ok = bool(got)
|
|
47
|
+
if not ok:
|
|
48
|
+
failures.append(f'{label}: got={got!r} want=真')
|
|
49
|
+
print(f'{"OK " if ok else "NG "} {label:70} {str(got)[:40]}')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
print('=== (A) 形の決まりの正本が読めるか ===')
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
path = finish_gate.shape_spec_path()
|
|
56
|
+
check('正本の置き場は自分と同じフォルダ', os.path.basename(path), 'report-shape.json')
|
|
57
|
+
truthy('正本が在る', os.path.isfile(path))
|
|
58
|
+
spec = finish_gate.load_shape_spec()
|
|
59
|
+
truthy('正本が読める', spec)
|
|
60
|
+
check('★関門は読み込み済みの正本を使う', finish_gate.SHAPE_SPEC is not None, True)
|
|
61
|
+
check('足りない物の並び順まで決まっている',
|
|
62
|
+
spec['requiredFields'],
|
|
63
|
+
['instruction_id', 'site', 'outcome', 'created_at', 'evidence', 'body'])
|
|
64
|
+
check('outcome に書ける語', spec['validOutcomes'], ['pushed', 'no_change', 'blocked'])
|
|
65
|
+
truthy('答え合わせの cases が在る', isinstance(spec.get('cases'), list) and spec['cases'])
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
print(f'\n=== (B) 正本の cases({len(spec["cases"])}件)の答えと1件も違わない ===')
|
|
69
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
for case in spec['cases']:
|
|
71
|
+
check(case['name'][:68], finish_gate.report_shape_missing(case['text']), case['missing'])
|
|
72
|
+
|
|
73
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
print('\n=== (C) 正本が読めないときは形の判定をしない(退行しない) ===')
|
|
75
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
check('正本が無い → 足りない物は並べない', finish_gate.report_shape_missing('やりました。', {}), [])
|
|
77
|
+
check('正本が壊れていても落ちない', finish_gate.load_shape_spec('/nope/report-shape.json'), None)
|
|
78
|
+
check('requiredFields が無い正本は使わない',
|
|
79
|
+
finish_gate.load_shape_spec(os.path.join(os.path.dirname(path), 'safe_verbs.json')), None)
|
|
80
|
+
good = spec['cases'][0]['text']
|
|
81
|
+
check('★正本が読めなくても「報告が無い」は今までどおり止める',
|
|
82
|
+
bool(finish_gate.report_residue(None, {})), True)
|
|
83
|
+
check('★正本が読めないときは形で止めない(fail open)',
|
|
84
|
+
finish_gate.report_residue(good, {}), None)
|
|
85
|
+
|
|
86
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
print('\n=== (D) ★下書きの印(本文の言葉)は正本に載せない ===')
|
|
88
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
raw = open(path, encoding='utf-8').read()
|
|
90
|
+
body_rules = json.loads(raw)
|
|
91
|
+
for marker in ('作成中', '下書き', '差し替える', 'あとで書く', 'TODO', 'TBD'):
|
|
92
|
+
check(f'正本の決まりに「{marker}」が入っていない',
|
|
93
|
+
any(marker in json.dumps(body_rules.get(k), ensure_ascii=False)
|
|
94
|
+
for k in ('requiredFields', 'validOutcomes')),
|
|
95
|
+
False)
|
|
96
|
+
truthy('下書きの印の正本は関門の側にある', finish_gate.DRAFT_MARKERS)
|
|
97
|
+
|
|
98
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
print('\n=== (E) 形と下書きの印は別の理由として返る ===')
|
|
100
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
draft = next(c for c in spec['cases'] if '下書きの言葉' in c['name'])
|
|
102
|
+
check('★下書きの言葉が残った報告は、形としては整っている',
|
|
103
|
+
finish_gate.report_shape_missing(draft['text']), [])
|
|
104
|
+
r = finish_gate.report_residue(draft['text'])
|
|
105
|
+
truthy('それでも関門は下書きとして差し戻す', r)
|
|
106
|
+
truthy(' 理由は「下書き」だと分かる', '下書き' in (r or ''))
|
|
107
|
+
broken = next(c for c in spec['cases'] if c['missing'] == ['body'])
|
|
108
|
+
r2 = finish_gate.report_residue(broken['text'])
|
|
109
|
+
truthy('形が整っていない報告は「形」の理由で差し戻す', r2)
|
|
110
|
+
truthy(' 理由は「形が整っていない」だと分かる', '形が整っていない' in (r2 or ''))
|
|
111
|
+
truthy(' 足りない物の名前が入る', '本文' in (r2 or ''))
|
|
112
|
+
check('★最終版は差し戻さない', finish_gate.report_residue(good), None)
|
|
113
|
+
check('本文の取り出しは囲みの下だけ', finish_gate.report_body(good).startswith('やりました'), True)
|
|
114
|
+
check('囲みが無ければ全文が本文', finish_gate.report_body('ただの文'), 'ただの文')
|
|
115
|
+
|
|
116
|
+
print('\n' + '=' * 72)
|
|
117
|
+
if failures:
|
|
118
|
+
print(f'NG {len(failures)} 件')
|
|
119
|
+
for f in failures:
|
|
120
|
+
print(' ' + f)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
print('すべて期待どおり')
|
package/scripts/update-guard.sh
CHANGED
|
@@ -438,7 +438,11 @@ INSTALL_FILES=(
|
|
|
438
438
|
hook_datalink.py # 各フックが使う管制との連絡線
|
|
439
439
|
finish_gate.py # stop_hook が使う「便の終わりの関門」の判定(便 7aa63a97)
|
|
440
440
|
)
|
|
441
|
-
|
|
441
|
+
# ★番人と同居させる設定(任意・あれば一緒に置く)。この一覧は sales-template/setup.sh の
|
|
442
|
+
# GUARD_CONFIGS と【同じ集合】にすること(test/setup-installer.test.ts が同一を固定する)。
|
|
443
|
+
# report-shape.json … 報告の「形」の決まりの正本(finish_gate.py が実行時に読む・便 461be339)。
|
|
444
|
+
# 配り忘れると関門が形を見られない(fail open=書きかけを差し戻せない)。
|
|
445
|
+
INSTALL_CONFIGS=(safe_verbs.json network_allow.json report-shape.json)
|
|
442
446
|
ENTRYPOINTS=(cc2_guard.py permission_request_hook.py stop_hook.py notification_hook.py)
|
|
443
447
|
|
|
444
448
|
# ①-0 見本と「配る中身」がズレていないか機械照合する(今回と同じ欠落事故の再発防止=根治)。
|