@ngockhoale/ukit 3.4.53 → 3.4.55

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.55 - 2026-10-22
6
+
7
+ **Completion Gate: preference, conditional-future and prohibition-list text is not an order (field false-block).**
8
+ - Field case: an audit prompt saying "Ưu tiên configuration change" (preference) and "Nếu chỉ cần configuration change:" (future branch) was classified `CONFIGURATION`/external, so the Stop gate demanded `operation-verification` for a config edit nobody ordered (task `task-0c7425cb…`, `accepted-count=0`). A follow-up prompt listing what an authorization does NOT allow as bullets ("không cho phép: - restart …") plus "Negative controls: … chỉnh config/restart/deploy" was classified `MIXED`/service-runtime.
9
+ - Fix (`neutralizeNonOrders` in all three classifier copies: `src/core/executionContracts.js`, `route-resolver.mjs` mirror, `skill-router.sh` inline): a config preference, a prohibition-header bullet list (bounded to its own sentence and ended by a blank line) and "negative/positive controls" test-case bullets are neutralized; the audit/inspect/review lead filter now tolerates bullet prefixes. Gate, ledger and loop-breaker logic untouched; genuine config/restart/deploy orders and mixed implementation tasks keep their obligations (negative controls in tests).
10
+ - Regression: `tests/hooks/configPreferenceNotOrder.test.js` (src + installed + hook copy; real prompt fixtures `concurrency-audit-config-preference-prompt.txt`, `authorization-boundary-bullets-prompt.txt`). Independent review found one false negative (header regex swallowed the whole line before it); fixed and pinned.
11
+ - Housekeeping: `docs/STATUS.md` rotated under its 150-line budget (verbatim archive), concurrency audit report registered in `manifests/documentation.yaml`.
12
+ - Verified offline on macOS/Node 22; classifier replay, not a live OMP/Claude Code/Codex session; Windows/Linux not run.
13
+
14
+ ## 3.4.54 - 2026-10-22
15
+
16
+ **Completion Gate: a stated prohibition is not an operation order (field false-block).**
17
+ - 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.
18
+ - 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.
19
+ - Regression: `tests/hooks/prohibitionNotOrder.test.js` (78 cases across src + installed copies) with the real field prompt fixture. Ledger, gates and release logic untouched.
20
+ - Verified offline on macOS/Node 22; OMP/Claude Code coverage is classifier replay, not live host.
21
+
5
22
  ## 3.4.53 - 2026-10-22
6
23
 
7
24
  **Stop-level closure of the UNPROVEN verdict (follow-up to 3.4.52).**
@@ -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
@@ -429,6 +440,17 @@ entries:
429
440
  validation: [manual]
430
441
  archive_policy: owner-decides
431
442
  notes: bounded investigation evidence; extension transition remains OPEN
443
+ - id: docs-analysis-agent-concurrency-optimization
444
+ path: docs/analysis/UKIT_AGENT_CONCURRENCY_OPTIMIZATION.md
445
+ class: canonical
446
+ audience: [owner, maintainer]
447
+ owner: runtime
448
+ source_of_truth: docs/analysis/UKIT_AGENT_CONCURRENCY_OPTIMIZATION.md
449
+ merge_strategy: none
450
+ load_policy: on-demand
451
+ validation: [manual]
452
+ archive_policy: replace-with-snapshot
453
+ notes: Phase A read-only concurrency audit; provisional metrics labelled, pilot not run
432
454
  - id: docs-analysis-subagent-cost-audit
433
455
  path: docs/analysis/SUBAGENT-COST-AUDIT.md
434
456
  class: archive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.53",
3
+ "version": "3.4.55",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -211,10 +211,17 @@ function neutralizeNonOrders(text) {
211
211
  let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
212
212
  if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
213
213
  return source
214
+ // A list item that DESCRIBES test cases ("Negative controls: yêu cầu thực sự chỉnh config/restart/deploy ...") is
215
+ // specification of the work, not an order; it runs to the next bullet or blank line.
216
+ .replace(/^[ \t]*(?:[-*+\u2022]|\d+[.)])[ \t]*(?:negative|positive)\s+controls?\b[^\n]*(?:\n(?![ \t]*(?:[-*+\u2022]|\d+[.)])|[ \t]*\n)[^\n]*)*/gm, ' ')
217
+ // A header that negates the list under it ("Authorization ... không cho phép:", "Prohibitions:", "not allowed:")
218
+ // makes every bullet an item of the prohibition, not an order.
219
+ .replace(/(?:^|(?<=[.;][ \t]))[^.;\n]*\b(?:khong\s+(?:cho\s+phep|duoc|lam)|(?:does|do|did|is|are)\s+not\s+(?:allow|permit|authori[sz]e)\w*|not\s+(?:allowed|permitted|authori[sz]ed)|prohibitions?|prohibited(?:\s+actions?)?|forbidden(?:\s+actions?)?|banned|must\s+not\s+do)\s*:[ \t]*(?:\n[ \t]*(?:[-*+\u2022]|\d+[.)])[^\n]*)+/gm, ' ')
214
220
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
215
221
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
216
222
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
217
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
223
+ .replace(/\b(?:uu tien|prefer(?:red)?)\s+(?:a\s+|an\s+)?(?:configuration|config|cau hinh)\s+(?:change|changes|update|thay doi)\b/g, ' ')
224
+ .replace(/\b(?:neu\s+(?:chi\s+)?(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
218
225
  .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
219
226
  .replace(/\bchinh\s+(?:xac|sach)\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b|\b(?:diem|cho|muc|phan)\s+(?:phai|can|nen)\s+(?:sua|fix)\b/g, ' ')
220
227
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
@@ -265,6 +272,22 @@ export function isVerificationOnlyRequest(promptText) {
265
272
  && !clauses.some((clause) => CHANGE_LEAD.test(clause));
266
273
  }
267
274
 
275
+ // "Deploy and restart are banned", "Forbidden: deploy", "deploy: banned" name operations to prohibit them. The clause split on
276
+ // "and"/commas would separate the ban from the first operation, so a banned operation list is removed before splitting.
277
+ function neutralizeBannedLists(text) {
278
+ const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
279
+ const LIST = `${OP}(?:\\s*(?:,|/|\\band\\b|\\bor\\b|\\bva\\b|\\bhoac\\b|\\balso\\b)\\s*${OP})*`;
280
+ const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
281
+ 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/;
282
+ 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');
283
+ return text.replace(span, (m, offset, whole) => {
284
+ const start = Math.max(whole.lastIndexOf('.', offset), whole.lastIndexOf(';', offset), whole.lastIndexOf('\n', offset)) + 1;
285
+ const stops = [whole.indexOf('.', offset + m.length), whole.indexOf(';', offset + m.length), whole.indexOf('\n', offset + m.length)].filter((i) => i >= 0);
286
+ const end = stops.length ? Math.min(...stops) : whole.length;
287
+ return LIFT.test(whole.slice(start, end)) ? m : ' ';
288
+ });
289
+ }
290
+
268
291
  export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
269
292
  // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
270
293
  const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
@@ -272,10 +295,10 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
272
295
  const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
273
296
  && /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
274
297
  ? { kind: 'conditional-release', preflight } : null;
275
- const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
298
+ const text = neutralizeBannedLists(neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()));
276
299
  const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
277
300
  const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
278
- && !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
301
+ && !/^[\s*+•>\d.)-]*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
279
302
  if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
280
303
  const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
281
304
  && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
@@ -293,11 +316,40 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
293
316
  // actionable clause (negated/deferred/audit clauses are already filtered out). Git and
294
317
  // runtime are separate domains with separate proof; a prompt naming both stays a coding
295
318
  // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
296
- const gitOperation = actionable.some((clause) => /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
319
+ // A clause that STATES an operation is prohibited ("production deploy vẫn bị cấm", "forbidden deploy") or merely lists a
320
+ // production operation as a bare noun item ("* Production restart.") names it; it does not order it.
321
+ const operationNotOrdered = (clause) => {
322
+ const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
323
+ const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
324
+ // A lift or past marker ("was prohibited, owner now allows", "even though it is not allowed", "khong bi chan")
325
+ // means the prohibition is history or lifted: the clause still orders the operation.
326
+ 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;
327
+ if (/^[\s*+•>\d.)-]*(?:production|prod|staging)\s+(?:restart|redeploy|deploy(?:ment)?)\s*$/.test(clause)) return true;
328
+ // Evaluated per operation occurrence: the clause orders nothing only when EVERY operation in it is prohibited.
329
+ // The ban must attach to its own operation: no parenthesis, dash, colon, conjunction, relative pronoun or second operation in between.
330
+ const gap = `(?:(?!\\b(?:which|that|who|where|when|if|and|but|then)\\b|${OP})[^.;,()\\-:]){0,40}?`;
331
+ const banAfter = new RegExp(`^${gap}\\b(?:(?:is|are|remains?|van|con)\\s+)?${BAN}\\b(?!\\s+(?:to|from)\\b)`);
332
+ // "deploy/restart is forbidden", "deploy and restart are banned": operations joined as a list share one ban.
333
+ const joined = new RegExp(`^\\s*(?:/|,|and|or|va|hoac)\\s*${OP}\\b`);
334
+ const bannedAfter = (rest) => {
335
+ if (banAfter.test(rest)) return true;
336
+ const j = joined.exec(rest);
337
+ return j ? bannedAfter(rest.slice(j[0].length)) : false;
338
+ };
339
+ const banBefore = /\b(?:forbidden|prohibited|banned)\s+(?:(?:production|prod)\s+)?$/;
340
+ let total = 0;
341
+ let banned = 0;
342
+ for (const match of clause.matchAll(new RegExp(`\\b${OP}\\b`, 'g'))) {
343
+ total += 1;
344
+ if (bannedAfter(clause.slice(match.index + match[0].length)) || banBefore.test(clause.slice(0, match.index))) banned += 1;
345
+ }
346
+ return total > 0 && banned === total;
347
+ };
348
+ 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
349
  // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
298
350
  // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
299
351
  // "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)
352
+ 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
353
  && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
302
354
  const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
303
355
  const operation = Boolean(nativeDomain);
@@ -1893,10 +1893,17 @@ function neutralizeNonOrders(text) {
1893
1893
  let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1894
1894
  if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
1895
1895
  return source
1896
+ // A list item that DESCRIBES test cases ("Negative controls: yêu cầu thực sự chỉnh config/restart/deploy ...") is
1897
+ // specification of the work, not an order; it runs to the next bullet or blank line.
1898
+ .replace(/^[ \t]*(?:[-*+\u2022]|\d+[.)])[ \t]*(?:negative|positive)\s+controls?\b[^\n]*(?:\n(?![ \t]*(?:[-*+\u2022]|\d+[.)])|[ \t]*\n)[^\n]*)*/gm, ' ')
1899
+ // A header that negates the list under it ("Authorization ... không cho phép:", "Prohibitions:", "not allowed:")
1900
+ // makes every bullet an item of the prohibition, not an order.
1901
+ .replace(/(?:^|(?<=[.;][ \t]))[^.;\n]*\b(?:khong\s+(?:cho\s+phep|duoc|lam)|(?:does|do|did|is|are)\s+not\s+(?:allow|permit|authori[sz]e)\w*|not\s+(?:allowed|permitted|authori[sz]ed)|prohibitions?|prohibited(?:\s+actions?)?|forbidden(?:\s+actions?)?|banned|must\s+not\s+do)\s*:[ \t]*(?:\n[ \t]*(?:[-*+\u2022]|\d+[.)])[^\n]*)+/gm, ' ')
1896
1902
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
1897
1903
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
1898
1904
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1899
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1905
+ .replace(/\b(?:uu tien|prefer(?:red)?)\s+(?:a\s+|an\s+)?(?:configuration|config|cau hinh)\s+(?:change|changes|update|thay doi)\b/g, ' ')
1906
+ .replace(/\b(?:neu\s+(?:chi\s+)?(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1900
1907
  .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
1901
1908
  .replace(/\bchinh\s+(?:xac|sach)\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b|\b(?:diem|cho|muc|phan)\s+(?:phai|can|nen)\s+(?:sua|fix)\b/g, ' ')
1902
1909
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
@@ -1936,7 +1943,7 @@ function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
1936
1943
  const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
1937
1944
  const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
1938
1945
  const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
1939
- && !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
1946
+ && !/^[\s*+•>\d.)-]*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
1940
1947
  const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
1941
1948
  && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
1942
1949
  const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
@@ -1108,10 +1108,17 @@ function neutralizeNonOrders(text) {
1108
1108
  let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1109
1109
  if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
1110
1110
  return source
1111
+ // A list item that DESCRIBES test cases ("Negative controls: yêu cầu thực sự chỉnh config/restart/deploy ...") is
1112
+ // specification of the work, not an order; it runs to the next bullet or blank line.
1113
+ .replace(/^[ \t]*(?:[-*+\u2022]|\d+[.)])[ \t]*(?:negative|positive)\s+controls?\b[^\n]*(?:\n(?![ \t]*(?:[-*+\u2022]|\d+[.)])|[ \t]*\n)[^\n]*)*/gm, ' ')
1114
+ // A header that negates the list under it ("Authorization ... không cho phép:", "Prohibitions:", "not allowed:")
1115
+ // makes every bullet an item of the prohibition, not an order.
1116
+ .replace(/(?:^|(?<=[.;][ \t]))[^.;\n]*\b(?:khong\s+(?:cho\s+phep|duoc|lam)|(?:does|do|did|is|are)\s+not\s+(?:allow|permit|authori[sz]e)\w*|not\s+(?:allowed|permitted|authori[sz]ed)|prohibitions?|prohibited(?:\s+actions?)?|forbidden(?:\s+actions?)?|banned|must\s+not\s+do)\s*:[ \t]*(?:\n[ \t]*(?:[-*+\u2022]|\d+[.)])[^\n]*)+/gm, ' ')
1111
1117
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
1112
1118
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
1113
1119
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1114
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1120
+ .replace(/\b(?:uu tien|prefer(?:red)?)\s+(?:a\s+|an\s+)?(?:configuration|config|cau hinh)\s+(?:change|changes|update|thay doi)\b/g, ' ')
1121
+ .replace(/\b(?:neu\s+(?:chi\s+)?(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1115
1122
  .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
1116
1123
  .replace(/\bchinh\s+(?:xac|sach)\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b|\b(?:diem|cho|muc|phan)\s+(?:phai|can|nen)\s+(?:sua|fix)\b/g, ' ')
1117
1124
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
@@ -1162,6 +1169,22 @@ export function isVerificationOnlyRequest(promptText) {
1162
1169
  && !clauses.some((clause) => CHANGE_LEAD.test(clause));
1163
1170
  }
1164
1171
 
1172
+ // "Deploy and restart are banned", "Forbidden: deploy", "deploy: banned" name operations to prohibit them. The clause split on
1173
+ // "and"/commas would separate the ban from the first operation, so a banned operation list is removed before splitting.
1174
+ function neutralizeBannedLists(text) {
1175
+ const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
1176
+ const LIST = `${OP}(?:\\s*(?:,|/|\\band\\b|\\bor\\b|\\bva\\b|\\bhoac\\b|\\balso\\b)\\s*${OP})*`;
1177
+ const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
1178
+ 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/;
1179
+ 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');
1180
+ return text.replace(span, (m, offset, whole) => {
1181
+ const start = Math.max(whole.lastIndexOf('.', offset), whole.lastIndexOf(';', offset), whole.lastIndexOf('\n', offset)) + 1;
1182
+ const stops = [whole.indexOf('.', offset + m.length), whole.indexOf(';', offset + m.length), whole.indexOf('\n', offset + m.length)].filter((i) => i >= 0);
1183
+ const end = stops.length ? Math.min(...stops) : whole.length;
1184
+ return LIFT.test(whole.slice(start, end)) ? m : ' ';
1185
+ });
1186
+ }
1187
+
1165
1188
  export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
1166
1189
  // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
1167
1190
  const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
@@ -1169,10 +1192,10 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
1169
1192
  const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
1170
1193
  && /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
1171
1194
  ? { kind: 'conditional-release', preflight } : null;
1172
- const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
1195
+ const text = neutralizeBannedLists(neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()));
1173
1196
  const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
1174
1197
  const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
1175
- && !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
1198
+ && !/^[\s*+•>\d.)-]*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
1176
1199
  if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
1177
1200
  const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
1178
1201
  && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
@@ -1190,11 +1213,40 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
1190
1213
  // actionable clause (negated/deferred/audit clauses are already filtered out). Git and
1191
1214
  // runtime are separate domains with separate proof; a prompt naming both stays a coding
1192
1215
  // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
1193
- const gitOperation = actionable.some((clause) => /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
1216
+ // A clause that STATES an operation is prohibited ("production deploy vẫn bị cấm", "forbidden deploy") or merely lists a
1217
+ // production operation as a bare noun item ("* Production restart.") names it; it does not order it.
1218
+ const operationNotOrdered = (clause) => {
1219
+ const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
1220
+ const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
1221
+ // A lift or past marker ("was prohibited, owner now allows", "even though it is not allowed", "khong bi chan")
1222
+ // means the prohibition is history or lifted: the clause still orders the operation.
1223
+ 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;
1224
+ if (/^[\s*+•>\d.)-]*(?:production|prod|staging)\s+(?:restart|redeploy|deploy(?:ment)?)\s*$/.test(clause)) return true;
1225
+ // Evaluated per operation occurrence: the clause orders nothing only when EVERY operation in it is prohibited.
1226
+ // The ban must attach to its own operation: no parenthesis, dash, colon, conjunction, relative pronoun or second operation in between.
1227
+ const gap = `(?:(?!\\b(?:which|that|who|where|when|if|and|but|then)\\b|${OP})[^.;,()\\-:]){0,40}?`;
1228
+ const banAfter = new RegExp(`^${gap}\\b(?:(?:is|are|remains?|van|con)\\s+)?${BAN}\\b(?!\\s+(?:to|from)\\b)`);
1229
+ // "deploy/restart is forbidden", "deploy and restart are banned": operations joined as a list share one ban.
1230
+ const joined = new RegExp(`^\\s*(?:/|,|and|or|va|hoac)\\s*${OP}\\b`);
1231
+ const bannedAfter = (rest) => {
1232
+ if (banAfter.test(rest)) return true;
1233
+ const j = joined.exec(rest);
1234
+ return j ? bannedAfter(rest.slice(j[0].length)) : false;
1235
+ };
1236
+ const banBefore = /\b(?:forbidden|prohibited|banned)\s+(?:(?:production|prod)\s+)?$/;
1237
+ let total = 0;
1238
+ let banned = 0;
1239
+ for (const match of clause.matchAll(new RegExp(`\\b${OP}\\b`, 'g'))) {
1240
+ total += 1;
1241
+ if (bannedAfter(clause.slice(match.index + match[0].length)) || banBefore.test(clause.slice(0, match.index))) banned += 1;
1242
+ }
1243
+ return total > 0 && banned === total;
1244
+ };
1245
+ 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
1246
  // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
1195
1247
  // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
1196
1248
  // "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)
1249
+ 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
1250
  && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
1199
1251
  const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
1200
1252
  const operation = Boolean(nativeDomain);