@ngockhoale/ukit 3.4.52 → 3.4.54
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 +17 -0
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +48 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +48 -3
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +3 -1
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.54 - 2026-10-22
|
|
6
|
+
|
|
7
|
+
**Completion Gate: a stated prohibition is not an operation order (field false-block).**
|
|
8
|
+
- Fix: `classifyTaskSemantics` (src + shipped `route-resolver.mjs` mirror) treated "Production deploy/restart vẫn bị cấm", "production deploy vẫn bị chặn", "forbidden deploy/publish" and a bare list item "Production restart." as actionable runtime operations, so an audit/report prompt became MIXED + service-runtime and the Stop gate demanded `operation-verification` for a restart nobody ordered.
|
|
9
|
+
- New guard `operationNotOrdered` applies to the git/runtime operation checks only: a prohibition attached to the operation, or a bare production-operation noun item, orders nothing. Lift/past markers ("was prohibited, owner now allows", "even though it is not allowed", "khong bi chan") keep the clause an order. Real orders (restart + verify, deploy to production, fix + restart) are unchanged.
|
|
10
|
+
- Regression: `tests/hooks/prohibitionNotOrder.test.js` (78 cases across src + installed copies) with the real field prompt fixture. Ledger, gates and release logic untouched.
|
|
11
|
+
- Verified offline on macOS/Node 22; OMP/Claude Code coverage is classifier replay, not live host.
|
|
12
|
+
|
|
13
|
+
## 3.4.53 - 2026-10-22
|
|
14
|
+
|
|
15
|
+
**Stop-level closure of the UNPROVEN verdict (follow-up to 3.4.52).**
|
|
16
|
+
- `recoveryInstruction`: a non-current `unproven` verdict no longer yields "verdict is stale, re-run at current head"; it names the real gap (masked pipe, exit status unproven) and never "fix the failure".
|
|
17
|
+
- `stop-coordinator`: the `verify:pass` predicate and `goalVerificationVerdict` treat `unproven` as unmet but report "unproven (exit status not proven)", not a verified failure; `unproven` never counts as a pass.
|
|
18
|
+
- New Stop-level regressions (`tests/hooks/executionLedgerStopLifecycle.test.js`, GC-03e): UNPROVEN neither failure nor PASS; exhausted retry budget ends visibly without a recovery loop or write demand; same-task same-command PASS clears its own blocker, a different command / UNPROVEN rerun / another task does not; a superseded failure stops producing recovery text.
|
|
19
|
+
- Note: `tests/handoff/**` is excluded from `vitest`; run `tests/handoff/goalCompletion.test.js` with `node --test` (vitest exits 1 with "No test files found").
|
|
20
|
+
- Verified offline on macOS/Node 22; no live host proof.
|
|
21
|
+
|
|
5
22
|
## 3.4.52 - 2026-10-22
|
|
6
23
|
|
|
7
24
|
**Verification-loop ledger integrity (post-3.4.51 follow-up).**
|
|
@@ -82,6 +82,17 @@ entries:
|
|
|
82
82
|
validation: [manual, test:tests/hooks/executionLedger.test.js, test:tests/hooks/verificationExitProof.test.js]
|
|
83
83
|
archive_policy: replace-with-snapshot
|
|
84
84
|
notes: evidence-based long-run field audit; current reproductions are distinct from incident attribution
|
|
85
|
+
- id: root-62h-field-regression-crosscheck
|
|
86
|
+
path: UKIT_62H_FIELD_REGRESSION_CROSSCHECK.md
|
|
87
|
+
class: canonical
|
|
88
|
+
audience: [owner, maintainer]
|
|
89
|
+
owner: runtime
|
|
90
|
+
source_of_truth: UKIT_62H_FIELD_REGRESSION_CROSSCHECK.md
|
|
91
|
+
merge_strategy: none
|
|
92
|
+
load_policy: on-demand
|
|
93
|
+
validation: [manual, test:tests/hooks/prohibitionNotOrder.test.js]
|
|
94
|
+
archive_policy: replace-with-snapshot
|
|
95
|
+
notes: BAGuide 62h field cross-check; observed/reproduced/hypothesis/unknown labelled, gate false-block root cause
|
|
85
96
|
- id: registry-self
|
|
86
97
|
path: manifests/documentation.yaml
|
|
87
98
|
class: canonical
|
package/package.json
CHANGED
|
@@ -265,6 +265,22 @@ export function isVerificationOnlyRequest(promptText) {
|
|
|
265
265
|
&& !clauses.some((clause) => CHANGE_LEAD.test(clause));
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// "Deploy and restart are banned", "Forbidden: deploy", "deploy: banned" name operations to prohibit them. The clause split on
|
|
269
|
+
// "and"/commas would separate the ban from the first operation, so a banned operation list is removed before splitting.
|
|
270
|
+
function neutralizeBannedLists(text) {
|
|
271
|
+
const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
|
|
272
|
+
const LIST = `${OP}(?:\\s*(?:,|/|\\band\\b|\\bor\\b|\\bva\\b|\\bhoac\\b|\\balso\\b)\\s*${OP})*`;
|
|
273
|
+
const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
|
|
274
|
+
const LIFT = /\b(?:was|were|previously|before|formerly|no\s+longer|even\s+though|although|though|now\s+allows?|now\s+permits?|owner\s+allows?|khong\s+bi|da\s+duoc\s+phep|except)\b/;
|
|
275
|
+
const span = new RegExp(`\\b(?:(?:forbidden|prohibited|banned)\\s*:\\s*${LIST}|${LIST}\\s*:?\\s*(?:(?:is|are|remains?|van|con)\\s+)?(?:both\\s+)?${BAN})\\b`, 'g');
|
|
276
|
+
return text.replace(span, (m, offset, whole) => {
|
|
277
|
+
const start = Math.max(whole.lastIndexOf('.', offset), whole.lastIndexOf(';', offset), whole.lastIndexOf('\n', offset)) + 1;
|
|
278
|
+
const stops = [whole.indexOf('.', offset + m.length), whole.indexOf(';', offset + m.length), whole.indexOf('\n', offset + m.length)].filter((i) => i >= 0);
|
|
279
|
+
const end = stops.length ? Math.min(...stops) : whole.length;
|
|
280
|
+
return LIFT.test(whole.slice(start, end)) ? m : ' ';
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
268
284
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
269
285
|
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
270
286
|
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
@@ -272,7 +288,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
272
288
|
const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
|
|
273
289
|
&& /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
|
|
274
290
|
? { kind: 'conditional-release', preflight } : null;
|
|
275
|
-
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
|
|
291
|
+
const text = neutralizeBannedLists(neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()));
|
|
276
292
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
277
293
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
278
294
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
@@ -293,11 +309,40 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
293
309
|
// actionable clause (negated/deferred/audit clauses are already filtered out). Git and
|
|
294
310
|
// runtime are separate domains with separate proof; a prompt naming both stays a coding
|
|
295
311
|
// task rather than being half-verified. A code-change verb anywhere keeps write evidence.
|
|
296
|
-
|
|
312
|
+
// A clause that STATES an operation is prohibited ("production deploy vẫn bị cấm", "forbidden deploy") or merely lists a
|
|
313
|
+
// production operation as a bare noun item ("* Production restart.") names it; it does not order it.
|
|
314
|
+
const operationNotOrdered = (clause) => {
|
|
315
|
+
const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
|
|
316
|
+
const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
|
|
317
|
+
// A lift or past marker ("was prohibited, owner now allows", "even though it is not allowed", "khong bi chan")
|
|
318
|
+
// means the prohibition is history or lifted: the clause still orders the operation.
|
|
319
|
+
if (/\b(?:was|were|previously|before|formerly|no\s+longer|even\s+though|although|though|now\s+allows?|now\s+permits?|owner\s+allows?|khong\s+bi|da\s+duoc\s+phep|except)\b/.test(clause)) return false;
|
|
320
|
+
if (/^[\s*+•>\d.)-]*(?:production|prod|staging)\s+(?:restart|redeploy|deploy(?:ment)?)\s*$/.test(clause)) return true;
|
|
321
|
+
// Evaluated per operation occurrence: the clause orders nothing only when EVERY operation in it is prohibited.
|
|
322
|
+
// The ban must attach to its own operation: no parenthesis, dash, colon, conjunction, relative pronoun or second operation in between.
|
|
323
|
+
const gap = `(?:(?!\\b(?:which|that|who|where|when|if|and|but|then)\\b|${OP})[^.;,()\\-:]){0,40}?`;
|
|
324
|
+
const banAfter = new RegExp(`^${gap}\\b(?:(?:is|are|remains?|van|con)\\s+)?${BAN}\\b(?!\\s+(?:to|from)\\b)`);
|
|
325
|
+
// "deploy/restart is forbidden", "deploy and restart are banned": operations joined as a list share one ban.
|
|
326
|
+
const joined = new RegExp(`^\\s*(?:/|,|and|or|va|hoac)\\s*${OP}\\b`);
|
|
327
|
+
const bannedAfter = (rest) => {
|
|
328
|
+
if (banAfter.test(rest)) return true;
|
|
329
|
+
const j = joined.exec(rest);
|
|
330
|
+
return j ? bannedAfter(rest.slice(j[0].length)) : false;
|
|
331
|
+
};
|
|
332
|
+
const banBefore = /\b(?:forbidden|prohibited|banned)\s+(?:(?:production|prod)\s+)?$/;
|
|
333
|
+
let total = 0;
|
|
334
|
+
let banned = 0;
|
|
335
|
+
for (const match of clause.matchAll(new RegExp(`\\b${OP}\\b`, 'g'))) {
|
|
336
|
+
total += 1;
|
|
337
|
+
if (bannedAfter(clause.slice(match.index + match[0].length)) || banBefore.test(clause.slice(0, match.index))) banned += 1;
|
|
338
|
+
}
|
|
339
|
+
return total > 0 && banned === total;
|
|
340
|
+
};
|
|
341
|
+
const gitOperation = actionable.some((clause) => !operationNotOrdered(clause) && /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
|
|
297
342
|
// 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
|
|
298
343
|
// when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
|
|
299
344
|
// "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
|
|
300
|
-
const runtimeOperation = actionable.some((clause) => /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
345
|
+
const runtimeOperation = actionable.some((clause) => !operationNotOrdered(clause) && /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
301
346
|
&& !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
|
|
302
347
|
const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
|
|
303
348
|
const operation = Boolean(nativeDomain);
|
|
@@ -1162,6 +1162,22 @@ export function isVerificationOnlyRequest(promptText) {
|
|
|
1162
1162
|
&& !clauses.some((clause) => CHANGE_LEAD.test(clause));
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
+
// "Deploy and restart are banned", "Forbidden: deploy", "deploy: banned" name operations to prohibit them. The clause split on
|
|
1166
|
+
// "and"/commas would separate the ban from the first operation, so a banned operation list is removed before splitting.
|
|
1167
|
+
function neutralizeBannedLists(text) {
|
|
1168
|
+
const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
|
|
1169
|
+
const LIST = `${OP}(?:\\s*(?:,|/|\\band\\b|\\bor\\b|\\bva\\b|\\bhoac\\b|\\balso\\b)\\s*${OP})*`;
|
|
1170
|
+
const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
|
|
1171
|
+
const LIFT = /\b(?:was|were|previously|before|formerly|no\s+longer|even\s+though|although|though|now\s+allows?|now\s+permits?|owner\s+allows?|khong\s+bi|da\s+duoc\s+phep|except)\b/;
|
|
1172
|
+
const span = new RegExp(`\\b(?:(?:forbidden|prohibited|banned)\\s*:\\s*${LIST}|${LIST}\\s*:?\\s*(?:(?:is|are|remains?|van|con)\\s+)?(?:both\\s+)?${BAN})\\b`, 'g');
|
|
1173
|
+
return text.replace(span, (m, offset, whole) => {
|
|
1174
|
+
const start = Math.max(whole.lastIndexOf('.', offset), whole.lastIndexOf(';', offset), whole.lastIndexOf('\n', offset)) + 1;
|
|
1175
|
+
const stops = [whole.indexOf('.', offset + m.length), whole.indexOf(';', offset + m.length), whole.indexOf('\n', offset + m.length)].filter((i) => i >= 0);
|
|
1176
|
+
const end = stops.length ? Math.min(...stops) : whole.length;
|
|
1177
|
+
return LIFT.test(whole.slice(start, end)) ? m : ' ';
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1165
1181
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1166
1182
|
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
1167
1183
|
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
@@ -1169,7 +1185,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1169
1185
|
const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
|
|
1170
1186
|
&& /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
|
|
1171
1187
|
? { kind: 'conditional-release', preflight } : null;
|
|
1172
|
-
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
|
|
1188
|
+
const text = neutralizeBannedLists(neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()));
|
|
1173
1189
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1174
1190
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1175
1191
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
@@ -1190,11 +1206,40 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1190
1206
|
// actionable clause (negated/deferred/audit clauses are already filtered out). Git and
|
|
1191
1207
|
// runtime are separate domains with separate proof; a prompt naming both stays a coding
|
|
1192
1208
|
// task rather than being half-verified. A code-change verb anywhere keeps write evidence.
|
|
1193
|
-
|
|
1209
|
+
// A clause that STATES an operation is prohibited ("production deploy vẫn bị cấm", "forbidden deploy") or merely lists a
|
|
1210
|
+
// production operation as a bare noun item ("* Production restart.") names it; it does not order it.
|
|
1211
|
+
const operationNotOrdered = (clause) => {
|
|
1212
|
+
const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
|
|
1213
|
+
const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
|
|
1214
|
+
// A lift or past marker ("was prohibited, owner now allows", "even though it is not allowed", "khong bi chan")
|
|
1215
|
+
// means the prohibition is history or lifted: the clause still orders the operation.
|
|
1216
|
+
if (/\b(?:was|were|previously|before|formerly|no\s+longer|even\s+though|although|though|now\s+allows?|now\s+permits?|owner\s+allows?|khong\s+bi|da\s+duoc\s+phep|except)\b/.test(clause)) return false;
|
|
1217
|
+
if (/^[\s*+•>\d.)-]*(?:production|prod|staging)\s+(?:restart|redeploy|deploy(?:ment)?)\s*$/.test(clause)) return true;
|
|
1218
|
+
// Evaluated per operation occurrence: the clause orders nothing only when EVERY operation in it is prohibited.
|
|
1219
|
+
// The ban must attach to its own operation: no parenthesis, dash, colon, conjunction, relative pronoun or second operation in between.
|
|
1220
|
+
const gap = `(?:(?!\\b(?:which|that|who|where|when|if|and|but|then)\\b|${OP})[^.;,()\\-:]){0,40}?`;
|
|
1221
|
+
const banAfter = new RegExp(`^${gap}\\b(?:(?:is|are|remains?|van|con)\\s+)?${BAN}\\b(?!\\s+(?:to|from)\\b)`);
|
|
1222
|
+
// "deploy/restart is forbidden", "deploy and restart are banned": operations joined as a list share one ban.
|
|
1223
|
+
const joined = new RegExp(`^\\s*(?:/|,|and|or|va|hoac)\\s*${OP}\\b`);
|
|
1224
|
+
const bannedAfter = (rest) => {
|
|
1225
|
+
if (banAfter.test(rest)) return true;
|
|
1226
|
+
const j = joined.exec(rest);
|
|
1227
|
+
return j ? bannedAfter(rest.slice(j[0].length)) : false;
|
|
1228
|
+
};
|
|
1229
|
+
const banBefore = /\b(?:forbidden|prohibited|banned)\s+(?:(?:production|prod)\s+)?$/;
|
|
1230
|
+
let total = 0;
|
|
1231
|
+
let banned = 0;
|
|
1232
|
+
for (const match of clause.matchAll(new RegExp(`\\b${OP}\\b`, 'g'))) {
|
|
1233
|
+
total += 1;
|
|
1234
|
+
if (bannedAfter(clause.slice(match.index + match[0].length)) || banBefore.test(clause.slice(0, match.index))) banned += 1;
|
|
1235
|
+
}
|
|
1236
|
+
return total > 0 && banned === total;
|
|
1237
|
+
};
|
|
1238
|
+
const gitOperation = actionable.some((clause) => !operationNotOrdered(clause) && /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
|
|
1194
1239
|
// 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
|
|
1195
1240
|
// when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
|
|
1196
1241
|
// "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
|
|
1197
|
-
const runtimeOperation = actionable.some((clause) => /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
1242
|
+
const runtimeOperation = actionable.some((clause) => !operationNotOrdered(clause) && /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
1198
1243
|
&& !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
|
|
1199
1244
|
const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
|
|
1200
1245
|
const operation = Boolean(nativeDomain);
|
|
@@ -3483,7 +3483,9 @@ function recoveryInstruction(missingEvidence, ledger = {}, routeSummary = {}, {
|
|
|
3483
3483
|
if (!instruction && ledger.verdict && typeof ledger.verdict === 'object') {
|
|
3484
3484
|
if (ledger.verdict.kind === 'check-only') {
|
|
3485
3485
|
instruction = 'check-only does not satisfy this lane; verify on the real surface (run the feature / inspect the artifact).';
|
|
3486
|
-
} else {
|
|
3486
|
+
} else if (ledger.verdict.kind !== 'unproven') {
|
|
3487
|
+
// An UNPROVEN verdict never satisfied the gate, so its currency is irrelevant: the
|
|
3488
|
+
// gap is the missing exit status (named below), not a stale head.
|
|
3487
3489
|
const currency = verdictCurrent(ledger.verdict, { cwd });
|
|
3488
3490
|
if (currency.current !== true) {
|
|
3489
3491
|
const sha = ledger.verdict.headSha ? String(ledger.verdict.headSha).slice(0, 12) : 'unknown';
|
|
@@ -1065,9 +1065,14 @@ async function evalVerifyPass(projectRoot) {
|
|
|
1065
1065
|
for (let i = receipts.length - 1; i >= 0; i -= 1) {
|
|
1066
1066
|
const r = receipts[i];
|
|
1067
1067
|
if (!r || typeof r !== 'object' || r.kind !== 'verification') continue;
|
|
1068
|
-
if (r.success === true && r.verdict?.kind !== 'failed') {
|
|
1068
|
+
if (r.success === true && r.verdict?.kind !== 'failed' && r.verdict?.kind !== 'unproven') {
|
|
1069
1069
|
return { pass: true, detail: 'last verification passed' };
|
|
1070
1070
|
}
|
|
1071
|
+
// A masked-pipe run (exit status unproven) is not a verified failure: same fail-closed
|
|
1072
|
+
// result, but the detail must say what is actually missing.
|
|
1073
|
+
if (r.exitStatusUnproven === true || r.verdict?.kind === 'unproven') {
|
|
1074
|
+
return { pass: false, detail: 'last verification unproven (exit status not proven)' };
|
|
1075
|
+
}
|
|
1071
1076
|
return { pass: false, detail: 'last verification failed' };
|
|
1072
1077
|
}
|
|
1073
1078
|
return { pass: false, detail: 'no verification receipt recorded' };
|
|
@@ -1136,6 +1141,7 @@ function goalVerificationVerdict(ledger) {
|
|
|
1136
1141
|
const verdict = typeof ledger.verdict === 'string' ? ledger.verdict.trim().toLowerCase() : null;
|
|
1137
1142
|
if (verdict === 'pass' || verdict === 'passed' || verdict === 'verified') return { pass: true, detail: 'verification verdict: pass' };
|
|
1138
1143
|
if (verdict === 'fail' || verdict === 'failed') return { pass: false, detail: 'verification verdict: fail' };
|
|
1144
|
+
if (verdict === 'unproven') return { pass: false, detail: 'verification verdict: unproven (exit status not proven)' };
|
|
1139
1145
|
if (ledger.verified === true) return { pass: true, detail: 'verification: verified' };
|
|
1140
1146
|
if (ledger.verified === false) return { pass: false, detail: 'verification: not verified' };
|
|
1141
1147
|
if (typeof ledger.verify === 'string' && ledger.verify.trim().toLowerCase() === 'pass') return { pass: true, detail: 'verify: pass' };
|