dsh-completion-guard 0.5.0 → 0.5.1
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 +22 -0
- package/CHANGELOG.zh-CN.md +22 -0
- package/README.md +14 -6
- package/README.zh-CN.md +14 -6
- package/bin/dsh-completion-guard-host-lock.mjs +16 -0
- package/dist/domain/index.d.ts +2 -2
- package/dist/domain/index.js +2 -2
- package/dist/{domain-Cx8vpxSj.js → domain-BqmJLHcu.js} +2645 -581
- package/dist/{index-Cd3wXBLi.d.ts → index-Dcee_NYF.d.ts} +517 -31
- package/dist/index.d.ts +3 -2
- package/dist/index.js +270 -18
- package/docs/ARCHITECTURE.md +7 -2
- package/docs/COMPATIBILITY.md +70 -5
- package/docs/HOST_LOCK_UPGRADE.md +53 -5
- package/docs/LOCAL_ACCEPTANCE.md +10 -2
- package/docs/PORTING_NOTES.md +22 -0
- package/docs/SEMANTIC_COMPATIBILITY.md +24 -0
- package/docs/upstream-deltas.json +30 -4
- package/manifests/supported-host.v1.json +298 -89
- package/package.json +33 -31
|
@@ -614,7 +614,7 @@ const ORDERED_TEXT_RULES = [
|
|
|
614
614
|
["verify", /验证|确认|确保|\bverif(?:y|ies|ied|ying)\b|\bconfirm\b/i]
|
|
615
615
|
];
|
|
616
616
|
function semanticActionFromText(text) {
|
|
617
|
-
if (/^\s*(
|
|
617
|
+
if (/^\s*(?:验证|校验|确认|确保|核对|verif(?:y|ies|ied|ying)\b|confirm\b)/i.test(text)) return "verify";
|
|
618
618
|
for (const [action, pattern] of ORDERED_TEXT_RULES) if (pattern.test(text)) return action;
|
|
619
619
|
return "generic_run";
|
|
620
620
|
}
|
|
@@ -776,21 +776,1048 @@ function npmEscapedPackageName(packageId) {
|
|
|
776
776
|
}
|
|
777
777
|
|
|
778
778
|
//#endregion
|
|
779
|
-
//#region src/domain/
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
779
|
+
//#region src/domain/semantics.ts
|
|
780
|
+
/** A clause whose head verb demands a verification rather than a change. */
|
|
781
|
+
const ACCEPTANCE_LEAD = /^(?:验收|验证|确认|确保|核对|检查|verify|confirm|ensure|check)/i;
|
|
782
|
+
/**
|
|
783
|
+
* The contract kind a scope maps to. A prohibition and an acceptance keep their
|
|
784
|
+
* own lanes; everything else is a requirement. Acceptance is decided from the
|
|
785
|
+
* clause's own head verb, so "确保构建通过" stays an acceptance while a
|
|
786
|
+
* conditional or prohibition clause is never mislabelled.
|
|
787
|
+
*/
|
|
788
|
+
function kindOfScope(directive, body = "") {
|
|
789
|
+
if (directive === "prohibition") return "prohibition";
|
|
790
|
+
if (directive === "directive" && ACCEPTANCE_LEAD.test(body.trim())) return "acceptance";
|
|
791
|
+
return "requirement";
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Blank out inline-code spans while preserving every byte offset, so a caller
|
|
795
|
+
* can classify authority against masked text and still slice the original.
|
|
796
|
+
* Backticks are Markdown emphasis, but they are also how a log line or a
|
|
797
|
+
* command is quoted — and a quoted command is data, never an order.
|
|
798
|
+
*/
|
|
799
|
+
const MASK_CACHE = /* @__PURE__ */ new Map();
|
|
800
|
+
const MASK_CACHE_LIMIT = 64;
|
|
801
|
+
function maskCodeSpans(text) {
|
|
802
|
+
const cached = MASK_CACHE.get(text);
|
|
803
|
+
if (cached !== void 0) return cached;
|
|
804
|
+
const masked = computeMaskedSpans(text);
|
|
805
|
+
if (MASK_CACHE.size >= MASK_CACHE_LIMIT) MASK_CACHE.clear();
|
|
806
|
+
MASK_CACHE.set(text, masked);
|
|
807
|
+
return masked;
|
|
808
|
+
}
|
|
809
|
+
function computeMaskedSpans(text) {
|
|
810
|
+
const characters = text.split("");
|
|
811
|
+
let cursor = 0;
|
|
812
|
+
while (cursor < text.length) {
|
|
813
|
+
if (text[cursor] !== "`") {
|
|
814
|
+
cursor += 1;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const end = text.indexOf("`", cursor + 1);
|
|
818
|
+
if (end < 0) break;
|
|
819
|
+
for (let index = cursor; index <= end; index += 1) characters[index] = " ";
|
|
820
|
+
cursor = end + 1;
|
|
821
|
+
}
|
|
822
|
+
return characters.join("");
|
|
823
|
+
}
|
|
824
|
+
/** Actions Guard can name, shared with the command-surface manifest. */
|
|
825
|
+
/**
|
|
826
|
+
* The manifest's operation verbs. Each alternative is wrapped with word
|
|
827
|
+
* boundaries, so an English verb never matches inside another word ("check"
|
|
828
|
+
* inside "change"); a Chinese alternative is left alone because a Han character
|
|
829
|
+
* has no word boundary to assert.
|
|
830
|
+
*/
|
|
831
|
+
const CJK_VERBS = [
|
|
832
|
+
"创建",
|
|
833
|
+
"生成",
|
|
834
|
+
"新建",
|
|
835
|
+
"写入",
|
|
836
|
+
"修改",
|
|
837
|
+
"编辑",
|
|
838
|
+
"更改",
|
|
839
|
+
"读取",
|
|
840
|
+
"阅读",
|
|
841
|
+
"打开",
|
|
842
|
+
"验证",
|
|
843
|
+
"校验",
|
|
844
|
+
"确认",
|
|
845
|
+
"确保",
|
|
846
|
+
"检查",
|
|
847
|
+
"核对",
|
|
848
|
+
"运行",
|
|
849
|
+
"执行",
|
|
850
|
+
"拉取",
|
|
851
|
+
"同步",
|
|
852
|
+
"更新",
|
|
853
|
+
"下载",
|
|
854
|
+
"安装",
|
|
855
|
+
"部署",
|
|
856
|
+
"上传",
|
|
857
|
+
"提交",
|
|
858
|
+
"推送",
|
|
859
|
+
"发布",
|
|
860
|
+
"升级",
|
|
861
|
+
"重启",
|
|
862
|
+
"重新启动",
|
|
863
|
+
"重载",
|
|
864
|
+
"合并",
|
|
865
|
+
"继续",
|
|
866
|
+
"撤销",
|
|
867
|
+
"删除"
|
|
868
|
+
];
|
|
869
|
+
/** Every CJK action word, longest first so 重新启动 wins over 新. */
|
|
870
|
+
const CJK_VERB_PATTERN = `(?:${[...CJK_VERBS].sort((a, b) => b.length - a.length).join("|")})`;
|
|
871
|
+
/** The same words as literal strings, for exact scanning without regex escapes. */
|
|
872
|
+
const CJK_VERB_WORDS = [...CJK_VERBS].sort((a, b) => b.length - a.length);
|
|
873
|
+
const ACTION_VERB_PATTERN = `(?:${COMMAND_SURFACE_MANIFEST.operationVerbs.map((entry) => entry.pattern.split("|").map((alternative) => /^[A-Za-z]/.test(alternative.trim()) ? `\\b${alternative.trim()}\\b` : alternative.trim()).join("|")).join("|")}|${CJK_VERB_PATTERN})`;
|
|
874
|
+
const ACTION_VERB = new RegExp(ACTION_VERB_PATTERN, "i");
|
|
875
|
+
/** Operation verbs beyond the guard action surface (local work and diagnosis). */
|
|
876
|
+
const WORK_VERB = /创建|生成|新建|写入|修改|编辑|运行|执行|编写|撰写|部署|安装|升级|提交|下载|上传|拉取|同步|重启|测试|检查|验证|确认|修复|更新|清理|整理|记录|构建|编译|重构|迁移|删除|回滚|发布|推送|合并|继续|恢复|还原|回滚|实现|\b(?:build|create|write|modify|change|edit|run|fix|update|install|push|publish|test|verify|check|commit|deploy|migrate|remove|delete|restart|revert|refactor|inspect|fetch|pull|implement)\b/i;
|
|
877
|
+
/** Explanatory framings: an action named afterwards is an object, not an order. */
|
|
878
|
+
const EXPLAIN_VERB = /解释|说明|讲解|介绍|阐述|分析|讨论|描述|科普|什么意思|是什么意思|有什么(?:作用|影响|区别)|\bexplain\b|\bdescribe\b|\bclarify\b|\bwhat\s+does\b|\bwhat\s+is\b|\bhow\s+does\b|\bmeaning\s+of\b/i;
|
|
879
|
+
/** Interrogative framings that make a scope a question rather than an order. */
|
|
880
|
+
const QUESTION_SCOPE = /[??]|是否|是不是|为什么|为何|怎么|如何|什么|哪些|哪一种|能否|可否|要不要|该不该|由谁|是谁|\b(?:whether|which|why|should|could|would)\b/i;
|
|
881
|
+
const NEGATORS = [
|
|
882
|
+
["不要", "zh"],
|
|
883
|
+
["不用", "zh"],
|
|
884
|
+
["不得", "zh"],
|
|
885
|
+
["不许", "zh"],
|
|
886
|
+
["不准", "zh"],
|
|
887
|
+
["不能", "zh"],
|
|
888
|
+
["不必", "zh"],
|
|
889
|
+
["无需", "zh"],
|
|
890
|
+
["毋须", "zh"],
|
|
891
|
+
["勿", "zh"],
|
|
892
|
+
["别", "zh"],
|
|
893
|
+
["甭", "zh"],
|
|
894
|
+
["不", "zh"],
|
|
895
|
+
["do not", "en"],
|
|
896
|
+
["does not", "en"],
|
|
897
|
+
["did not", "en"],
|
|
898
|
+
["don't", "en"],
|
|
899
|
+
["doesn't", "en"],
|
|
900
|
+
["won't", "en"],
|
|
901
|
+
["can't", "en"],
|
|
902
|
+
["cannot", "en"],
|
|
903
|
+
["never", "en"],
|
|
904
|
+
["avoid", "en"],
|
|
905
|
+
["without", "en"],
|
|
906
|
+
["no longer", "en"]
|
|
907
|
+
];
|
|
908
|
+
/** Characters that end one coordinated scope and may begin the next. */
|
|
909
|
+
const SEPARATORS = new Set([
|
|
910
|
+
",",
|
|
911
|
+
",",
|
|
912
|
+
"、",
|
|
913
|
+
";",
|
|
914
|
+
";",
|
|
915
|
+
"。",
|
|
916
|
+
".",
|
|
917
|
+
"!",
|
|
918
|
+
"!",
|
|
919
|
+
"?",
|
|
920
|
+
"?",
|
|
921
|
+
":",
|
|
922
|
+
":",
|
|
923
|
+
"\n",
|
|
924
|
+
"\r"
|
|
925
|
+
]);
|
|
926
|
+
const CONNECTORS = [
|
|
927
|
+
"但是",
|
|
928
|
+
"不过",
|
|
929
|
+
"然而",
|
|
930
|
+
"同时",
|
|
931
|
+
"并且",
|
|
932
|
+
"而且",
|
|
933
|
+
"以及",
|
|
934
|
+
"然后",
|
|
935
|
+
"接着",
|
|
936
|
+
"而是",
|
|
937
|
+
"但",
|
|
938
|
+
"而",
|
|
939
|
+
"也",
|
|
940
|
+
"并",
|
|
941
|
+
"且",
|
|
942
|
+
"又",
|
|
943
|
+
"再",
|
|
944
|
+
"就",
|
|
945
|
+
"则"
|
|
946
|
+
];
|
|
947
|
+
const ENGLISH_CONNECTORS = [
|
|
948
|
+
"but",
|
|
949
|
+
"and",
|
|
950
|
+
"then",
|
|
951
|
+
"also",
|
|
952
|
+
"however",
|
|
953
|
+
"yet"
|
|
954
|
+
];
|
|
955
|
+
const CONNECTOR_PATTERN = `(?:${[...CONNECTORS].sort((a, b) => b.length - a.length).join("|")}|${ENGLISH_CONNECTORS.join("|")})`;
|
|
956
|
+
const CONTINUATION_AFTER_SEPARATOR = new RegExp(`^\\s*${CONNECTOR_PATTERN}`, "i");
|
|
957
|
+
/**
|
|
958
|
+
* Instruction openings that make the text after a bare conjunction its own
|
|
959
|
+
* clause. "并检查 GUI 效果" is a second instruction; "并在本地仓库记录" is
|
|
960
|
+
* handled separately as a locative, and anything else stays one object list.
|
|
961
|
+
*/
|
|
962
|
+
const CROSS_CLAUSE_HEAD = /^\s*(?:检查|查看|确认|验证|测试|运行|执行|安装|应用|更新|升级|记录|提交|推送|发布|部署|重启|重新启动|创建|新建|生成|修改|编辑|拉取|抓取|删除|回滚|清理|整理|实现|完成)/u;
|
|
963
|
+
/**
|
|
964
|
+
* Openings that make the text after a conjunction a DISTINCT instruction rather
|
|
965
|
+
* than the second half of one action. "并确认全部通过" completes the action
|
|
966
|
+
* before it, so 确认 is deliberately absent here.
|
|
967
|
+
*/
|
|
968
|
+
const DISTINCT_CLAUSE_HEAD = /^\s*(?:检查|查看|测试|验证|运行|执行|安装|应用|更新|升级|提交|推送|发布|部署|重启|重新启动|创建|新建|生成|修改|编辑|拉取|抓取|删除|回滚|清理|整理)/u;
|
|
969
|
+
/**
|
|
970
|
+
* A place clause that follows a coordinating conjunction: the shape of "并在
|
|
971
|
+
* 本地仓库记录", where the conjunction joins an action to where it happens
|
|
972
|
+
* rather than to a second action.
|
|
973
|
+
*/
|
|
974
|
+
const LOCATIVE_CLAUSE = /^\s*在.{1,40}?(?:记录|保存|写入)$/u;
|
|
975
|
+
const USER_ACTOR_PATTERNS = [
|
|
976
|
+
/(?:由|让|给|请)\s*(?:我|本人|我们)/,
|
|
977
|
+
/(?:我|我们)(?:自己|本人)?\s*(?:来|去|会|将|要)?\s*(?:手动|亲自|自行)?\s*(?:重启|重新启动|升级|安装|更新|执行|运行|操作|完成|处理|部署|发布|推送|合并|确认|登录|审批|提供|准备|搭建|检查|验证|测试)/,
|
|
978
|
+
/\bI(?:'ll| will| am going to| myself)\b/i,
|
|
979
|
+
/\b(?:on my own|by myself)\b/i
|
|
980
|
+
];
|
|
981
|
+
const AGENT_ACTOR_PATTERNS = [
|
|
982
|
+
/(?:由|让|请)\s*(?:你|您|助手|代理)/,
|
|
983
|
+
/(?:你|您)(?:来|去|会|将|要|负责|自己)/,
|
|
984
|
+
/\byou (?:should|must|need to|will|are to)\b/i
|
|
985
|
+
];
|
|
986
|
+
const OUTPUT_NOUN = /命令|脚本|指令|步骤|清单|说明|文档|模板|command|script|instructions?|checklist|snippet/i;
|
|
987
|
+
const OUTPUT_REQUEST = /(?:给|帮|替|为)(?:我|我们)?\s*(?:写|生成|整理|列|准备|提供|输出|来)|生成(?:一|两|几)?(?:条|个|份)|输出(?:一|个|份)?|列出|列一(?:下|个)|\b(?:provide|write|generate|outline|list|draft)\b|give\s+me/i;
|
|
988
|
+
const CONDITION_MARKERS = [
|
|
989
|
+
["如果", "prefix"],
|
|
990
|
+
["假如", "prefix"],
|
|
991
|
+
["倘若", "prefix"],
|
|
992
|
+
["若是", "prefix"],
|
|
993
|
+
["一旦", "prefix"],
|
|
994
|
+
["除非", "prefix"],
|
|
995
|
+
["只有", "prefix"],
|
|
996
|
+
["只要", "prefix"],
|
|
997
|
+
["等到", "prefix"],
|
|
998
|
+
["若", "prefix"],
|
|
999
|
+
["在", "prefix"],
|
|
1000
|
+
["if", "prefix"],
|
|
1001
|
+
["unless", "prefix"],
|
|
1002
|
+
["once", "prefix"],
|
|
1003
|
+
["when", "prefix"],
|
|
1004
|
+
["provided that", "prefix"],
|
|
1005
|
+
["after", "prefix"],
|
|
1006
|
+
["之后", "suffix"],
|
|
1007
|
+
["以后", "suffix"],
|
|
1008
|
+
["才", "suffix"],
|
|
1009
|
+
["再", "suffix"]
|
|
1010
|
+
];
|
|
1011
|
+
const RESUME_MARKER = /(?:收到|得到|等到|等待|经)\s*.{0,12}?(?:明确|显式|最终)?\s*(?:回报|回复|答复|确认|批准|同意|授权|指示|通知)|(?:我|用户)(?:明确|最终)?\s*(?:确认|回复|回报|批准|同意|授权)(?:后再|之后|后|以后)?|after\s+(?:I|the user)\s+(?:confirm|reply|approve|authorize)|once\s+(?:I|the user)\s+(?:confirm|reply|approve)|waiting\s+for\s+(?:the\s+)?(?:user|you)/i;
|
|
1012
|
+
/**
|
|
1013
|
+
* The resumption event itself, without the request prefix a scope may open
|
|
1014
|
+
* with. Used to locate the event inside a scope rather than at its start.
|
|
1015
|
+
*/
|
|
1016
|
+
const RESUMPTION_EVENT = /(?:收到|得到|等到|等待)\s*.{0,12}?(?:确认|回复|回报|批准|同意|授权|指示|通知)\s*(?:后再|之后|后|以后|再)|(?:我|用户)(?:明确|最终)?\s*(?:确认|回复|回报|批准|同意|授权)\s*(?:后再|之后|后|以后|再)|(?:after|once)\s+(?:I|the user)\s+(?:confirm|reply|approve|authorize)|waiting\s+for\s+(?:the\s+)?(?:user|you)/i;
|
|
1017
|
+
/**
|
|
1018
|
+
* A scope that OPENS with the resumption event it waits on. Anchored at the
|
|
1019
|
+
* start and greedy, so the match runs to the end of the event itself
|
|
1020
|
+
* ("收到我的确认后"): the condition is what the scope says after it.
|
|
1021
|
+
*/
|
|
1022
|
+
const RESUME_SCOPE_MARKER = /^(?:请在|请|麻烦|帮我|需要你|务必)?\s*(?:(?:收到|得到|等到|等待)\s*.{0,12}?(?:确认|回复|回报|批准|同意|授权|指示|通知)\s*(?:后再|之后|后|以后|再)|(?:我|用户)(?:明确|最终)?\s*(?:确认|回复|回报|批准|同意|授权)\s*(?:后再|之后|后|以后|再)|(?:after|once)\s+(?:I|the user)\s+(?:confirm|reply|approve|authorize)|waiting\s+for\s+(?:the\s+)?(?:user|you))/i;
|
|
1023
|
+
const NARRATIVE_PAST = /(?:已经|已|刚刚|刚才|此前|之前)(?:经)?(?:推送|发布|提交|安装|升级|重启|合并|完成|修改|更新|删除|创建|写入)|\b(?:already|have|has|had)\s+(?:been\s+)?(?:pushed|published|committed|installed|upgraded|restarted|merged|completed|finished|modified|updated)\b/i;
|
|
1024
|
+
/**
|
|
1025
|
+
* Completion aspects that turn a clause into a report: a verb finished with
|
|
1026
|
+
* 了/过/完了/好了 states what happened, so it orders nothing. A directive never
|
|
1027
|
+
* carries them ("修改 README" is an order, "修改了 README" is a report).
|
|
1028
|
+
*/
|
|
1029
|
+
const NARRATIVE_ASPECT = /(?:完了|好了|过了)|(?:已经|已|刚刚|刚才|此前|之前)[\p{Script=Han}]{0,4}(?:了|过)|\b(?:was|were|has been|have been)\b/iu;
|
|
1030
|
+
const NARRATIVE_DIRECTIVE = /请|需要你|帮我|麻烦|务必|\b(?:please|must)\b/i;
|
|
1031
|
+
const UNRESOLVED_SCOPE = /^(?:看看|看一下|瞅瞅|研究一下|了解|随便|maybe|perhaps|somehow|figure\s+out)/i;
|
|
1032
|
+
/**
|
|
1033
|
+
* A completed confirmation receipt: the root reports that the event it was
|
|
1034
|
+
* waiting for already happened. It reserves nothing, so it must not mint a
|
|
1035
|
+
* wait, and it is not work either.
|
|
1036
|
+
*/
|
|
1037
|
+
const CONFIRMATION_RECEIPT = /^(?:我)?\s*(?:已|已经)?\s*(?:收到|得到|等到|等待)(?:了|过)?\s*(?:我|你|您|用户)?\s*的?\s*.{0,12}?(?:确认|回复|回报|批准|同意|授权|指示|通知)\s*(?:了|啦|过|收到)\s*[。..!!]?$/u;
|
|
1038
|
+
const SENTENCE_END = new Set([
|
|
1039
|
+
"。",
|
|
1040
|
+
"!",
|
|
1041
|
+
"?",
|
|
1042
|
+
"!",
|
|
1043
|
+
"?",
|
|
1044
|
+
"\n",
|
|
1045
|
+
"\r"
|
|
1046
|
+
]);
|
|
1047
|
+
function isWordBoundary(text, index) {
|
|
1048
|
+
if (index <= 0) return true;
|
|
1049
|
+
return !/[\p{L}\p{N}_]/u.test(text[index - 1]);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* The first negator in `text` at or after `from`.
|
|
1053
|
+
*
|
|
1054
|
+
* A multi-word English negator is matched at BOTH of its words ("do not"), so a
|
|
1055
|
+
* caller scanning for a negated verb does not have to know where the phrase
|
|
1056
|
+
* began. `index` 0 is always a boundary; later positions are boundaries only
|
|
1057
|
+
* when the preceding character is not a word character.
|
|
1058
|
+
*/
|
|
1059
|
+
function firstNegation(text, from = 0) {
|
|
1060
|
+
for (let cursor = from; cursor < text.length; cursor += 1) {
|
|
1061
|
+
const token = negatorAt(text, cursor);
|
|
1062
|
+
if (token) return {
|
|
1063
|
+
index: cursor,
|
|
1064
|
+
token
|
|
788
1065
|
};
|
|
789
1066
|
}
|
|
1067
|
+
}
|
|
1068
|
+
/** Match a negator at exactly `index`, the longest alternative winning. */
|
|
1069
|
+
function negatorAt(text, index) {
|
|
1070
|
+
const lower = text.toLowerCase();
|
|
1071
|
+
const candidates = NEGATORS.filter(([token]) => lower.startsWith(token, index)).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0]));
|
|
1072
|
+
for (const [token] of candidates) {
|
|
1073
|
+
if (token.length === 1 && /[\u3400-\u9fff]/.test(token)) {
|
|
1074
|
+
if (!/[\p{Script=Han}\p{L}\p{N}]/u.test(text[index + 1] ?? "")) continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (/^[a-z]/.test(token)) {
|
|
1077
|
+
if (!isWordBoundary(text, index)) continue;
|
|
1078
|
+
if (/[\p{L}\p{N}_-]/u.test(text[index + token.length] ?? "")) continue;
|
|
1079
|
+
const after = text[index + token.length] ?? "";
|
|
1080
|
+
if (/[./@\\]/u.test(after) && !/\s/u.test(text[index + token.length + 1] ?? "")) continue;
|
|
1081
|
+
}
|
|
1082
|
+
return token;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Index of the first action verb at or after `offset`.
|
|
1087
|
+
*
|
|
1088
|
+
* A vocabulary entry that a multi-character action immediately continues is the
|
|
1089
|
+
* first character of that word rather than a verb of its own — the 升 of 升级,
|
|
1090
|
+
* the 然 of 然后 — so the longer action is chosen instead. Without that rule
|
|
1091
|
+
* "然后完成…" reads as two verbs and every condition analysis downstream anchors
|
|
1092
|
+
* on the wrong one.
|
|
1093
|
+
*/
|
|
1094
|
+
function firstActionVerb(text, offset = 0, before = text.length) {
|
|
1095
|
+
const matches = actionVerbMatches(text, offset, before);
|
|
1096
|
+
return matches.length > 0 ? matches[0].index : -1;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Every action word in `[offset, before)`, ordered by position, with the words
|
|
1100
|
+
* that are only a prefix of a longer action removed (the 升 of 升级, the 然 of
|
|
1101
|
+
* 然后). The remaining candidates are the verbs an instruction can be about.
|
|
1102
|
+
*/
|
|
1103
|
+
function actionVerbMatches(text, offset = 0, before = text.length) {
|
|
1104
|
+
const span = text.slice(offset, before);
|
|
1105
|
+
const earliest = [];
|
|
1106
|
+
for (const pattern of [ACTION_VERB, WORK_VERB]) {
|
|
1107
|
+
const match = pattern.exec(span);
|
|
1108
|
+
if (match && !(match[0].length === 1 && /[A-Za-z]/.test(match[0]))) earliest.push({
|
|
1109
|
+
index: offset + match.index,
|
|
1110
|
+
length: match[0].length
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return earliest.sort((a, b) => a.index - b.index);
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* True when a negator's scope covers the verb starting at `index`.
|
|
1117
|
+
*
|
|
1118
|
+
* The negator has to be phrase-initial, so the 不 of 手动 and the 无 of 无论 are
|
|
1119
|
+
* not read as bans; a contrast or list separator between the negator and the
|
|
1120
|
+
* verb ends its scope ("不仅…而且运行" keeps the run positive).
|
|
1121
|
+
*/
|
|
1122
|
+
function verbIsNegated(text, index) {
|
|
1123
|
+
const ceiling = Math.min(index, 12);
|
|
1124
|
+
for (let back = 1; back <= ceiling; back += 1) {
|
|
1125
|
+
const at = index - back;
|
|
1126
|
+
const token = negatorAt(text, at);
|
|
1127
|
+
if (!token || at + token.length > index) continue;
|
|
1128
|
+
if (at > 0 && /[\u3400-\u9fff]/.test(text[at - 1])) continue;
|
|
1129
|
+
if (/[,,、;;。!!??\n\r]/.test(text.slice(at + token.length, index))) continue;
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
/** True when an unnegated operation verb occurs inside `[from, to)`. */
|
|
1135
|
+
function hasPositiveVerb(text, from, to) {
|
|
1136
|
+
const index = firstActionVerb(text, from, to);
|
|
1137
|
+
if (index < 0) return false;
|
|
1138
|
+
return !verbIsNegated(text, index);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* The verb a negator bans. A Chinese negator may put an adverb between itself
|
|
1142
|
+
* and its verb ("不正式发布"), so candidate verbs are walked in order and the
|
|
1143
|
+
* first one that is a real word rather than part of the preceding word wins.
|
|
1144
|
+
*/
|
|
1145
|
+
function bannedVerbIndex(text, afterNegator, before) {
|
|
1146
|
+
const span = text.slice(afterNegator, before);
|
|
1147
|
+
for (const word of CJK_VERB_WORDS) {
|
|
1148
|
+
const at = span.indexOf(word);
|
|
1149
|
+
if (at < 0) continue;
|
|
1150
|
+
return afterNegator + at;
|
|
1151
|
+
}
|
|
1152
|
+
const candidates = actionVerbMatches(text, afterNegator, before);
|
|
1153
|
+
for (const candidate of actionVerbMatches(text, 0, afterNegator)) candidates.push(candidate);
|
|
1154
|
+
if (candidates.length === 0) return -1;
|
|
1155
|
+
return candidates.map((candidate) => candidate.index).reduce((best, index) => Math.abs(index - afterNegator) < Math.abs(best - afterNegator) ? index : best);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* A resumption condition: everything a scope says before the event that ends
|
|
1159
|
+
* the wait ("收到我的确认后再推送" waits for the confirmation, so the push is
|
|
1160
|
+
* not executable yet). Leading request words are not part of the condition, and
|
|
1161
|
+
* a marker separated from the scope start by more than a clause belongs to a
|
|
1162
|
+
* different statement.
|
|
1163
|
+
*/
|
|
1164
|
+
function resumptionConditionOf(scope) {
|
|
1165
|
+
const text = scope.text;
|
|
1166
|
+
if (scope.directive === "conditional") return text.replace(/^(?:请在|请|麻烦|帮我|需要你|务必)\s*/u, "").trim() || void 0;
|
|
1167
|
+
return leadingResumptionCondition(text, firstActionVerb(maskCodeSpans(text)) >= 0);
|
|
1168
|
+
}
|
|
1169
|
+
function leadingResumptionCondition(text, hasAction) {
|
|
1170
|
+
const masked = maskCodeSpans(text);
|
|
1171
|
+
const marker = RESUME_SCOPE_MARKER.exec(masked);
|
|
1172
|
+
if (!marker) return void 0;
|
|
1173
|
+
const guarded = masked.slice(marker[0].length).replace(/^[\s,,、::]+/u, "").replace(/^(?:再|才|就|则|即)\s*/u, "").trim().replace(/[。..!!??]+$/u, "");
|
|
1174
|
+
if (guarded && /[;;。]/u.test(guarded)) return void 0;
|
|
1175
|
+
if (!hasAction) return void 0;
|
|
1176
|
+
return marker[0].replace(/^(?:请在|请|麻烦|帮我|需要你|务必)\s*/u, "").trim() || void 0;
|
|
1177
|
+
}
|
|
1178
|
+
/** End index of the negated span beginning at `start`. */
|
|
1179
|
+
function negatedSpanEnd(text, start) {
|
|
1180
|
+
for (let cursor = start + 1; cursor < text.length; cursor += 1) {
|
|
1181
|
+
const character = text[cursor];
|
|
1182
|
+
if (character === "\n" || character === "\r") return cursor;
|
|
1183
|
+
if (character === "。" || character === "!" || character === "?" || character === "!" || character === "?") return cursor;
|
|
1184
|
+
if (character === "." && (cursor + 1 >= text.length || /\s/.test(text[cursor + 1]))) return cursor;
|
|
1185
|
+
if (character === "但" && text[cursor + 1] !== "是") return cursor;
|
|
1186
|
+
if (character === "而" && text[cursor + 1] === "是") return cursor;
|
|
1187
|
+
if (character === ";" || character === ";") return cursor;
|
|
1188
|
+
if (negatorAt(text, cursor)) return cursor;
|
|
1189
|
+
if (!SEPARATORS.has(character)) continue;
|
|
1190
|
+
const rest = text.slice(cursor + 1);
|
|
1191
|
+
if (CONTINUATION_AFTER_SEPARATOR.test(rest)) continue;
|
|
1192
|
+
if (character === "," || character === "," || character === "、") return cursor + 1;
|
|
1193
|
+
if (hasPositiveVerb(text, cursor + 1, text.length)) return cursor;
|
|
1194
|
+
}
|
|
1195
|
+
return text.length;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Whether a run that follows a negator names an action directly ("推送、不
|
|
1199
|
+
* 发布" → true, "任何改动" → false). Only the guard's own action surface counts:
|
|
1200
|
+
* consultative verbs such as 完成 are deliberately absent, so "尚未完成" stays a
|
|
1201
|
+
* statement instead of becoming a ban.
|
|
1202
|
+
*/
|
|
1203
|
+
function namesActionSpan(text) {
|
|
1204
|
+
const match = /^[^\p{Script=Han}A-Za-z]*([\p{Script=Han}A-Za-z][\p{Script=Han}A-Za-z0-9_-]*)/u.exec(text);
|
|
1205
|
+
if (!match) return false;
|
|
1206
|
+
const head = match[1];
|
|
1207
|
+
for (const entry of COMMAND_SURFACE_MANIFEST.operationVerbs) if (new RegExp(`^(?:${entry.pattern})$`, "i").test(head)) return true;
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Split one message into scopes, in source order.
|
|
1212
|
+
*
|
|
1213
|
+
* The working list holds `[text, offset]` runs of the original message. A run is
|
|
1214
|
+
* resolved into one scope as soon as a rule matches; otherwise the runner splits
|
|
1215
|
+
* it into a head and a tail and pushes the tail back, so the split is iterative
|
|
1216
|
+
* and no run is ever re-read out of order.
|
|
1217
|
+
*
|
|
1218
|
+
* A negation opens a scope covering every action it governs — the scope ends at
|
|
1219
|
+
* a new positive verb, at a contrast, or (for a coordinated ban such as
|
|
1220
|
+
* "不推送、不发布") at the end of the run. A separator that is *followed by a
|
|
1221
|
+
* coordinating conjunction* also ends the current scope: "修复代码,但不推送"
|
|
1222
|
+
* is a task plus a ban, while "更新皮肤中心并在本地仓库记录" stays one
|
|
1223
|
+
* coordinated scope until the conjunction itself.
|
|
1224
|
+
*/
|
|
1225
|
+
function scopeOf(raw, options = {}) {
|
|
1226
|
+
const source = raw.trim();
|
|
1227
|
+
if (!source) return [];
|
|
1228
|
+
const pending = [{
|
|
1229
|
+
text: source,
|
|
1230
|
+
offset: 0
|
|
1231
|
+
}];
|
|
1232
|
+
const resolved = [];
|
|
1233
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1234
|
+
const push = (entry) => {
|
|
1235
|
+
const key = `${entry.offset}\u0000${entry.scope.directive}\u0000${entry.scope.text}`;
|
|
1236
|
+
if (emitted.has(key)) return;
|
|
1237
|
+
emitted.add(key);
|
|
1238
|
+
resolved.push(entry);
|
|
1239
|
+
};
|
|
1240
|
+
while (pending.length > 0) {
|
|
1241
|
+
const run = pending.pop();
|
|
1242
|
+
const text = run.text.trim();
|
|
1243
|
+
if (!text) continue;
|
|
1244
|
+
const inheritedCondition = run.inherited;
|
|
1245
|
+
const offset = run.offset + run.text.indexOf(text);
|
|
1246
|
+
const masked = maskCodeSpans(text);
|
|
1247
|
+
const conditionPrefix = prefixConditionIndex(masked.toLowerCase());
|
|
1248
|
+
const negation = firstNegation(masked);
|
|
1249
|
+
let earliestVerb = firstActionVerb(masked, conditionPrefix !== void 0 ? lastBoundaryIndex(text, conditionPrefix) : 0);
|
|
1250
|
+
if (earliestVerb < 0 && conditionPrefix !== void 0) {
|
|
1251
|
+
const tail$1 = expressionTailVerb(masked, conditionPrefix);
|
|
1252
|
+
if (tail$1 > conditionPrefix) earliestVerb = tail$1;
|
|
1253
|
+
}
|
|
1254
|
+
const negationIndex = negation ? negation.index : -1;
|
|
1255
|
+
const banScanEnd = masked.length;
|
|
1256
|
+
const bannedVerb = negation ? bannedVerbIndex(masked, negationIndex + negation.token.length, banScanEnd) : -1;
|
|
1257
|
+
const negationBansAction = negation !== void 0 && (bannedVerb >= 0 || namesActionSpan(masked.slice(negationIndex + negation.token.length, banScanEnd)));
|
|
1258
|
+
negation !== void 0 && bannedVerb >= 0 && /^[\s::,,、]*$/u.test(masked.slice(negationIndex + negation.token.length, bannedVerb));
|
|
1259
|
+
const earliestNegation = negationBansAction ? negationIndex : -1;
|
|
1260
|
+
const earliestNegationToken = negationBansAction ? negation.token : "";
|
|
1261
|
+
const locativePrefix = conditionPrefix !== void 0 && text[conditionPrefix] === "在" && /^在.{1,24}?(?:记录|保存|写入|提交|运行|执行|测试|检查|验证|完成)/u.test(text.slice(conditionPrefix, earliestVerb));
|
|
1262
|
+
if (conditionPrefix !== void 0 && earliestNegation < 0 && !locativePrefix) {
|
|
1263
|
+
const conditional = conditionSplit(text, conditionPrefix, earliestVerb, options);
|
|
1264
|
+
if (conditional) {
|
|
1265
|
+
for (const scope of conditional) push({
|
|
1266
|
+
scope,
|
|
1267
|
+
offset: offset + (scope.start ?? 0)
|
|
1268
|
+
});
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (earliestNegation >= 0) {
|
|
1273
|
+
const head$1 = text.slice(0, earliestNegation).trim();
|
|
1274
|
+
const end$1 = negatedSpanEnd(masked, earliestNegation);
|
|
1275
|
+
const banText = text.slice(earliestNegation, end$1).trim();
|
|
1276
|
+
const tail$1 = text.slice(end$1).replace(/^[\s。..!!??,,;;、]+/, "").trim();
|
|
1277
|
+
if (tail$1) pending.push({
|
|
1278
|
+
text: tail$1,
|
|
1279
|
+
offset: offset + end$1,
|
|
1280
|
+
...inheritedCondition ? { inherited: inheritedCondition } : {}
|
|
1281
|
+
});
|
|
1282
|
+
if (banText) {
|
|
1283
|
+
const clauseStart = conditionPrefix !== void 0 && conditionPrefix < earliestNegation ? lastBoundaryIndex(text, conditionPrefix) : -1;
|
|
1284
|
+
const conditionText = inheritedCondition ?? (clauseStart >= 0 ? stripConditionConnector(text.slice(clauseStart, earliestNegation)) : "");
|
|
1285
|
+
push({
|
|
1286
|
+
offset,
|
|
1287
|
+
scope: {
|
|
1288
|
+
text: banText,
|
|
1289
|
+
body: stripNegators(banText, earliestNegationToken) || banText,
|
|
1290
|
+
directive: "prohibition",
|
|
1291
|
+
...conditionText ? { condition: conditionText } : {}
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
if (head$1) pending.push({
|
|
1296
|
+
text: head$1,
|
|
1297
|
+
offset
|
|
1298
|
+
});
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const end = positiveScopeEnd(masked, options);
|
|
1302
|
+
const head = text.slice(0, end).trim();
|
|
1303
|
+
const tail = text.slice(end).trim();
|
|
1304
|
+
if (tail) pending.push({
|
|
1305
|
+
text: tail,
|
|
1306
|
+
offset: offset + end
|
|
1307
|
+
});
|
|
1308
|
+
if (head) {
|
|
1309
|
+
const inherited = conditionPrefix !== void 0 && conditionPrefix < head.length ? text.slice(conditionPrefix, head.length).replace(/^[\s,,、;;::]+/, "").trim() : "";
|
|
1310
|
+
push({
|
|
1311
|
+
offset,
|
|
1312
|
+
scope: {
|
|
1313
|
+
text: head,
|
|
1314
|
+
body: stripConnectors(head),
|
|
1315
|
+
directive: classifyPositive(head),
|
|
1316
|
+
...inherited ? { condition: inherited } : {}
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1322
|
+
return resolved.sort((a, b) => a.offset - b.offset).filter((entry) => {
|
|
1323
|
+
const key = `${entry.offset}\u0000${entry.scope.directive}\u0000${entry.scope.text}`;
|
|
1324
|
+
if (seen.has(key)) return false;
|
|
1325
|
+
seen.add(key);
|
|
1326
|
+
return true;
|
|
1327
|
+
}).map((entry) => entry.scope).filter((scope) => /[\p{L}\p{N}]/u.test(scope.body) && /[\p{L}\p{N}]/u.test(scope.text));
|
|
1328
|
+
}
|
|
1329
|
+
function positiveScopeEnd(masked, options = {}) {
|
|
1330
|
+
const limit = masked.length;
|
|
1331
|
+
let cursor = 0;
|
|
1332
|
+
while (cursor < limit && (masked[cursor] === "," || masked[cursor] === "," || masked[cursor] === "、" || masked[cursor] === "并" || masked[cursor] === "且" || /\s/u.test(masked[cursor]))) cursor += 1;
|
|
1333
|
+
while (cursor < limit) {
|
|
1334
|
+
const character = masked.slice(cursor, cursor + 1);
|
|
1335
|
+
if (SENTENCE_END.has(character)) return cursor + 1;
|
|
1336
|
+
if (character === ";" || character === ";") return cursor + 1;
|
|
1337
|
+
if (!(character === "," || character === "," || character === "、" || character === "并" || character === "且")) {
|
|
1338
|
+
cursor += 1;
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const rest = masked.slice(cursor + 1);
|
|
1342
|
+
if (options.coordinationSplit === false) {
|
|
1343
|
+
cursor += 1;
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (/^\s*(?:直到|直至|一直到)\s*/u.test(rest)) {
|
|
1347
|
+
cursor += 1;
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (CONTINUATION_AFTER_SEPARATOR.test(rest)) return cursor + 1;
|
|
1351
|
+
if (/^\s*(?:由|让|请|给)\s*(?:你|您|我|本人)/u.test(rest)) return cursor + 1;
|
|
1352
|
+
CROSS_CLAUSE_HEAD.test(rest);
|
|
1353
|
+
const locative = LOCATIVE_CLAUSE.test(rest);
|
|
1354
|
+
const conjunctionSeparator = character === "并" || character === "且";
|
|
1355
|
+
const comma = character === "," || character === ",";
|
|
1356
|
+
const enumeration = character === "、";
|
|
1357
|
+
if (comma) {
|
|
1358
|
+
if (!(conjunctionSeparator || CONTINUATION_AFTER_SEPARATOR.test(rest))) {
|
|
1359
|
+
cursor += 1;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
return cursor;
|
|
1363
|
+
}
|
|
1364
|
+
if (enumeration) {
|
|
1365
|
+
if (!locative) {
|
|
1366
|
+
cursor += 1;
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
return cursor;
|
|
1370
|
+
}
|
|
1371
|
+
if (!(DISTINCT_CLAUSE_HEAD.test(rest) || locative)) {
|
|
1372
|
+
cursor += 1;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
return cursor;
|
|
1376
|
+
}
|
|
1377
|
+
return masked.length;
|
|
1378
|
+
}
|
|
1379
|
+
function conditionSplit(text, conditionPrefix, verb, options = {}) {
|
|
1380
|
+
const masked = maskCodeSpans(text);
|
|
1381
|
+
const lower = masked.toLowerCase();
|
|
1382
|
+
if (conditionPrefix === void 0) return suffixConditionSplit(text, lower, masked, options);
|
|
1383
|
+
if (verb < 0) return void 0;
|
|
1384
|
+
const candidates = conditionCandidates(text, lower, masked, conditionPrefix);
|
|
1385
|
+
if (candidates.length === 0) return void 0;
|
|
1386
|
+
const guardedScope = candidates[0];
|
|
1387
|
+
const markerIndex = guardedScope.start ?? 0;
|
|
1388
|
+
const guardedStart = guardedScope.start ?? 0;
|
|
1389
|
+
const condition = guardedScope.condition ?? "";
|
|
1390
|
+
if (!condition.trim() || !guardedScope.text.trim()) return void 0;
|
|
1391
|
+
const scopes = [];
|
|
1392
|
+
const clauseStart = lastBoundaryIndex(text, markerIndex);
|
|
1393
|
+
const lead = text.slice(0, clauseStart).trim();
|
|
1394
|
+
if (lead) scopes.push(...scopeOf(lead, options));
|
|
1395
|
+
const conditionClause = text.slice(clauseStart, guardedStart).trim();
|
|
1396
|
+
if (conditionClause) scopes.push({
|
|
1397
|
+
text: conditionClause,
|
|
1398
|
+
body: conditionClause,
|
|
1399
|
+
directive: "conditional",
|
|
1400
|
+
condition: condition.trim(),
|
|
1401
|
+
start: clauseStart
|
|
1402
|
+
});
|
|
1403
|
+
scopes.push({
|
|
1404
|
+
text: guardedScope.text.trim(),
|
|
1405
|
+
body: guardedScope.text.replace(/^[\s,,、;;::]+/, "").replace(/^(?:才|再|就|则|即)\s*/, "").trim(),
|
|
1406
|
+
directive: "directive",
|
|
1407
|
+
condition: condition.trim(),
|
|
1408
|
+
start: guardedStart
|
|
1409
|
+
});
|
|
1410
|
+
return scopes;
|
|
1411
|
+
}
|
|
1412
|
+
/** Index just past the last clause separator at or before `index`. */
|
|
1413
|
+
function lastBoundaryIndex(text, index) {
|
|
1414
|
+
let cursor = index;
|
|
1415
|
+
while (cursor > 0) {
|
|
1416
|
+
const character = text[cursor - 1];
|
|
1417
|
+
if (character === "在") break;
|
|
1418
|
+
if (character === ";" || character === ";" || character === "。" || character === "!" || character === "?" || character === "!" || character === "?" || character === "\n" || character === "\r") return cursor;
|
|
1419
|
+
cursor -= 1;
|
|
1420
|
+
}
|
|
1421
|
+
return 0;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* A trailing condition marker ("…才…") needs no prefix marker when it sits
|
|
1425
|
+
* directly before the action it guards: "收到我的明确回报后再继续".
|
|
1426
|
+
*/
|
|
1427
|
+
function suffixConditionSplit(text, lower, masked, options = {}) {
|
|
1428
|
+
const verb = firstActionVerb(masked);
|
|
1429
|
+
if (verb < 0) return void 0;
|
|
1430
|
+
const marker = /(?:之后|以后|后再|后才|再继续|才继续|再|才)/u.exec(text.slice(verb));
|
|
1431
|
+
if (!marker) return void 0;
|
|
1432
|
+
const condition = trimConditionTail(text.slice(0, verb + marker.index));
|
|
1433
|
+
const guarded = text.slice(verb + marker[0].length);
|
|
1434
|
+
if (!condition || !guarded.trim()) return void 0;
|
|
1435
|
+
return [{
|
|
1436
|
+
text: condition,
|
|
1437
|
+
body: condition,
|
|
1438
|
+
directive: "conditional",
|
|
1439
|
+
condition
|
|
1440
|
+
}, {
|
|
1441
|
+
text: guarded.trim(),
|
|
1442
|
+
body: guarded.replace(/^[\s,,、;;::]+/, "").replace(/^(?:才|再|就|则|即)\s*/, "").trim(),
|
|
1443
|
+
directive: "directive",
|
|
1444
|
+
condition,
|
|
1445
|
+
start: verb + marker[0].length
|
|
1446
|
+
}];
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* The condition clauses inside one clause run, each paired with the action it
|
|
1450
|
+
* guards. A marker that appears after the action's own verb but allows nothing
|
|
1451
|
+
* before that verb is not a condition at all — "confirm" contains "if", and
|
|
1452
|
+
* "We ship after the test passes" carries a subject the marker does not guard.
|
|
1453
|
+
*/
|
|
1454
|
+
function conditionCandidates(text, lower, masked, conditionPrefix) {
|
|
1455
|
+
const clauseStart = lastBoundaryIndex(text, conditionPrefix);
|
|
1456
|
+
const clause = lower.slice(clauseStart);
|
|
1457
|
+
const candidates = [];
|
|
1458
|
+
for (const [token, kind] of CONDITION_MARKERS) {
|
|
1459
|
+
if (kind !== "prefix" || token === "在") continue;
|
|
1460
|
+
const index = prefixIndexOf(clause, token);
|
|
1461
|
+
if (index < 0) continue;
|
|
1462
|
+
const absolute = clauseStart + index;
|
|
1463
|
+
if (firstNegation(masked.slice(absolute))) continue;
|
|
1464
|
+
let after = firstActionVerb(masked, absolute + token.length);
|
|
1465
|
+
if (after < 0) {
|
|
1466
|
+
const tail = expressionTailVerb(masked, absolute + token.length);
|
|
1467
|
+
if (tail >= absolute + token.length) after = tail;
|
|
1468
|
+
}
|
|
1469
|
+
const before = firstActionVerb(masked, clauseStart, absolute);
|
|
1470
|
+
if (after >= 0 && masked.slice(absolute + token.length, after).trim().length === 0) {
|
|
1471
|
+
candidates.push({
|
|
1472
|
+
markerAt: absolute,
|
|
1473
|
+
condition: trimConditionTail(text.slice(clauseStart, absolute)),
|
|
1474
|
+
guarded: text.slice(after),
|
|
1475
|
+
guardedAt: after
|
|
1476
|
+
});
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (after < 0) {
|
|
1480
|
+
if (before < 0 || !/^(?:[\p{L}\p{N}]+[\s]*){0,3}[\p{L}\p{N}]+$/u.test(text.slice(clauseStart, absolute).trim())) continue;
|
|
1481
|
+
candidates.push({
|
|
1482
|
+
markerAt: absolute,
|
|
1483
|
+
condition: trimConditionTail(text.slice(absolute + token.length)),
|
|
1484
|
+
guarded: text.slice(clauseStart, absolute).trim(),
|
|
1485
|
+
guardedAt: clauseStart
|
|
1486
|
+
});
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (absolute >= before && before >= 0) continue;
|
|
1490
|
+
candidates.push({
|
|
1491
|
+
markerAt: absolute,
|
|
1492
|
+
condition: trimConditionTail(text.slice(absolute + token.length, after)),
|
|
1493
|
+
guarded: text.slice(after),
|
|
1494
|
+
guardedAt: after
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
if (candidates.length === 0) return [];
|
|
1498
|
+
const best = candidates.reduce((left, right) => right.markerAt < left.markerAt ? right : left);
|
|
1499
|
+
const guarded = {
|
|
1500
|
+
text: best.guarded.trim(),
|
|
1501
|
+
body: best.guarded.replace(/^[\s,,、;;::]+/, "").replace(/^(?:才|再|就|则|即)\s*/, "").trim(),
|
|
1502
|
+
directive: "directive",
|
|
1503
|
+
condition: best.condition,
|
|
1504
|
+
start: best.guardedAt
|
|
1505
|
+
};
|
|
1506
|
+
return best.markerAt > 0 ? [guarded] : [guarded];
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* The condition a marker supplies: everything between the clause start and the
|
|
1510
|
+
* guarded action, without the connector that introduces the ban ("除非…否则不要
|
|
1511
|
+
* 合并" → "除非我明确说可以").
|
|
1512
|
+
*/
|
|
1513
|
+
function stripConditionConnector(value) {
|
|
1514
|
+
return value.replace(/^[\s,,、;;::]+/, "").replace(/[\s,,、;;::]*(?:否则|不然|then|otherwise)[\s,,、;;:]*$/i, "").trim();
|
|
1515
|
+
}
|
|
1516
|
+
/** Drop the temporal tail a condition marker may leave behind ("之后", "以后"). */
|
|
1517
|
+
function trimConditionTail(value) {
|
|
1518
|
+
return value.replace(/[\s,,、;;::]+$/, "").replace(/(?:之后|以后|后)$/, "").trim();
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* A known action word at the end of the clause, used when the guarded action is
|
|
1522
|
+
* expressed as a plain Chinese verb: "若…才推送" ends in 推送, which the action
|
|
1523
|
+
* surface does not treat as a verb because it is the object of 才. Only a closed
|
|
1524
|
+
* vocabulary is accepted, so ordinary prose is never mistaken for an action.
|
|
1525
|
+
*/
|
|
1526
|
+
const BOUND_ACTION_TAIL = /(创建|生成|写入|修改|编辑|运行|执行|编写|撰写|部署|安装|升级|提交|下载|上传|拉取|同步|重启|测试|检查|验证|确认|修复|更新|清理|整理|记录|构建|编译|重构|迁移|删除|回滚|发布|推送|实现|合并|提交|回退|检查)[。..!!??,,;;、\s]*$/u;
|
|
1527
|
+
function expressionTailVerb(masked, from) {
|
|
1528
|
+
const slice = masked.slice(from);
|
|
1529
|
+
const match = BOUND_ACTION_TAIL.exec(slice);
|
|
1530
|
+
return match ? from + match.index : -1;
|
|
1531
|
+
}
|
|
1532
|
+
/** Index of the first prefix condition marker, skipping a locative 在. */
|
|
1533
|
+
function prefixConditionIndex(lower) {
|
|
1534
|
+
const head = lower;
|
|
1535
|
+
let best;
|
|
1536
|
+
for (const [token, kind] of CONDITION_MARKERS) {
|
|
1537
|
+
if (kind !== "prefix" || token === "在") continue;
|
|
1538
|
+
const index = prefixIndexOf(head, token);
|
|
1539
|
+
if (index < 0) continue;
|
|
1540
|
+
if (best === void 0 || index < best) best = index;
|
|
1541
|
+
}
|
|
1542
|
+
return best;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Word-bounded matcher per English marker, compiled once. Building the pattern
|
|
1546
|
+
* inside the scan recompiled it for every marker of every scope, which
|
|
1547
|
+
* dominated capture cost on long messages.
|
|
1548
|
+
*/
|
|
1549
|
+
const ENGLISH_MARKER_MATCHERS = /* @__PURE__ */ new Map();
|
|
1550
|
+
function englishMarkerMatcher(token) {
|
|
1551
|
+
let matcher = ENGLISH_MARKER_MATCHERS.get(token);
|
|
1552
|
+
if (!matcher) {
|
|
1553
|
+
matcher = new RegExp(`(?:^|[^\\p{L}\\p{N}_])${escapeRegExp(token)}(?![\\p{L}\\p{N}_])`, "iu");
|
|
1554
|
+
ENGLISH_MARKER_MATCHERS.set(token, matcher);
|
|
1555
|
+
}
|
|
1556
|
+
return matcher;
|
|
1557
|
+
}
|
|
1558
|
+
function prefixIndexOf(text, token) {
|
|
1559
|
+
if (/^[a-z]/.test(token)) {
|
|
1560
|
+
const match = englishMarkerMatcher(token).exec(text);
|
|
1561
|
+
return match ? match.index + (match[0].length - token.length) : -1;
|
|
1562
|
+
}
|
|
1563
|
+
if (token === "在") {
|
|
1564
|
+
const match = /在[^。!?;]{0,24}?(?:之前|以前)/.exec(text);
|
|
1565
|
+
return match ? match.index : -1;
|
|
1566
|
+
}
|
|
1567
|
+
return text.indexOf(token);
|
|
1568
|
+
}
|
|
1569
|
+
function escapeRegExp(value) {
|
|
1570
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1571
|
+
}
|
|
1572
|
+
function stripNegators(text, matched) {
|
|
1573
|
+
let value = text.trim();
|
|
1574
|
+
value = value.replace(new RegExp(`^${CONNECTOR_PATTERN}\\s*`, "i"), "").trim();
|
|
1575
|
+
if (matched && value.toLowerCase().startsWith(matched.toLowerCase())) value = value.slice(matched.length);
|
|
1576
|
+
return stripNegatorsPrefix(value).trim();
|
|
1577
|
+
}
|
|
1578
|
+
function stripNegatorsPrefix(value) {
|
|
1579
|
+
let text = value;
|
|
1580
|
+
for (let guard = 0; guard < 8; guard += 1) {
|
|
1581
|
+
const trimmed = text.replace(/^[\s,,、;;::]+/, "");
|
|
1582
|
+
let changed = trimmed !== text;
|
|
1583
|
+
text = trimmed;
|
|
1584
|
+
for (const [token] of NEGATORS) {
|
|
1585
|
+
if (!text.toLowerCase().startsWith(token.toLowerCase())) continue;
|
|
1586
|
+
if (/^[a-z]/.test(token) && /[\p{L}\p{N}_]/u.test(text[token.length] ?? "")) continue;
|
|
1587
|
+
text = text.slice(token.length);
|
|
1588
|
+
changed = true;
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
if (!changed) break;
|
|
1592
|
+
}
|
|
1593
|
+
return text;
|
|
1594
|
+
}
|
|
1595
|
+
function stripConnectors(text) {
|
|
1596
|
+
return text.replace(new RegExp(`^${CONNECTOR_PATTERN}\\s*`, "i"), "").trim();
|
|
1597
|
+
}
|
|
1598
|
+
/** Classify a non-negated scope. */
|
|
1599
|
+
function classifyPositive(text) {
|
|
1600
|
+
const masked = maskCodeSpans(text);
|
|
1601
|
+
if (firstActionVerb(masked) < 0 && firstActionVerb(text) >= 0) return "informational";
|
|
1602
|
+
const explain = EXPLAIN_VERB.exec(masked);
|
|
1603
|
+
if (explain) {
|
|
1604
|
+
const verb = firstActionVerb(masked);
|
|
1605
|
+
if (verb < 0 || verb >= explain.index) return "informational";
|
|
1606
|
+
}
|
|
1607
|
+
if (QUESTION_SCOPE.test(masked)) return "informational";
|
|
1608
|
+
if (UNRESOLVED_SCOPE.test(masked)) return "informational";
|
|
1609
|
+
if ((NARRATIVE_PAST.test(masked) || NARRATIVE_ASPECT.test(masked)) && !NARRATIVE_DIRECTIVE.test(masked)) return "narrative";
|
|
1610
|
+
if (CONFIRMATION_RECEIPT.test(masked.trim())) return "narrative";
|
|
1611
|
+
return "directive";
|
|
1612
|
+
}
|
|
1613
|
+
function executeeOf(text, directive) {
|
|
1614
|
+
if (directive === "informational" || directive === "narrative" || directive === "conditional") return "unresolved";
|
|
1615
|
+
const masked = maskCodeSpans(text);
|
|
1616
|
+
if (USER_ACTOR_PATTERNS.some((pattern) => pattern.test(masked))) return "user";
|
|
1617
|
+
if (AGENT_ACTOR_PATTERNS.some((pattern) => pattern.test(masked))) return "agent";
|
|
1618
|
+
return "agent";
|
|
1619
|
+
}
|
|
1620
|
+
/** True when the scope asks for command/instruction TEXT rather than execution. */
|
|
1621
|
+
function isOutputRequest(text) {
|
|
1622
|
+
const masked = maskCodeSpans(text);
|
|
1623
|
+
return OUTPUT_NOUN.test(masked) && OUTPUT_REQUEST.test(masked);
|
|
1624
|
+
}
|
|
1625
|
+
function dispositionOf(scope, executee) {
|
|
1626
|
+
if (scope.directive === "prohibition") return "prohibition";
|
|
1627
|
+
if (scope.directive === "informational" || scope.directive === "narrative") return "informational";
|
|
1628
|
+
if (scope.directive === "conditional") return "conditional_wait";
|
|
1629
|
+
if (scope.condition) return "conditional_wait";
|
|
1630
|
+
if (executee === "user") return "human_actor";
|
|
1631
|
+
if (isOutputRequest(scope.text)) return "informational";
|
|
1632
|
+
return "executable_now";
|
|
1633
|
+
}
|
|
1634
|
+
function resumeEventOf(scope) {
|
|
1635
|
+
const match = RESUME_MARKER.exec(scope.condition ?? scope.text);
|
|
1636
|
+
return match ? match[0].trim() : void 0;
|
|
1637
|
+
}
|
|
1638
|
+
function interpret(scope) {
|
|
1639
|
+
const executee = executeeOf(scope.text, scope.directive);
|
|
1640
|
+
const resumption = scope.condition === void 0 && (scope.directive === "directive" || scope.directive === "conditional") ? resumptionConditionOf(scope) : void 0;
|
|
1641
|
+
const conditioned = resumption ? {
|
|
1642
|
+
...scope,
|
|
1643
|
+
condition: resumption
|
|
1644
|
+
} : scope;
|
|
1645
|
+
const authorityDisposition = dispositionOf(conditioned, executee);
|
|
1646
|
+
const resumeEvent = scope.directive === "directive" ? resumeEventOf(conditioned) : void 0;
|
|
1647
|
+
const method = scope.directive === "prohibition" ? void 0 : semanticMethod(scope.body);
|
|
790
1648
|
return {
|
|
791
|
-
|
|
792
|
-
body:
|
|
1649
|
+
text: scope.text,
|
|
1650
|
+
body: scope.body,
|
|
1651
|
+
directive: scope.directive,
|
|
1652
|
+
executee,
|
|
1653
|
+
...conditioned.condition ? { condition: conditioned.condition } : {},
|
|
1654
|
+
...resumeEvent ? { resumeEvent } : {},
|
|
1655
|
+
immediatelyExecutable: authorityDisposition === "executable_now",
|
|
1656
|
+
authorityDisposition,
|
|
1657
|
+
...method ? { method } : {},
|
|
1658
|
+
fingerprint: fingerprintOf([
|
|
1659
|
+
scope.text,
|
|
1660
|
+
scope.body,
|
|
1661
|
+
scope.directive,
|
|
1662
|
+
executee,
|
|
1663
|
+
authorityDisposition,
|
|
1664
|
+
conditioned.condition ?? "",
|
|
1665
|
+
resumeEvent ?? "",
|
|
1666
|
+
method ?? ""
|
|
1667
|
+
].join("\0"))
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function semanticMethod(text) {
|
|
1671
|
+
const match = /(?:用|使用|通过|借助|利用|以)\s*([A-Za-z][A-Za-z0-9_-]*)/.exec(text) ?? /\b(?:via|using|use|with)\s+(?:the\s+)?([A-Za-z][A-Za-z0-9_-]*)/i.exec(text);
|
|
1672
|
+
return match ? match[1].toLowerCase() : void 0;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* A short, stable identity for one interpretation: a content hash of the
|
|
1676
|
+
* interpretation itself, so a replay of identical bytes reproduces it exactly
|
|
1677
|
+
* and two different readings never collide.
|
|
1678
|
+
*/
|
|
1679
|
+
function fingerprintOf(source) {
|
|
1680
|
+
let hash = 2166136261;
|
|
1681
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1682
|
+
hash ^= source.charCodeAt(index);
|
|
1683
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
1684
|
+
}
|
|
1685
|
+
return `i${hash.toString(16).padStart(8, "0")}`;
|
|
1686
|
+
}
|
|
1687
|
+
/** Interpret one already-segmented clause. */
|
|
1688
|
+
function interpretClause(text, options = {}) {
|
|
1689
|
+
const normalized = normalizeClause(text);
|
|
1690
|
+
const scopes = scopeOf(normalized, options);
|
|
1691
|
+
if (scopes.length === 0) return interpret({
|
|
1692
|
+
text: normalized,
|
|
1693
|
+
body: normalized,
|
|
1694
|
+
directive: "informational"
|
|
1695
|
+
});
|
|
1696
|
+
if (scopes.length === 1) return interpret(scopes[0]);
|
|
1697
|
+
const directive = scopes.some((scope) => scope.directive === "directive") ? "directive" : scopes.some((scope) => scope.directive === "prohibition") ? "prohibition" : "informational";
|
|
1698
|
+
return interpret({
|
|
1699
|
+
text: normalized,
|
|
1700
|
+
body: scopes.map((scope) => scope.body).join(";"),
|
|
1701
|
+
directive
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
/** Interpret a whole message into independent scopes, in source order. */
|
|
1705
|
+
function interpretMessage(text, options = {}) {
|
|
1706
|
+
return scopeOf(normalizeClause(text), options).flatMap((scope) => splitTrailingResumption(scope)).map((scope) => interpret(scope));
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Whether the item is an executable obligation right now. A prohibition is a
|
|
1710
|
+
* standing constraint, a human-owned action belongs to the user, a conditional
|
|
1711
|
+
* action waits for its condition, and an explanation is not work. None of them
|
|
1712
|
+
* may block completion or be certified as agent work.
|
|
1713
|
+
*
|
|
1714
|
+
* An item without an interpretation is a legacy or fixture item created before
|
|
1715
|
+
* this module existed; it keeps its historical executable reading.
|
|
1716
|
+
*/
|
|
1717
|
+
function isExecutableItem(item) {
|
|
1718
|
+
if (item.kind === "prohibition") return false;
|
|
1719
|
+
if (item.waitAuthorization !== void 0) return false;
|
|
1720
|
+
if (item.authorityDisposition === void 0) return true;
|
|
1721
|
+
if (item.authorityDisposition !== "executable_now") return false;
|
|
1722
|
+
return item.executee === void 0 || item.executee === "agent";
|
|
1723
|
+
}
|
|
1724
|
+
/** Whether an item is an open obligation for certification purposes. */
|
|
1725
|
+
function isOpenObligation(item) {
|
|
1726
|
+
return item.status === "pending" && isExecutableItem(item);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* The action a scope names. `semanticActionFromText` maps the command surface,
|
|
1730
|
+
* but a prohibition keeps a bare verb as its body ("不要提交并推送" → 提交并推送),
|
|
1731
|
+
* and the closed CJK vocabulary is consulted first so such a ban is still
|
|
1732
|
+
* recorded against the action it forbids.
|
|
1733
|
+
*/
|
|
1734
|
+
function semanticActionOfScope(body, source = body, isProhibition = false) {
|
|
1735
|
+
const masked = maskCodeSpans(source);
|
|
1736
|
+
const negation = isProhibition ? {
|
|
1737
|
+
index: 0,
|
|
1738
|
+
token: ""
|
|
1739
|
+
} : firstNegation(masked);
|
|
1740
|
+
if (negation) {
|
|
1741
|
+
const banned = bannedVerbIndex(masked, negation.index + negation.token.length, masked.length);
|
|
1742
|
+
if (banned >= 0) {
|
|
1743
|
+
const action = semanticActionFromText(CJK_VERB_WORDS.find((entry) => masked.startsWith(entry, banned)) ?? masked.slice(banned, banned + 2));
|
|
1744
|
+
if (action !== "generic_run") return action;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return semanticActionFromText(body);
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Split a scope whose action is stated after a resumption clause: "请先测试,
|
|
1751
|
+
* 收到我的确认后再推送" runs the test now and reserves the push for the
|
|
1752
|
+
* confirmation. Only a comma-separated split is used, so the earlier action
|
|
1753
|
+
* keeps its own executable meaning and the later one waits.
|
|
1754
|
+
*/
|
|
1755
|
+
function splitTrailingResumption(scope) {
|
|
1756
|
+
if (scope.condition !== void 0 || scope.directive === "prohibition") return [scope];
|
|
1757
|
+
const masked = maskCodeSpans(scope.text);
|
|
1758
|
+
const marker = RESUMPTION_EVENT.exec(masked);
|
|
1759
|
+
if (!marker || marker.index === 0) return [scope];
|
|
1760
|
+
if (firstActionVerb(masked.slice(0, marker.index)) < 0) return [scope];
|
|
1761
|
+
const boundary = masked.slice(0, marker.index).search(/[,,][^,,]*$/);
|
|
1762
|
+
if (boundary < 0) return [scope];
|
|
1763
|
+
const head = scope.text.slice(0, boundary + 1).trim();
|
|
1764
|
+
const rest = scope.text.slice(boundary + 1).trim();
|
|
1765
|
+
if (!head || !rest) return [scope];
|
|
1766
|
+
if (firstActionVerb(maskCodeSpans(head)) < 0) return [scope];
|
|
1767
|
+
if (firstActionVerb(maskCodeSpans(rest)) < 0) return [scope];
|
|
1768
|
+
return [{
|
|
1769
|
+
text: head,
|
|
1770
|
+
body: head,
|
|
1771
|
+
directive: classifyPositive(head)
|
|
1772
|
+
}, {
|
|
1773
|
+
text: rest,
|
|
1774
|
+
body: rest,
|
|
1775
|
+
directive: "directive"
|
|
1776
|
+
}];
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Every stateful action the clause names, in source order. A clause may order
|
|
1780
|
+
* more than one ("安装插件,重启 DSH"); each is a separate evidence obligation
|
|
1781
|
+
* even though the clause stays one top-level item.
|
|
1782
|
+
*/
|
|
1783
|
+
function statefulActionsOfScope(body) {
|
|
1784
|
+
const masked = maskCodeSpans(body);
|
|
1785
|
+
const found = [];
|
|
1786
|
+
const consider = (at, word) => {
|
|
1787
|
+
const action = semanticActionFromText(word);
|
|
1788
|
+
if (isStatefulAction(action)) found.push({
|
|
1789
|
+
at,
|
|
1790
|
+
action
|
|
1791
|
+
});
|
|
793
1792
|
};
|
|
1793
|
+
for (const word of CJK_VERB_WORDS) {
|
|
1794
|
+
let at = masked.indexOf(word);
|
|
1795
|
+
while (at >= 0) {
|
|
1796
|
+
consider(at, word);
|
|
1797
|
+
at = masked.indexOf(word, at + word.length);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
for (const match of masked.matchAll(/\b(?:install|apply|restart|reload|commit|push|publish|pull|fetch|create|modify|edit)\b/gi)) consider(match.index, match[0]);
|
|
1801
|
+
found.sort((a, b) => a.at - b.at);
|
|
1802
|
+
const ordered = [];
|
|
1803
|
+
for (const entry of found) if (ordered.at(-1) !== entry.action) ordered.push(entry.action);
|
|
1804
|
+
return ordered;
|
|
1805
|
+
}
|
|
1806
|
+
/** Actions this interpretation names, in source order (diagnostics only). */
|
|
1807
|
+
function namedActions(text) {
|
|
1808
|
+
return interpretMessage(text).map((scope) => semanticActionOfScope(scope.body)).filter((action) => action !== "generic_run");
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
//#endregion
|
|
1812
|
+
//#region src/domain/capture.ts
|
|
1813
|
+
/**
|
|
1814
|
+
* Whether a clause opens with an explicit ban. The lane question ("is this a
|
|
1815
|
+
* constraint or a duty?") is answered by {@link ScopeInterpretation}; this stays
|
|
1816
|
+
* exported because the framing/segmentation callers ask it directly.
|
|
1817
|
+
*/
|
|
1818
|
+
function classifyClause(text) {
|
|
1819
|
+
const [first] = interpretMessage(normalizeClause(text));
|
|
1820
|
+
return first ? kindOfScope(first.directive, first.body) : "requirement";
|
|
794
1821
|
}
|
|
795
1822
|
const METHOD_TOOL = "(?:bash|shell|powershell|pwsh|git|read|write|edit|node|python|python3|npm|pnpm|tsc|vitest)";
|
|
796
1823
|
const METHOD_ALIASES = {
|
|
@@ -845,21 +1872,71 @@ function isInformationalMessage(text) {
|
|
|
845
1872
|
function extractOperation(text) {
|
|
846
1873
|
for (const [operation, pattern] of OPERATION_PATTERNS) if (pattern.test(text)) return operation;
|
|
847
1874
|
}
|
|
848
|
-
|
|
1875
|
+
/**
|
|
1876
|
+
* A target token, as a human writes it.
|
|
1877
|
+
*
|
|
1878
|
+
* A path and a bare word do not start the same way: "/work/repo" and "./repo"
|
|
1879
|
+
* open with a separator, so a leading character class of letters, digits and
|
|
1880
|
+
* "@" cannot match them at all, and the field silently falls back to an
|
|
1881
|
+
* unrelated value. Paths therefore get their own branch, which requires the
|
|
1882
|
+
* separator plus at least one more character — a lone "/" is punctuation, not
|
|
1883
|
+
* a path.
|
|
1884
|
+
*/
|
|
1885
|
+
const TARGET_TAIL = "[\\p{L}\\p{N}@._/\\\\:+%?&=#\\[\\]-]";
|
|
1886
|
+
const TARGET_TOKEN = `(?:\`[^\`]+\`|"[^"]+"|'[^']+'|${`[.~]*[\\\\/]${TARGET_TAIL}+`}|${`[\\p{L}\\p{N}@]${TARGET_TAIL}*`})`;
|
|
849
1887
|
function unquoteTargetToken(value) {
|
|
850
1888
|
if (!value) return void 0;
|
|
851
1889
|
const trimmed = value.trim().replace(/[.,;,。;]+$/, "");
|
|
852
1890
|
const unquoted = /^(?:`([^`]+)`|"([^"]+)"|'([^']+)')$/.exec(trimmed);
|
|
853
1891
|
return (unquoted?.[1] ?? unquoted?.[2] ?? unquoted?.[3] ?? trimmed) || void 0;
|
|
854
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* The value of a labelled field ("repository X", "版本:1.2.3").
|
|
1895
|
+
*
|
|
1896
|
+
* The label must END where it ends: a label that is only a prefix of a longer
|
|
1897
|
+
* word is skipped, so the literal word "repository" is never read as the label
|
|
1898
|
+
* "repo" followed by the value "sitory".
|
|
1899
|
+
*/
|
|
855
1900
|
function labeledToken(text, labels) {
|
|
856
|
-
|
|
1901
|
+
const label = new RegExp(`(?:${labels})`, "iu");
|
|
1902
|
+
const after = new RegExp(`^(?![\\p{L}\\p{N}_])\\s*(?:[:=:]|为|是)?\\s*(${TARGET_TOKEN})`, "iu");
|
|
1903
|
+
const OTHER_LABEL = /^(?:to|from|on|into|with|at|using|version|profile|registry|remote|refspec|branch|service|repository|repo|包|插件|制品|服务|仓库|版本|配置档|远端|分支|注册表)$/i;
|
|
1904
|
+
let cursor = 0;
|
|
1905
|
+
while (cursor <= text.length) {
|
|
1906
|
+
const match = label.exec(text.slice(cursor));
|
|
1907
|
+
if (!match) return void 0;
|
|
1908
|
+
cursor = cursor + match.index + match[0].length;
|
|
1909
|
+
const value = unquoteTargetToken(after.exec(text.slice(cursor))?.[1]);
|
|
1910
|
+
if (value && !OTHER_LABEL.test(value)) return value;
|
|
1911
|
+
if (cursor >= text.length) return void 0;
|
|
1912
|
+
}
|
|
857
1913
|
}
|
|
1914
|
+
/**
|
|
1915
|
+
* The object a verb acts on. The verb is matched first, then — separately — an
|
|
1916
|
+
* optional noun that has to end at a word boundary, and only the text AFTER
|
|
1917
|
+
* that noun is the target. Matching the noun and the token in one pattern let
|
|
1918
|
+
* the noun eat a prefix of the real word ("repository" consumed as "repo" +
|
|
1919
|
+
* "sitory"), which captured "sitory" as a repository name.
|
|
1920
|
+
*/
|
|
858
1921
|
function actionObjectToken(text, verbs, nouns) {
|
|
859
|
-
const
|
|
1922
|
+
const verb = new RegExp(`(?:${verbs})`, "iu").exec(text);
|
|
1923
|
+
if (!verb) return void 0;
|
|
1924
|
+
let cursor = verb.index + verb[0].length;
|
|
1925
|
+
const noun = new RegExp(`^\\s*(?:${nouns})(?![\\p{L}\\p{N}_])`, "iu").exec(text.slice(cursor));
|
|
1926
|
+
if (noun) cursor += noun[0].length;
|
|
1927
|
+
else cursor += text.slice(cursor).match(/^\s*[\p{Script=Han}]{0,2}\s*/u)?.[0].length ?? 0;
|
|
1928
|
+
const rest = text.slice(cursor).replace(/^\s*(?:[:=:]|为)?\s*/u, "");
|
|
1929
|
+
const token = unquoteTargetToken(new RegExp(`^(${TARGET_TOKEN})`, "u").exec(rest)?.[1]);
|
|
860
1930
|
if (!token || /^(?:the|a|an|this|that|to|from|in|on|into|with|package|plugin|artifact|service|repository|repo|包|插件|制品|服务|仓库)$/i.test(token)) return void 0;
|
|
861
1931
|
return token;
|
|
862
1932
|
}
|
|
1933
|
+
/** The verbs that name each repository-facing action, for unlabelled objects. */
|
|
1934
|
+
const GIT_OBJECT_VERB = {
|
|
1935
|
+
push: "push|推送",
|
|
1936
|
+
pull: "pull|拉取",
|
|
1937
|
+
fetch: "fetch|抓取|获取",
|
|
1938
|
+
commit: "commit|提交"
|
|
1939
|
+
};
|
|
863
1940
|
function splitPackageSpec(spec) {
|
|
864
1941
|
if (!spec) return {};
|
|
865
1942
|
const at = spec.lastIndexOf("@");
|
|
@@ -927,7 +2004,7 @@ function captureRequestedTarget(action, text, subject, surface) {
|
|
|
927
2004
|
} };
|
|
928
2005
|
}
|
|
929
2006
|
if (action === "pull" || action === "fetch" || action === "commit" || action === "push") {
|
|
930
|
-
const repository = labeledToken(text, "repository|repo|仓库") ?? (subject !== "scope" ? subject : void 0);
|
|
2007
|
+
const repository = labeledToken(text, "repository|repo|仓库") ?? actionObjectToken(text, GIT_OBJECT_VERB[action], "repository|repo|仓库") ?? (subject !== "scope" ? subject : void 0);
|
|
931
2008
|
if (!repository) return {
|
|
932
2009
|
target: {},
|
|
933
2010
|
reasonCode: "requested_target_repository_missing"
|
|
@@ -966,29 +2043,29 @@ function extractArtifactPaths(text) {
|
|
|
966
2043
|
}
|
|
967
2044
|
return [...found];
|
|
968
2045
|
}
|
|
969
|
-
function segmentClauses(text,
|
|
2046
|
+
function segmentClauses(text, options = {}) {
|
|
970
2047
|
const normalized = normalizeClause(text);
|
|
971
2048
|
if (!normalized) return [];
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
paths: extractArtifactPaths(body)
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
return segments;
|
|
2049
|
+
return interpretMessage(normalized, options).filter((interpretation) => interpretation.text.trim().length > 0).map((interpretation) => ({
|
|
2050
|
+
kind: kindOfScope(interpretation.directive, interpretation.body),
|
|
2051
|
+
body: interpretation.body,
|
|
2052
|
+
text: interpretation.text,
|
|
2053
|
+
paths: extractArtifactPaths(interpretation.body),
|
|
2054
|
+
interpretation
|
|
2055
|
+
}));
|
|
983
2056
|
}
|
|
984
2057
|
/**
|
|
985
2058
|
* Build a GuardItem from an already-classified clause body and a resolved
|
|
986
2059
|
* verification subject/surface.
|
|
2060
|
+
*
|
|
2061
|
+
* The optional `interpretation` carries the scope reading taken from the same
|
|
2062
|
+
* bytes. It is passed through rather than re-derived, so the obligation lane and
|
|
2063
|
+
* the authority of one clause cannot disagree between callers.
|
|
987
2064
|
*/
|
|
988
|
-
function captureItem(kind, body, sourceMessageId, id, revision, subject, surface, method, operation,
|
|
2065
|
+
function captureItem(kind, body, sourceMessageId, id, revision, subject, surface, method, operation, interpretation) {
|
|
989
2066
|
const sanitized = sanitizeClauseText(body);
|
|
990
|
-
const unsupportedVisual =
|
|
991
|
-
const semanticAction = unsupportedVisual ? "generic_run" :
|
|
2067
|
+
const unsupportedVisual = /\bGUI\b|界面|视觉|截图|颜色|布局|视觉效果/i.test(sanitized);
|
|
2068
|
+
const semanticAction = unsupportedVisual ? "generic_run" : semanticActionOfScope(sanitized, interpretation?.text ?? sanitized, kind === "prohibition");
|
|
992
2069
|
const capturedTarget = captureRequestedTarget(semanticAction, sanitized, subject, surface);
|
|
993
2070
|
const effectiveOperation = semanticAction === "verify" ? "verify" : operation;
|
|
994
2071
|
const item = {
|
|
@@ -1015,9 +2092,18 @@ function captureItem(kind, body, sourceMessageId, id, revision, subject, surface
|
|
|
1015
2092
|
targetCaptureStatus: capturedTarget.reasonCode ? "clarification_required" : "resolved",
|
|
1016
2093
|
...capturedTarget.reasonCode ? { targetCaptureReasonCode: capturedTarget.reasonCode } : {},
|
|
1017
2094
|
taskKind: kind === "prohibition" ? void 0 : classifyTaskIntent(sanitized),
|
|
1018
|
-
authority: "root_instruction"
|
|
2095
|
+
authority: "root_instruction",
|
|
2096
|
+
...kind === "requirement" ? buildActionPlan(sanitized, subject, surface, semanticAction) : {},
|
|
2097
|
+
...interpretation ? {
|
|
2098
|
+
directive: interpretation.directive,
|
|
2099
|
+
executee: interpretation.executee,
|
|
2100
|
+
authorityDisposition: interpretation.authorityDisposition,
|
|
2101
|
+
...interpretation.condition ? { condition: interpretation.condition } : {},
|
|
2102
|
+
...interpretation.resumeEvent ? { resumeEvent: interpretation.resumeEvent } : {},
|
|
2103
|
+
interpretationFingerprint: interpretation.fingerprint
|
|
2104
|
+
} : {}
|
|
1019
2105
|
};
|
|
1020
|
-
if (/(?:等待|暂停|等).{0,12}(?:用户|你|您|我).{0,12}(?:选择|确认|输入)(?:.{0,8}(?:后|再)?继续)?|收到.{0,8}(?:用户|你|您|我)?的?确认.{0,8}(?:后)?再继续|\bwait for (?:the )?(?:user|your)\b|\bcontinue only after (?:the )?(?:user's?|your) confirmation\b/i.test(sanitized)) item.waitAuthorization = {
|
|
2106
|
+
if ((interpretation ? interpretation.authorityDisposition !== "executable_now" && (interpretation.resumeEvent !== void 0 || interpretation.authorityDisposition === "conditional_wait") : false) || /(?:等待|暂停|等).{0,12}(?:用户|你|您|我).{0,12}(?:选择|确认|输入)(?:.{0,8}(?:后|再)?继续)?|收到.{0,8}(?:用户|你|您|我)?的?确认.{0,8}(?:后)?再继续|\bwait for (?:the )?(?:user|your)\b|\bcontinue only after (?:the )?(?:user's?|your) confirmation\b/i.test(sanitized)) item.waitAuthorization = {
|
|
1021
2107
|
kind: "root_explicit_wait",
|
|
1022
2108
|
id: `wait:${id}:${sha256(sanitized).slice(0, 12)}`
|
|
1023
2109
|
};
|
|
@@ -1035,16 +2121,33 @@ function captureItem(kind, body, sourceMessageId, id, revision, subject, surface
|
|
|
1035
2121
|
};
|
|
1036
2122
|
return item;
|
|
1037
2123
|
}
|
|
2124
|
+
/** The stateful actions a clause names, with the target captured for each. */
|
|
2125
|
+
function buildActionPlan(body, subject, surface, primary) {
|
|
2126
|
+
const actions = statefulActionsOfScope(body);
|
|
2127
|
+
if (actions.length === 0 && isStatefulAction(primary)) actions.push(primary);
|
|
2128
|
+
if (actions.length <= 1) return {};
|
|
2129
|
+
return { actionPlan: actions.map((action) => {
|
|
2130
|
+
const captured = captureRequestedTarget(action, body, subject, surface);
|
|
2131
|
+
return {
|
|
2132
|
+
action,
|
|
2133
|
+
requestedTarget: captured.target,
|
|
2134
|
+
targetCaptureStatus: captured.reasonCode ? "clarification_required" : "resolved",
|
|
2135
|
+
...captured.reasonCode ? { targetCaptureReasonCode: captured.reasonCode } : {}
|
|
2136
|
+
};
|
|
2137
|
+
}) };
|
|
2138
|
+
}
|
|
1038
2139
|
/**
|
|
1039
2140
|
* Capture one contract clause. Every captured item receives a concrete
|
|
1040
2141
|
* verification contract: a named artifact path (artifact surface) or the
|
|
1041
2142
|
* session scope (scope surface), so an unrelated file read can never close it.
|
|
1042
2143
|
*/
|
|
1043
|
-
function captureClause(text, sourceMessageId, id, revision, scope = {}) {
|
|
1044
|
-
const
|
|
2144
|
+
function captureClause(text, sourceMessageId, id, revision, scope = {}, options = {}) {
|
|
2145
|
+
const [interpretation] = interpretMessage(text, options);
|
|
2146
|
+
const kind = interpretation ? kindOfScope(interpretation.directive, interpretation.body) : "requirement";
|
|
2147
|
+
const body = interpretation?.body ?? text;
|
|
1045
2148
|
const path$1 = extractArtifactPaths(sanitizeClauseText(body))[0] ?? "";
|
|
1046
2149
|
const surface = path$1 ? "artifact" : "scope";
|
|
1047
|
-
return captureItem(kind, body, sourceMessageId, id, revision, path$1 || scope.cwd || "scope", surface, extractMethod(body), extractOperation(body));
|
|
2150
|
+
return captureItem(kind, body, sourceMessageId, id, revision, path$1 || scope.cwd || "scope", surface, interpretation?.method ?? extractMethod(body), extractOperation(body), interpretation);
|
|
1048
2151
|
}
|
|
1049
2152
|
|
|
1050
2153
|
//#endregion
|
|
@@ -1115,6 +2218,19 @@ function deriveItemDiagnosis(p, item) {
|
|
|
1115
2218
|
},
|
|
1116
2219
|
attempt_fingerprint: fingerprint(p, item, "certified")
|
|
1117
2220
|
};
|
|
2221
|
+
if (item.status === "pending" && item.waitAuthorization?.kind === "root_explicit_wait") return {
|
|
2222
|
+
...base,
|
|
2223
|
+
certification: "unavailable",
|
|
2224
|
+
reason_code: "root_condition_pending",
|
|
2225
|
+
repairability: "user_input_required",
|
|
2226
|
+
missing_fields: [],
|
|
2227
|
+
missing_facets: [],
|
|
2228
|
+
next_action: {
|
|
2229
|
+
kind: "none",
|
|
2230
|
+
resume_condition: `Wait for the matching trusted root input: ${item.resumeEvent ?? item.condition ?? item.normalizedText}. Keep this obligation pending; do not execute it or collect effect evidence before release.`
|
|
2231
|
+
},
|
|
2232
|
+
attempt_fingerprint: fingerprint(p, item, "root_condition_pending")
|
|
2233
|
+
};
|
|
1118
2234
|
if (action !== "generic_run" && !item.legacyFlags?.length && item.targetCaptureStatus === "clarification_required") {
|
|
1119
2235
|
const missingFields = item.targetCaptureReasonCode ? [TARGET_FIELD_REASONS[item.targetCaptureReasonCode] ?? item.targetCaptureReasonCode] : [];
|
|
1120
2236
|
return {
|
|
@@ -1630,11 +2746,11 @@ function rebindResponse(p, args) {
|
|
|
1630
2746
|
revision: item.revision,
|
|
1631
2747
|
kind: item.kind,
|
|
1632
2748
|
status: item.status,
|
|
1633
|
-
semantic_action: item.semanticAction,
|
|
1634
|
-
target_capture_status: item.targetCaptureStatus
|
|
2749
|
+
...item.semanticAction !== void 0 ? { semantic_action: item.semanticAction } : {},
|
|
2750
|
+
...item.targetCaptureStatus !== void 0 ? { target_capture_status: item.targetCaptureStatus } : {}
|
|
1635
2751
|
},
|
|
1636
2752
|
diagnosis: deriveItemDiagnosis(p, item),
|
|
1637
|
-
pending_proposal_id: pendingProposal
|
|
2753
|
+
...pendingProposal ? { pending_proposal_id: pendingProposal.id } : {}
|
|
1638
2754
|
};
|
|
1639
2755
|
}
|
|
1640
2756
|
const proposal = p.rebindProposals.get(args.proposal_id ?? "");
|
|
@@ -1810,6 +2926,8 @@ function createProjection() {
|
|
|
1810
2926
|
lastGuardEventSeq: -1,
|
|
1811
2927
|
continuationAttempts: /* @__PURE__ */ new Map(),
|
|
1812
2928
|
persistenceCorrectionAttempts: /* @__PURE__ */ new Map(),
|
|
2929
|
+
noProgressClaims: /* @__PURE__ */ new Map(),
|
|
2930
|
+
handledControlSeqs: /* @__PURE__ */ new Set(),
|
|
1813
2931
|
rebindRejections: /* @__PURE__ */ new Map(),
|
|
1814
2932
|
integrity: "valid"
|
|
1815
2933
|
};
|
|
@@ -1836,52 +2954,593 @@ function currentContractDigest(projection) {
|
|
|
1836
2954
|
}
|
|
1837
2955
|
|
|
1838
2956
|
//#endregion
|
|
1839
|
-
//#region src/domain/
|
|
1840
|
-
/**
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
"
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2957
|
+
//#region src/domain/boundary.ts
|
|
2958
|
+
/** Bounded, replay-derived qualifications that callers may cite verbatim. */
|
|
2959
|
+
function availableBoundaryQualifications(projection) {
|
|
2960
|
+
const rows = [];
|
|
2961
|
+
for (const item of projection.items.values()) {
|
|
2962
|
+
if (item.status !== "pending") continue;
|
|
2963
|
+
if (item.waitAuthorization) rows.push({
|
|
2964
|
+
id: item.waitAuthorization.id,
|
|
2965
|
+
kind: item.waitAuthorization.kind,
|
|
2966
|
+
disposition: "user_wait",
|
|
2967
|
+
source: "root_contract",
|
|
2968
|
+
status: "pending"
|
|
2969
|
+
});
|
|
2970
|
+
if (item.deferAuthorization) rows.push({
|
|
2971
|
+
id: item.deferAuthorization.id,
|
|
2972
|
+
kind: item.deferAuthorization.kind,
|
|
2973
|
+
disposition: "deferred",
|
|
2974
|
+
source: "root_contract",
|
|
2975
|
+
status: "pending"
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
for (const operation of projection.externalOperations.values()) {
|
|
2979
|
+
if (operation.epoch !== projection.epoch || operation.status !== "pending" && operation.status !== "running") continue;
|
|
2980
|
+
rows.push({
|
|
2981
|
+
id: operation.id,
|
|
2982
|
+
kind: "external_operation_pending",
|
|
2983
|
+
disposition: "external_wait",
|
|
2984
|
+
source: "trusted_adapter",
|
|
2985
|
+
status: operation.status
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
return rows.sort((a, b) => a.id.localeCompare(b.id)).slice(0, 32);
|
|
2989
|
+
}
|
|
2990
|
+
function qualificationReason(projection, request) {
|
|
2991
|
+
const ids = new Set(request.qualificationIds);
|
|
2992
|
+
if (ids.size !== request.qualificationIds.length || ids.size === 0) return "boundary_qualification_ids_invalid";
|
|
2993
|
+
if (request.disposition === "user_wait") {
|
|
2994
|
+
if (request.qualificationKind !== "root_explicit_wait" && request.qualificationKind !== "user_decision_item") return "boundary_qualification_kind_mismatch";
|
|
2995
|
+
const known$1 = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.waitAuthorization?.kind === request.qualificationKind).map((item) => item.waitAuthorization.id));
|
|
2996
|
+
return request.qualificationIds.every((id) => known$1.has(id)) ? void 0 : "boundary_disposition_unqualified";
|
|
2997
|
+
}
|
|
2998
|
+
if (request.disposition === "external_wait") {
|
|
2999
|
+
if (request.qualificationKind !== "external_operation_pending") return "boundary_qualification_kind_mismatch";
|
|
3000
|
+
return request.qualificationIds.every((id) => {
|
|
3001
|
+
const operation = projection.externalOperations.get(id);
|
|
3002
|
+
return operation?.epoch === projection.epoch && (operation.status === "running" || operation.status === "pending");
|
|
3003
|
+
}) ? void 0 : "boundary_disposition_unqualified";
|
|
3004
|
+
}
|
|
3005
|
+
if (request.disposition === "guard_bounded_stop") {
|
|
3006
|
+
if (request.qualificationKind !== "guard_no_progress") return "boundary_qualification_kind_mismatch";
|
|
3007
|
+
const fingerprint$1 = request.qualificationIds.length === 1 ? request.qualificationIds[0] : void 0;
|
|
3008
|
+
if (!fingerprint$1 || fingerprint$1 !== progressFingerprint(projection)) return "boundary_disposition_unqualified";
|
|
3009
|
+
return (projection.noProgressClaims.get(fingerprint$1)?.size ?? 0) >= NO_PROGRESS_TURNS_BEFORE_STOP - 1 ? void 0 : "boundary_disposition_unqualified";
|
|
3010
|
+
}
|
|
3011
|
+
if (request.qualificationKind !== "root_explicit_defer") return "boundary_qualification_kind_mismatch";
|
|
3012
|
+
const known = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.deferAuthorization?.kind === request.qualificationKind).map((item) => item.deferAuthorization.id));
|
|
3013
|
+
return request.qualificationIds.every((id) => known.has(id)) ? void 0 : "boundary_disposition_unqualified";
|
|
3014
|
+
}
|
|
3015
|
+
function qualifyBoundary(projection, request) {
|
|
3016
|
+
const contractSha256 = currentContractDigest(projection);
|
|
3017
|
+
const reason = projection.integrity !== "valid" ? "boundary_integrity_invalid" : projection.hostStatus !== "supported" && projection.currentGoalRef ? "boundary_host_lock_unsupported" : qualificationReason(projection, request);
|
|
3018
|
+
const manifest = {
|
|
3019
|
+
protocolVersion: "1",
|
|
3020
|
+
disposition: request.disposition,
|
|
3021
|
+
qualificationKind: request.qualificationKind,
|
|
3022
|
+
qualificationIds: [...request.qualificationIds].sort(),
|
|
3023
|
+
epoch: projection.epoch,
|
|
3024
|
+
contractRevision: projection.contractRevision,
|
|
3025
|
+
contractSha256,
|
|
3026
|
+
goalRef: projection.currentGoalRef ?? null
|
|
3027
|
+
};
|
|
3028
|
+
const candidateSha256 = sha256(JSON.stringify(manifest));
|
|
3029
|
+
return {
|
|
3030
|
+
protocolVersion: "1",
|
|
3031
|
+
id: `B${projection.boundaries.length + 1}`,
|
|
3032
|
+
disposition: request.disposition,
|
|
3033
|
+
qualificationKind: request.qualificationKind,
|
|
3034
|
+
qualificationIds: [...request.qualificationIds],
|
|
3035
|
+
epoch: projection.epoch,
|
|
3036
|
+
contractRevision: projection.contractRevision,
|
|
3037
|
+
contractSha256,
|
|
3038
|
+
...projection.currentGoalRef ? { goalRef: { ...projection.currentGoalRef } } : {},
|
|
3039
|
+
candidateSha256,
|
|
3040
|
+
...request.callId ? { callId: request.callId } : {},
|
|
3041
|
+
persistedResult: reason ? "rejected" : "accepted",
|
|
3042
|
+
reasonCode: reason ?? "boundary_persisted_accepted"
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Reconstruct the immutable candidate against the latest replay projection.
|
|
3047
|
+
* A persisted acceptance is not effectuation authority after any contract,
|
|
3048
|
+
* Goal, epoch, or qualification change.
|
|
3049
|
+
*/
|
|
3050
|
+
function isCurrentAcceptedBoundary(projection, boundary) {
|
|
3051
|
+
if (boundary.persistedResult !== "accepted" || boundary.epoch !== projection.epoch || boundary.contractRevision !== projection.contractRevision || boundary.contractSha256 !== currentContractDigest(projection)) return false;
|
|
3052
|
+
const currentGoal = projection.currentGoalRef;
|
|
3053
|
+
if (boundary.goalRef ? !currentGoal || !sameRef(currentGoal, boundary.goalRef) : currentGoal !== void 0) return false;
|
|
3054
|
+
const reconstructed = qualifyBoundary(projection, {
|
|
3055
|
+
disposition: boundary.disposition,
|
|
3056
|
+
qualificationKind: boundary.qualificationKind,
|
|
3057
|
+
qualificationIds: boundary.qualificationIds,
|
|
3058
|
+
...boundary.callId ? { callId: boundary.callId } : {}
|
|
3059
|
+
});
|
|
3060
|
+
return reconstructed.persistedResult === "accepted" && reconstructed.candidateSha256 === boundary.candidateSha256;
|
|
3061
|
+
}
|
|
3062
|
+
function sameRef(state, ref) {
|
|
3063
|
+
return state?.id === ref.id && state.revision === ref.revision;
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Effectuate only a replay-confirmed accepted boundary. The first disarm result
|
|
3067
|
+
* and an independent get() must both read the same active Goal ref as disarmed.
|
|
3068
|
+
* A failure after disarm may have taken effect is never auto-rearmed.
|
|
3069
|
+
*/
|
|
3070
|
+
async function effectuateBoundary(boundary, access) {
|
|
3071
|
+
const base = {
|
|
3072
|
+
boundaryId: boundary.id,
|
|
3073
|
+
...boundary.goalRef ? { goalRef: boundary.goalRef } : {}
|
|
3074
|
+
};
|
|
3075
|
+
if (boundary.persistedResult !== "accepted") return {
|
|
3076
|
+
...base,
|
|
3077
|
+
reasonCode: "boundary_not_accepted",
|
|
3078
|
+
stopAllowed: false,
|
|
3079
|
+
resumeRequired: false
|
|
3080
|
+
};
|
|
3081
|
+
if (access.requalify) try {
|
|
3082
|
+
if (!await access.requalify()) return {
|
|
3083
|
+
...base,
|
|
3084
|
+
reasonCode: "boundary_pre_effect_failure",
|
|
3085
|
+
stopAllowed: false,
|
|
3086
|
+
resumeRequired: false
|
|
3087
|
+
};
|
|
3088
|
+
} catch {
|
|
3089
|
+
return {
|
|
3090
|
+
...base,
|
|
3091
|
+
reasonCode: "boundary_pre_effect_failure",
|
|
3092
|
+
stopAllowed: false,
|
|
3093
|
+
resumeRequired: false
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
if (!boundary.goalRef) return {
|
|
3097
|
+
...base,
|
|
3098
|
+
reasonCode: "boundary_no_goal_safe_yield",
|
|
3099
|
+
stopAllowed: true,
|
|
3100
|
+
resumeRequired: false
|
|
3101
|
+
};
|
|
3102
|
+
let before;
|
|
3103
|
+
try {
|
|
3104
|
+
before = await access.get();
|
|
3105
|
+
} catch {
|
|
3106
|
+
return {
|
|
3107
|
+
...base,
|
|
3108
|
+
reasonCode: "boundary_pre_effect_failure",
|
|
3109
|
+
stopAllowed: false,
|
|
3110
|
+
resumeRequired: false
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
if (!sameRef(before, boundary.goalRef) || before?.phase !== "active") return {
|
|
3114
|
+
...base,
|
|
3115
|
+
reasonCode: "boundary_goal_ref_stale",
|
|
3116
|
+
stopAllowed: false,
|
|
3117
|
+
resumeRequired: false
|
|
3118
|
+
};
|
|
3119
|
+
if (before.activation === "disarmed") return {
|
|
3120
|
+
...base,
|
|
3121
|
+
reasonCode: "boundary_already_disarmed",
|
|
3122
|
+
stopAllowed: true,
|
|
3123
|
+
resumeRequired: false
|
|
3124
|
+
};
|
|
3125
|
+
let firstReadback;
|
|
3126
|
+
try {
|
|
3127
|
+
firstReadback = await access.disarm();
|
|
3128
|
+
} catch {
|
|
3129
|
+
return {
|
|
3130
|
+
...base,
|
|
3131
|
+
reasonCode: "boundary_post_effect_unknown",
|
|
3132
|
+
stopAllowed: false,
|
|
3133
|
+
resumeRequired: true
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
if (!firstReadback || !sameRef(firstReadback, boundary.goalRef) || firstReadback.phase !== "active") return {
|
|
3137
|
+
...base,
|
|
3138
|
+
reasonCode: "boundary_post_effect_unknown",
|
|
3139
|
+
stopAllowed: false,
|
|
3140
|
+
resumeRequired: true
|
|
3141
|
+
};
|
|
3142
|
+
if (firstReadback.activation !== "disarmed") return {
|
|
3143
|
+
...base,
|
|
3144
|
+
reasonCode: "boundary_readback_still_armed",
|
|
3145
|
+
stopAllowed: false,
|
|
3146
|
+
resumeRequired: false
|
|
3147
|
+
};
|
|
3148
|
+
try {
|
|
3149
|
+
const independent = await access.get();
|
|
3150
|
+
if (!sameRef(independent, boundary.goalRef) || independent?.phase !== "active") return {
|
|
3151
|
+
...base,
|
|
3152
|
+
reasonCode: "boundary_post_effect_unknown",
|
|
3153
|
+
stopAllowed: false,
|
|
3154
|
+
resumeRequired: true
|
|
3155
|
+
};
|
|
3156
|
+
if (independent.activation !== "disarmed") return {
|
|
3157
|
+
...base,
|
|
3158
|
+
reasonCode: "boundary_readback_still_armed",
|
|
3159
|
+
stopAllowed: false,
|
|
3160
|
+
resumeRequired: false
|
|
3161
|
+
};
|
|
3162
|
+
} catch {
|
|
3163
|
+
return {
|
|
3164
|
+
...base,
|
|
3165
|
+
reasonCode: "boundary_post_effect_unknown",
|
|
3166
|
+
stopAllowed: false,
|
|
3167
|
+
resumeRequired: true
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
return {
|
|
3171
|
+
...base,
|
|
3172
|
+
reasonCode: "boundary_effectuated",
|
|
3173
|
+
stopAllowed: true,
|
|
3174
|
+
resumeRequired: false
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
//#endregion
|
|
3179
|
+
//#region src/domain/goal-gate.ts
|
|
3180
|
+
function hasCurrentCertificate(projection) {
|
|
3181
|
+
const checkpoint = projection.checkpoints.at(-1);
|
|
3182
|
+
let reason;
|
|
3183
|
+
if (projection.integrity !== "valid") reason = "integrity_invalid";
|
|
3184
|
+
else if (projection.hostStatus !== "supported") reason = "host_lock_unsupported";
|
|
3185
|
+
else if (!checkpoint || checkpoint.result !== "certified") reason = "certificate_missing";
|
|
3186
|
+
else if (checkpoint.epoch !== projection.epoch) reason = "stale_epoch";
|
|
3187
|
+
else if (checkpoint.sessionRefDigest !== projection.sessionRefDigest) reason = "foreign_session";
|
|
3188
|
+
else if (checkpoint.hostLockDigest !== projection.hostLockDigest) reason = "stale_host_lock";
|
|
3189
|
+
else if (checkpoint.contractRevision !== projection.contractRevision) reason = "stale_contract_revision";
|
|
3190
|
+
else if (projection.currentGoalRef ? checkpoint.goalRef?.id !== projection.currentGoalRef.id || checkpoint.goalRef.revision !== projection.currentGoalRef.revision : checkpoint.goalRef !== void 0) reason = "stale_goal_ref";
|
|
3191
|
+
projection.certificateStatusReason = reason;
|
|
3192
|
+
return reason === void 0;
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Denies `update_goal(action=complete)` while the guard is enabled and no
|
|
3196
|
+
* current completion certificate exists. The gate itself has no bypass; a
|
|
3197
|
+
* workflow that genuinely finished but cannot certify (for example a contract
|
|
3198
|
+
* polluted by session-layer talk, or evidence that lives in another session)
|
|
3199
|
+
* has three explicit remediation routes:
|
|
3200
|
+
*
|
|
3201
|
+
* 1. `/context-guard off` disables the guard, so completion is no longer
|
|
3202
|
+
* gated. Use only after the user confirms the work is actually done.
|
|
3203
|
+
* 2. `/context-guard clear` supersedes every pending requirement and
|
|
3204
|
+
* acceptance under a `CLEAR:<revision>` sentinel (prohibitions are
|
|
3205
|
+
* retained) and bumps the contract revision; an empty-binding checkpoint
|
|
3206
|
+
* can then certify while the guard stays enabled.
|
|
3207
|
+
* 3. `update_goal(action=blocked)` records the blocker truthfully, which is
|
|
3208
|
+
* never denied by this gate.
|
|
3209
|
+
*/
|
|
3210
|
+
function goalCompletionDenial(projection, toolName, argumentsValue, configuredToolName = "update_goal") {
|
|
3211
|
+
if (toolName !== configuredToolName || typeof argumentsValue !== "object" || argumentsValue === null) return void 0;
|
|
3212
|
+
if (argumentsValue.action !== "complete") return void 0;
|
|
3213
|
+
if (!projection.enabled) return void 0;
|
|
3214
|
+
const args = argumentsValue;
|
|
3215
|
+
if (projection.hostStatus !== "supported") return `Context Guard denial [stale_host]: host lock is unsupported or unavailable (${projection.hostReasonCode ?? "unknown_host"}).`;
|
|
3216
|
+
if (!projection.currentGoalRef) return "Context Guard denial [no_goal]: no current Goal reference is available.";
|
|
3217
|
+
if (args.goal_id !== projection.currentGoalRef.id || args.revision !== projection.currentGoalRef.revision) return "Context Guard denial [stale_goal_ref]: update_goal must use the exact current goal_id and revision.";
|
|
3218
|
+
if (hasCurrentCertificate(projection)) return void 0;
|
|
3219
|
+
if (projection.certificateStatusReason === "stale_host_lock") return "Context Guard denial [stale_host]: the completion certificate belongs to a different host identity.";
|
|
3220
|
+
if (projection.certificateStatusReason === "stale_goal_ref") return "Context Guard denial [stale_goal_ref]: the completion certificate belongs to a different Goal reference.";
|
|
3221
|
+
return projection.integrity === "valid" ? "Context Guard denial [certificate_missing]: a current completion certificate is required." : "Context Guard denial [certificate_missing]: integrity is unknown or corrupt, so no current certificate is usable.";
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
//#endregion
|
|
3225
|
+
//#region src/domain/stop-policy.ts
|
|
3226
|
+
/**
|
|
3227
|
+
* What "relevant progress" means, as one value.
|
|
3228
|
+
*
|
|
3229
|
+
* The inputs are the recorded state a caller could not have faked without
|
|
3230
|
+
* changing the work itself: the epoch and contract revision, the open items and
|
|
3231
|
+
* their blockers, the qualified evidence set, the boundary qualifications
|
|
3232
|
+
* available right now, and the Goal's identity and activation. Deliberately
|
|
3233
|
+
* absent: timestamps, event counts, wording, checkpoint bodies, and the Goal
|
|
3234
|
+
* *revision* — editing a Goal's text is not progress, and treating it as such
|
|
3235
|
+
* would let a re-statement reset the stop budget.
|
|
3236
|
+
*/
|
|
3237
|
+
/**
|
|
3238
|
+
* How many times the same progress fingerprint must be observed at a turn
|
|
3239
|
+
* boundary before Guard stops the automatic continuation.
|
|
3240
|
+
*
|
|
3241
|
+
* The first sighting is a baseline, not a stalled turn: it is the state a turn
|
|
3242
|
+
* either advanced to or started from, and the host's driver owns continuation
|
|
3243
|
+
* there. The second sighting is the first turn that produced nothing new, which
|
|
3244
|
+
* earns the one diagnosis and correction opportunity. The third is the bounded
|
|
3245
|
+
* stop. The count is a resource bound on repetition, never a way to declare the
|
|
3246
|
+
* task finished.
|
|
3247
|
+
*/
|
|
3248
|
+
const NO_PROGRESS_TURNS_BEFORE_STOP = 3;
|
|
3249
|
+
/** Marks the durable no-progress record; replay reads the budget from these. */
|
|
3250
|
+
const NO_PROGRESS_RECORD_PREFIX = "Context Guard no-progress record: ";
|
|
3251
|
+
/**
|
|
3252
|
+
* The identity of the turn boundary a decision is taken at.
|
|
3253
|
+
*
|
|
3254
|
+
* Guard does not own the host's turn counter, and a retry must be recognisable
|
|
3255
|
+
* as the same boundary rather than as a new one. The last durable event is that
|
|
3256
|
+
* identity: it is derivable from the log alone, it is stable across a reload,
|
|
3257
|
+
* and it only advances when the session actually records something new.
|
|
3258
|
+
*/
|
|
3259
|
+
function decisionBoundaryKey(projection) {
|
|
3260
|
+
return projection.hostTurn;
|
|
3261
|
+
}
|
|
3262
|
+
function progressFingerprint(projection) {
|
|
3263
|
+
const open = [...projection.items.values()].filter((item) => item.status === "pending").map((item) => `${item.id}:${item.revision}:${item.normalizedText}`).sort();
|
|
3264
|
+
const evidence = [...projection.evidence.values()].filter((row) => row.epoch === projection.epoch && row.outcome === "success").map((row) => row.id).sort();
|
|
3265
|
+
const qualifications = availableBoundaryQualifications(projection).map((row) => `${row.id}:${row.status}`).sort();
|
|
3266
|
+
return JSON.stringify({
|
|
3267
|
+
epoch: projection.epoch,
|
|
3268
|
+
contractRevision: projection.contractRevision,
|
|
3269
|
+
open,
|
|
3270
|
+
evidence,
|
|
3271
|
+
qualifications,
|
|
3272
|
+
goal: projection.currentGoalRef?.id ?? null
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
const QUOTED = /["'“”‘’`].*?(?:complete|done|finished|完成|做完|搞定).*?["'“”‘’`]/i;
|
|
3276
|
+
const EXAMPLE = /\b(?:for example|e\.g\.|such as|like saying|例如|比如|举例|作为一个例子)\b/i;
|
|
3277
|
+
const QUESTION = /\?[ \t]*$|\b(?:should|could|would|can|will|what|how|whether)\b.*\?/i;
|
|
3278
|
+
const TRAILING_NEGATION = /\b(?:not (?:yet |quite |fully )?(?:complete|done|finished)|isn'?t (?:complete|done|finished)|hasn'?t (?:been )?(?:completed|finished)|尚未完成|还没完成|未完成|没有完成|还未完成)\b/i;
|
|
3279
|
+
const CONDITIONAL = /\b(?:if|unless|once|when|whenever|provided that|只要|如果|假如|一旦|除非)\b/i;
|
|
3280
|
+
const PARTIAL_ONLY = /\b(?:step|phase|stage|milestone)\s+\d+\b|第[一二三四五六七八九十\d]+\s*(?:步|阶段|环节)|(?:第一步|第二步|第三步)/i;
|
|
3281
|
+
const WHOLE_COMPLETION_EN = /\b(?:the )?(?:task|work|job|everything|all tasks?|all work) (?:is|are) (?:now )?(?:complete|done|finished|completed)\b|\b(?:task|work) (?:has been )?(?:completed|finished)\b|\ball (?:tasks|work|requirements) (?:have been )?(?:completed|done|met)\b/i;
|
|
3282
|
+
const WHOLE_COMPLETION_ZH = /(?:任务|工作|所有任务|全部工作|整体)(?:已经|已)?(?:全部)?(?:完成|搞定|做完)|(?:已|已经)(?:全部|所有)?(?:完成|搞定)(?:了)?(?:全部|所有)?(?:任务|工作)?/i;
|
|
3283
|
+
/** Bare completion confirmations, e.g. "Done." or "搞定了。" */
|
|
3284
|
+
const BARE_COMPLETION = /^(?:done|finished|completed|all\s+done)[.!]?$|^(?:已完成|完成了|搞定了|搞定|完成|done)[。..!!]?$/i;
|
|
3285
|
+
/** Continuation intent following a claim makes it partial, not whole-task. */
|
|
3286
|
+
const CONTINUATION = /接下来|下一步|然后|接着|继续|再去|最后再|还差|剩下|剩余|第二步|第三步|,\s*(?:next|then|after that|moving on)\b/i;
|
|
3287
|
+
function looksQuotedOrExemplary(text) {
|
|
3288
|
+
return QUOTED.test(text) || EXAMPLE.test(text);
|
|
3289
|
+
}
|
|
3290
|
+
function isWholeTaskCompletionClaim(text) {
|
|
3291
|
+
const normalized = normalizeClause(text);
|
|
3292
|
+
if (!normalized) return false;
|
|
3293
|
+
if (QUESTION.test(normalized)) return false;
|
|
3294
|
+
if (TRAILING_NEGATION.test(normalized)) return false;
|
|
3295
|
+
if (CONDITIONAL.test(normalized)) return false;
|
|
3296
|
+
if (CONTINUATION.test(normalized)) return false;
|
|
3297
|
+
if (looksQuotedOrExemplary(normalized)) return false;
|
|
3298
|
+
if (PARTIAL_ONLY.test(normalized) && !WHOLE_COMPLETION_EN.test(normalized) && !WHOLE_COMPLETION_ZH.test(normalized)) return false;
|
|
3299
|
+
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
3300
|
+
if (BARE_COMPLETION.test(normalizeTitleLine(firstLine))) return leadingBareCompletionClaim(text);
|
|
3301
|
+
return BARE_COMPLETION.test(normalized) || WHOLE_COMPLETION_EN.test(normalized) || WHOLE_COMPLETION_ZH.test(normalized);
|
|
3302
|
+
}
|
|
3303
|
+
const DECORATION_LEAD = /^\s*(?:[\p{Extended_Pictographic}\u2764\u2705\u2714\u2716\u2728\u274C\u26A0\u2611\u2612\u2713\u2717\u274E\u2B50\u2B55\u2022\u00B7\u25E6\u25AA\u25AB\u25CF\u25CB\u25A0\u25A1\u2013\u2014-]|\uFE0F|\uFE0E|\u200D)+/u;
|
|
3304
|
+
/** Strip a leading run of decorative glyphs from a title line. */
|
|
3305
|
+
function stripDecorationPrefix(text) {
|
|
3306
|
+
let value = text;
|
|
3307
|
+
let previous = "";
|
|
3308
|
+
while (value !== previous) {
|
|
3309
|
+
previous = value;
|
|
3310
|
+
value = value.replace(DECORATION_LEAD, "");
|
|
3311
|
+
}
|
|
3312
|
+
return value.replace(/^\s+/, "");
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Normalize a title line for the bare-completion test. Markdown heading markers,
|
|
3316
|
+
* fully-wrapping emphasis (`**…**`, `__…__`, `*…*`, `_…_`), and a leading run of
|
|
3317
|
+
* decorative glyphs are removed ITERATIVELY until stable, because stripping one
|
|
3318
|
+
* layer may expose another (`## ✅ **完成。**`). Blockquotes (`>`), quoted
|
|
3319
|
+
* titles, and examples are left untouched so they still fail closed.
|
|
3320
|
+
*/
|
|
3321
|
+
function normalizeTitleLine(line) {
|
|
3322
|
+
let value = line.trim();
|
|
3323
|
+
if (value.startsWith(">")) return value;
|
|
3324
|
+
let previous = "";
|
|
3325
|
+
while (value !== previous) {
|
|
3326
|
+
previous = value;
|
|
3327
|
+
value = value.replace(/^#{1,6}\s+/, "").replace(/^\*\*(.+?)\*\*$/, "$1").replace(/^__(.+?)__$/, "$1").replace(/^\*(.+?)\*$/, "$1").replace(/^_(.+?)_$/, "$1");
|
|
3328
|
+
value = stripDecorationPrefix(value);
|
|
3329
|
+
}
|
|
3330
|
+
return value;
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* A reply whose first non-empty line is a standalone bare completion ("完成。"
|
|
3334
|
+
* or "Done.") followed by a results summary. The whole text no longer matches
|
|
3335
|
+
* the single-line BARE_COMPLETION anchor, but the summary must still be treated
|
|
3336
|
+
* as a whole-task completion claim.
|
|
3337
|
+
*/
|
|
3338
|
+
function leadingBareCompletionClaim(text) {
|
|
3339
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3340
|
+
const first = lines[0];
|
|
3341
|
+
if (!first || !BARE_COMPLETION.test(normalizeTitleLine(first))) return false;
|
|
3342
|
+
const rest = normalizeClause(lines.slice(1).join("\n"));
|
|
3343
|
+
if (!rest) return true;
|
|
3344
|
+
if (CONTINUATION.test(rest)) return false;
|
|
3345
|
+
if (TRAILING_NEGATION.test(rest)) return false;
|
|
3346
|
+
if (CONDITIONAL.test(rest)) return false;
|
|
3347
|
+
if (looksQuotedOrExemplary(rest)) return false;
|
|
3348
|
+
if (PARTIAL_ONLY.test(rest)) return false;
|
|
3349
|
+
return true;
|
|
3350
|
+
}
|
|
3351
|
+
function classifyCompletionClaim(text) {
|
|
3352
|
+
const normalized = normalizeClause(text);
|
|
3353
|
+
if (/waiting for (?:you|the user|input|your)|please (?:review|confirm|approve)|等待(?:您|你|用户)|请(?:确认|审阅|批准)/i.test(normalized)) return "user_wait";
|
|
3354
|
+
if (/waiting for (?:the )?(?:result|output|response|build|test|deployment)|等待(?:结果|输出|构建|测试|部署|响应)/i.test(normalized)) return "external_wait";
|
|
3355
|
+
if (isWholeTaskCompletionClaim(normalized)) return "complete";
|
|
3356
|
+
return "report";
|
|
3357
|
+
}
|
|
3358
|
+
/** Assistant prose is retained only as a bounded diagnostic observation. */
|
|
3359
|
+
function observeAssistantOutcome(text) {
|
|
3360
|
+
const disposition = classifyCompletionClaim(text);
|
|
3361
|
+
if (disposition === "complete") return {
|
|
3362
|
+
kind: "completion_claim",
|
|
3363
|
+
reasonCode: "assistant_completion_claim_observed"
|
|
3364
|
+
};
|
|
3365
|
+
if (disposition === "user_wait") return {
|
|
3366
|
+
kind: "user_wait_claim",
|
|
3367
|
+
reasonCode: "assistant_user_wait_claim_observed"
|
|
3368
|
+
};
|
|
3369
|
+
if (disposition === "external_wait") return {
|
|
3370
|
+
kind: "external_wait_claim",
|
|
3371
|
+
reasonCode: "assistant_external_wait_claim_observed"
|
|
3372
|
+
};
|
|
3373
|
+
return {
|
|
3374
|
+
kind: "report",
|
|
3375
|
+
reasonCode: "assistant_report_observed"
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
/**
|
|
3379
|
+
* Stop Protocol 2.0 decision. This function deliberately has no assistant-text
|
|
3380
|
+
* parameter: completion wording, quotation, negation and translation cannot
|
|
3381
|
+
* steer the protocol. A structured root persistence authorization may request
|
|
3382
|
+
* one fallback correction; subsequent attempts safe-yield. An active, armed
|
|
3383
|
+
* Goal remains exclusively owned by the host Goal Round Driver.
|
|
3384
|
+
*/
|
|
3385
|
+
function decideTurnBoundary(projection) {
|
|
3386
|
+
if (!projection.enabled) return {
|
|
3387
|
+
action: "stop",
|
|
3388
|
+
reason: "guard_disabled"
|
|
3389
|
+
};
|
|
3390
|
+
if (projection.integrity !== "valid") return {
|
|
3391
|
+
action: "stop",
|
|
3392
|
+
reason: "integrity_invalid_safe_yield"
|
|
3393
|
+
};
|
|
3394
|
+
if (hasCurrentCertificate(projection)) return {
|
|
3395
|
+
action: "stop",
|
|
3396
|
+
reason: "current_certificate"
|
|
3397
|
+
};
|
|
3398
|
+
const boundary = projection.boundaries.at(-1);
|
|
3399
|
+
if (boundary?.persistedResult === "accepted" && boundary.epoch === projection.epoch && boundary.contractRevision === projection.contractRevision) return {
|
|
3400
|
+
action: "stop",
|
|
3401
|
+
reason: "accepted_boundary_pending_effectuation"
|
|
3402
|
+
};
|
|
3403
|
+
if (projection.currentGoalPhase === "active" && projection.currentGoalActivation === "armed") {
|
|
3404
|
+
const fingerprint$1 = progressFingerprint(projection);
|
|
3405
|
+
const claims = projection.noProgressClaims.get(fingerprint$1) ?? /* @__PURE__ */ new Map();
|
|
3406
|
+
const hostTurn = decisionBoundaryKey(projection);
|
|
3407
|
+
if (hostTurn === void 0) return {
|
|
3408
|
+
action: "stop",
|
|
3409
|
+
reason: "no_progress_identity_unavailable"
|
|
3410
|
+
};
|
|
3411
|
+
const boundaryKey = String(hostTurn);
|
|
3412
|
+
const prior = [...claims].filter(([key]) => key !== boundaryKey).length;
|
|
3413
|
+
const claim = {
|
|
3414
|
+
fingerprint: fingerprint$1,
|
|
3415
|
+
boundaryKey,
|
|
3416
|
+
attempt: prior + 1
|
|
3417
|
+
};
|
|
3418
|
+
if (prior === 0) return {
|
|
3419
|
+
action: "stop",
|
|
3420
|
+
reason: "goal_round_driver_owns_continuation",
|
|
3421
|
+
noProgressClaim: claim
|
|
3422
|
+
};
|
|
3423
|
+
if (prior < NO_PROGRESS_TURNS_BEFORE_STOP - 1) return {
|
|
3424
|
+
action: "continue",
|
|
3425
|
+
reason: "no_progress_diagnosis_steer",
|
|
3426
|
+
noProgressClaim: claim
|
|
3427
|
+
};
|
|
3428
|
+
return {
|
|
3429
|
+
action: "stop",
|
|
3430
|
+
reason: "no_progress_bounded_disarm"
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
if (projection.currentGoalRef) return {
|
|
3434
|
+
action: "stop",
|
|
3435
|
+
reason: projection.currentGoalPhase === "paused" ? "goal_paused_by_user_safe_yield" : "goal_not_continuable_safe_yield"
|
|
3436
|
+
};
|
|
3437
|
+
if ([...projection.items.values()].some((item) => item.status === "pending" && item.persistenceAuthorization)) {
|
|
3438
|
+
const key = `${projection.epoch}:${projection.contractRevision}`;
|
|
3439
|
+
const attempts = projection.persistenceCorrectionAttempts.get(key) ?? 0;
|
|
3440
|
+
if (attempts < 1) {
|
|
3441
|
+
projection.persistenceCorrectionAttempts.set(key, attempts + 1);
|
|
3442
|
+
return {
|
|
3443
|
+
action: "continue",
|
|
3444
|
+
reason: "protocol_correction_steer"
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
return {
|
|
3449
|
+
action: "stop",
|
|
3450
|
+
reason: "safe_yield_pending_preserved"
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
function decideTurnStopping(projection, _assistantText, _turn, _maxAttempts) {
|
|
3454
|
+
return decideTurnBoundary(projection);
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Whether the last trusted ROOT instruction asked to pause.
|
|
3458
|
+
*
|
|
3459
|
+
* The source filter is the contract, not a heuristic: a quoted log, a tool
|
|
3460
|
+
* result, a plugin notice or a model message is not a `user/message` with
|
|
3461
|
+
* `source.kind === 'user'`, so none of them can reach this function at all, and
|
|
3462
|
+
* neither can the model's own summary of one. A negated pause ("不要暂停") is not
|
|
3463
|
+
* a pause request, and the check is anchored to a clause head so a pause word
|
|
3464
|
+
* mentioned inside a longer instruction is not a control request.
|
|
3465
|
+
*/
|
|
3466
|
+
function latestRootInstruction(events) {
|
|
3467
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
3468
|
+
const event = events[index];
|
|
3469
|
+
if (event.type !== "user/message") continue;
|
|
3470
|
+
const data = event.data;
|
|
3471
|
+
if (data.source?.kind !== "user") continue;
|
|
3472
|
+
const text = (data.content ?? []).filter((part) => part?.type === "text").map((part) => part.text ?? "").join("\n");
|
|
3473
|
+
if (text.trim()) return {
|
|
3474
|
+
text,
|
|
3475
|
+
seq: event.seq ?? 0
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
/** Marks a root control request Guard has already carried to the host. */
|
|
3480
|
+
const CONTROL_RECORD_PREFIX = "Context Guard control record: ";
|
|
3481
|
+
const PAUSE_REQUEST = /(?:^|[。!?;;,,、\s])(?:请|麻烦)?\s*(?:先)?\s*(?:暂停|停一下|停一停|先停|暂时停止)(?:一下|下|吧)?\s*(?:[。!?;;,,、]|$)|\b(?:please\s+)?(?:pause|hold\s+on|stop\s+for\s+now)\b/i;
|
|
3482
|
+
const NEGATED_PAUSE = /(?:不要|不用|别|无需|不必)\s*(?:先)?\s*(?:暂停|停)|\b(?:do\s+not|don't|never)\s+(?:pause|stop)\b/i;
|
|
3483
|
+
function isRootPauseRequest(text) {
|
|
3484
|
+
if (NEGATED_PAUSE.test(text)) return false;
|
|
3485
|
+
return PAUSE_REQUEST.test(text);
|
|
3486
|
+
}
|
|
3487
|
+
function latestAssistantText(events) {
|
|
3488
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
3489
|
+
const event = events[index];
|
|
3490
|
+
if (event.type !== "assistant/message") continue;
|
|
3491
|
+
const text = event.data.message?.content?.filter((block$1) => block$1.type === "text").map((block$1) => block$1.text ?? "").join("\n") ?? "";
|
|
3492
|
+
if (text.trim()) return text;
|
|
3493
|
+
}
|
|
3494
|
+
return "";
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
//#endregion
|
|
3498
|
+
//#region src/domain/digest.ts
|
|
3499
|
+
/**
|
|
3500
|
+
* Digest v3 canonical manifest derivation for Context Guard certificates.
|
|
3501
|
+
*
|
|
3502
|
+
* This is the DSH-side implementation of the frozen cross-language digest
|
|
3503
|
+
* contract documented in `docs/SEMANTIC_COMPATIBILITY.md`. The canonical
|
|
3504
|
+
* fixture lives in codex-context-guard (`tests/fixtures/conformance/digest_v3`)
|
|
3505
|
+
* and is byte-mirrored under `tests/fixtures/conformance/digest_v3` together
|
|
3506
|
+
* with `UPSTREAM_PIN.json`; the vitest suite re-derives all 29 golden vectors
|
|
3507
|
+
* and fails on any byte difference. Any change to the algorithm, separators,
|
|
3508
|
+
* typed token language, allowlists, or serialization is a new digest version
|
|
3509
|
+
* and must regenerate the vectors in both repositories.
|
|
3510
|
+
*
|
|
3511
|
+
* Fail-closed rules pinned here: lone surrogates are rejected before hashing,
|
|
3512
|
+
* values are never Unicode-normalized, dynamic keys must match the snake_case
|
|
3513
|
+
* grammar, collections reject duplicate members, canonical maps sort by
|
|
3514
|
+
* semantic key bytes (never by encoded field bytes), and predicate digests are
|
|
3515
|
+
* always recomputed from the actual parameter payload.
|
|
3516
|
+
*/
|
|
3517
|
+
var DigestError = class extends Error {};
|
|
3518
|
+
const MAX_ENCODED_NAME_BYTES = 256;
|
|
3519
|
+
const MAX_SEMANTIC_KEY_BYTES = 64;
|
|
3520
|
+
const MAX_VALUE_BYTES = 4096;
|
|
3521
|
+
const MAX_FIELDS_PER_RECORD = 128;
|
|
3522
|
+
const MAX_PRED_PARAMS_BYTES = 4096;
|
|
3523
|
+
const DYNAMIC_KEY_RE = /^[a-z0-9_]{1,64}$/;
|
|
3524
|
+
const PACKAGE_NAME_RE = /^[@a-z0-9._/-]{1,128}$/;
|
|
3525
|
+
const ENUM_TOKEN_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
3526
|
+
const HEX_RE = /^[0-9a-f]+$/;
|
|
3527
|
+
const SURFACE_ENUM = [
|
|
3528
|
+
"artifact",
|
|
3529
|
+
"ui",
|
|
3530
|
+
"visual",
|
|
3531
|
+
"scope"
|
|
3532
|
+
];
|
|
3533
|
+
const OUTCOME_ENUM = [
|
|
3534
|
+
"success",
|
|
3535
|
+
"failure",
|
|
3536
|
+
"unknown",
|
|
3537
|
+
"durability-unknown"
|
|
3538
|
+
];
|
|
3539
|
+
const EVIDENCE_ROLE_ENUM = [
|
|
3540
|
+
"resolution",
|
|
3541
|
+
"effect",
|
|
3542
|
+
"state"
|
|
3543
|
+
];
|
|
1885
3544
|
const PRED_PARAMS_KIND_ENUM = ["inline", "manifest"];
|
|
1886
3545
|
/** Frozen canonical key vocabulary; product manifests draw allowlists from it. */
|
|
1887
3546
|
const PRODUCT_KEY_VOCABULARY = [
|
|
@@ -2777,6 +4436,10 @@ function renderRecoveryPacket(projection, options = {}) {
|
|
|
2777
4436
|
};
|
|
2778
4437
|
const requirement = (item) => {
|
|
2779
4438
|
const diagnosis = deriveItemDiagnosis(projection, item);
|
|
4439
|
+
if (diagnosis.reason_code === "root_condition_pending") {
|
|
4440
|
+
if (add(`[${clip(item.id, 20)}] root_condition_pending; wait for trusted root: ${item.resumeEvent ?? item.condition ?? item.normalizedText}; do not execute before release`, compact ? 160 : 310)) count++;
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
2780
4443
|
const remedy = diagnosis.repairability === "agent_repairable" ? "Collect matching evidence; checkpoint" : diagnosis.repairability === "historical_gap" ? "Read back observed state; do not re-execute" : diagnosis.certification === "unsupported" ? "Deliver honestly; stays uncertified unless a fresh instruction names a supported action" : "Restore audited host/adapter capability";
|
|
2781
4444
|
if (add(`[${clip(item.id, 20)}] ${diagnosis.reason_code}; ${compact ? remedy : diagnosis.next_action.resume_condition ?? remedy}; ${clip(item.normalizedText, 70)}`, compact ? 110 : 310)) count++;
|
|
2782
4445
|
};
|
|
@@ -2866,8 +4529,9 @@ function evidenceProblem(projection, item, binding) {
|
|
|
2866
4529
|
offendingEvidenceIds: notSuccess
|
|
2867
4530
|
};
|
|
2868
4531
|
const requiredAction = item.semanticAction ?? "generic_run";
|
|
4532
|
+
const compatibleWith = [requiredAction, ...(item.actionPlan ?? []).map((entry) => entry.action)];
|
|
2869
4533
|
const facts = citedEvidence(projection, binding);
|
|
2870
|
-
const incompatible = facts.filter((fact) => !actionCompatible(
|
|
4534
|
+
const incompatible = facts.filter((fact) => !compatibleWith.some((action) => actionCompatible(action, fact.semanticAction ?? "generic_run")));
|
|
2871
4535
|
if (incompatible.length) {
|
|
2872
4536
|
const compatibleCount = facts.length - incompatible.length;
|
|
2873
4537
|
return {
|
|
@@ -3205,6 +4869,13 @@ function certifyCheckpoint(projection, bindings, id, commit = true) {
|
|
|
3205
4869
|
});
|
|
3206
4870
|
continue;
|
|
3207
4871
|
}
|
|
4872
|
+
if (item.actionPlan && item.actionPlan.length > 0) {
|
|
4873
|
+
const planProblem = bindingActionPlanProblem(projection, item, binding);
|
|
4874
|
+
if (planProblem) {
|
|
4875
|
+
rejectedBindings.push(planProblem);
|
|
4876
|
+
continue;
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
3208
4879
|
if (!binding.evidenceIds.length) {
|
|
3209
4880
|
rejectedBindings.push({
|
|
3210
4881
|
itemId: item.id,
|
|
@@ -3302,230 +4973,97 @@ function certifyCheckpoint(projection, bindings, id, commit = true) {
|
|
|
3302
4973
|
};
|
|
3303
4974
|
}
|
|
3304
4975
|
}
|
|
3305
|
-
function openItems(projection) {
|
|
3306
|
-
return [...projection.items.values()].filter((item) => item.status === "pending" && item.kind !== "prohibition").map((item) => item.id);
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
//#endregion
|
|
3310
|
-
//#region src/domain/boundary.ts
|
|
3311
|
-
/** Bounded, replay-derived qualifications that callers may cite verbatim. */
|
|
3312
|
-
function availableBoundaryQualifications(projection) {
|
|
3313
|
-
const rows = [];
|
|
3314
|
-
for (const item of projection.items.values()) {
|
|
3315
|
-
if (item.status !== "pending") continue;
|
|
3316
|
-
if (item.waitAuthorization) rows.push({
|
|
3317
|
-
id: item.waitAuthorization.id,
|
|
3318
|
-
kind: item.waitAuthorization.kind,
|
|
3319
|
-
disposition: "user_wait",
|
|
3320
|
-
source: "root_contract",
|
|
3321
|
-
status: "pending"
|
|
3322
|
-
});
|
|
3323
|
-
if (item.deferAuthorization) rows.push({
|
|
3324
|
-
id: item.deferAuthorization.id,
|
|
3325
|
-
kind: item.deferAuthorization.kind,
|
|
3326
|
-
disposition: "deferred",
|
|
3327
|
-
source: "root_contract",
|
|
3328
|
-
status: "pending"
|
|
3329
|
-
});
|
|
3330
|
-
}
|
|
3331
|
-
for (const operation of projection.externalOperations.values()) {
|
|
3332
|
-
if (operation.epoch !== projection.epoch || operation.status !== "pending" && operation.status !== "running") continue;
|
|
3333
|
-
rows.push({
|
|
3334
|
-
id: operation.id,
|
|
3335
|
-
kind: "external_operation_pending",
|
|
3336
|
-
disposition: "external_wait",
|
|
3337
|
-
source: "trusted_adapter",
|
|
3338
|
-
status: operation.status
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
return rows.sort((a, b) => a.id.localeCompare(b.id)).slice(0, 32);
|
|
3342
|
-
}
|
|
3343
|
-
function qualificationReason(projection, request) {
|
|
3344
|
-
const ids = new Set(request.qualificationIds);
|
|
3345
|
-
if (ids.size !== request.qualificationIds.length || ids.size === 0) return "boundary_qualification_ids_invalid";
|
|
3346
|
-
if (request.disposition === "user_wait") {
|
|
3347
|
-
if (request.qualificationKind !== "root_explicit_wait" && request.qualificationKind !== "user_decision_item") return "boundary_qualification_kind_mismatch";
|
|
3348
|
-
const known$1 = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.waitAuthorization?.kind === request.qualificationKind).map((item) => item.waitAuthorization.id));
|
|
3349
|
-
return request.qualificationIds.every((id) => known$1.has(id)) ? void 0 : "boundary_disposition_unqualified";
|
|
3350
|
-
}
|
|
3351
|
-
if (request.disposition === "external_wait") {
|
|
3352
|
-
if (request.qualificationKind !== "external_operation_pending") return "boundary_qualification_kind_mismatch";
|
|
3353
|
-
return request.qualificationIds.every((id) => {
|
|
3354
|
-
const operation = projection.externalOperations.get(id);
|
|
3355
|
-
return operation?.epoch === projection.epoch && (operation.status === "running" || operation.status === "pending");
|
|
3356
|
-
}) ? void 0 : "boundary_disposition_unqualified";
|
|
3357
|
-
}
|
|
3358
|
-
if (request.qualificationKind !== "root_explicit_defer") return "boundary_qualification_kind_mismatch";
|
|
3359
|
-
const known = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.deferAuthorization?.kind === request.qualificationKind).map((item) => item.deferAuthorization.id));
|
|
3360
|
-
return request.qualificationIds.every((id) => known.has(id)) ? void 0 : "boundary_disposition_unqualified";
|
|
3361
|
-
}
|
|
3362
|
-
function qualifyBoundary(projection, request) {
|
|
3363
|
-
const contractSha256 = currentContractDigest(projection);
|
|
3364
|
-
const reason = projection.integrity !== "valid" ? "boundary_integrity_invalid" : projection.hostStatus !== "supported" && projection.currentGoalRef ? "boundary_host_lock_unsupported" : qualificationReason(projection, request);
|
|
3365
|
-
const manifest = {
|
|
3366
|
-
protocolVersion: "1",
|
|
3367
|
-
disposition: request.disposition,
|
|
3368
|
-
qualificationKind: request.qualificationKind,
|
|
3369
|
-
qualificationIds: [...request.qualificationIds].sort(),
|
|
3370
|
-
epoch: projection.epoch,
|
|
3371
|
-
contractRevision: projection.contractRevision,
|
|
3372
|
-
contractSha256,
|
|
3373
|
-
goalRef: projection.currentGoalRef ?? null
|
|
3374
|
-
};
|
|
3375
|
-
const candidateSha256 = sha256(JSON.stringify(manifest));
|
|
3376
|
-
return {
|
|
3377
|
-
protocolVersion: "1",
|
|
3378
|
-
id: `B${projection.boundaries.length + 1}`,
|
|
3379
|
-
disposition: request.disposition,
|
|
3380
|
-
qualificationKind: request.qualificationKind,
|
|
3381
|
-
qualificationIds: [...request.qualificationIds],
|
|
3382
|
-
epoch: projection.epoch,
|
|
3383
|
-
contractRevision: projection.contractRevision,
|
|
3384
|
-
contractSha256,
|
|
3385
|
-
...projection.currentGoalRef ? { goalRef: { ...projection.currentGoalRef } } : {},
|
|
3386
|
-
candidateSha256,
|
|
3387
|
-
...request.callId ? { callId: request.callId } : {},
|
|
3388
|
-
persistedResult: reason ? "rejected" : "accepted",
|
|
3389
|
-
reasonCode: reason ?? "boundary_persisted_accepted"
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3392
|
-
/**
|
|
3393
|
-
* Reconstruct the immutable candidate against the latest replay projection.
|
|
3394
|
-
* A persisted acceptance is not effectuation authority after any contract,
|
|
3395
|
-
* Goal, epoch, or qualification change.
|
|
3396
|
-
*/
|
|
3397
|
-
function isCurrentAcceptedBoundary(projection, boundary) {
|
|
3398
|
-
if (boundary.persistedResult !== "accepted" || boundary.epoch !== projection.epoch || boundary.contractRevision !== projection.contractRevision || boundary.contractSha256 !== currentContractDigest(projection)) return false;
|
|
3399
|
-
const currentGoal = projection.currentGoalRef;
|
|
3400
|
-
if (boundary.goalRef ? !currentGoal || !sameRef(currentGoal, boundary.goalRef) : currentGoal !== void 0) return false;
|
|
3401
|
-
const reconstructed = qualifyBoundary(projection, {
|
|
3402
|
-
disposition: boundary.disposition,
|
|
3403
|
-
qualificationKind: boundary.qualificationKind,
|
|
3404
|
-
qualificationIds: boundary.qualificationIds,
|
|
3405
|
-
...boundary.callId ? { callId: boundary.callId } : {}
|
|
3406
|
-
});
|
|
3407
|
-
return reconstructed.persistedResult === "accepted" && reconstructed.candidateSha256 === boundary.candidateSha256;
|
|
3408
|
-
}
|
|
3409
|
-
function sameRef(state, ref) {
|
|
3410
|
-
return state?.id === ref.id && state.revision === ref.revision;
|
|
3411
|
-
}
|
|
3412
4976
|
/**
|
|
3413
|
-
*
|
|
3414
|
-
*
|
|
3415
|
-
*
|
|
4977
|
+
* Per-action closure check for a multi-action clause. Each planned action needs
|
|
4978
|
+
* a matching closure whose resolved target matches the target captured for that
|
|
4979
|
+
* action, whose cited evidence succeeded, and whose evidence is not older than
|
|
4980
|
+
* the item revision it is closing.
|
|
3416
4981
|
*/
|
|
3417
|
-
|
|
3418
|
-
const
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
if (!
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
resumeRequired: false
|
|
3434
|
-
};
|
|
3435
|
-
} catch {
|
|
3436
|
-
return {
|
|
3437
|
-
...base,
|
|
3438
|
-
reasonCode: "boundary_pre_effect_failure",
|
|
3439
|
-
stopAllowed: false,
|
|
3440
|
-
resumeRequired: false
|
|
3441
|
-
};
|
|
3442
|
-
}
|
|
3443
|
-
if (!boundary.goalRef) return {
|
|
3444
|
-
...base,
|
|
3445
|
-
reasonCode: "boundary_no_goal_safe_yield",
|
|
3446
|
-
stopAllowed: true,
|
|
3447
|
-
resumeRequired: false
|
|
3448
|
-
};
|
|
3449
|
-
let before;
|
|
3450
|
-
try {
|
|
3451
|
-
before = await access.get();
|
|
3452
|
-
} catch {
|
|
3453
|
-
return {
|
|
3454
|
-
...base,
|
|
3455
|
-
reasonCode: "boundary_pre_effect_failure",
|
|
3456
|
-
stopAllowed: false,
|
|
3457
|
-
resumeRequired: false
|
|
3458
|
-
};
|
|
3459
|
-
}
|
|
3460
|
-
if (!sameRef(before, boundary.goalRef) || before?.phase !== "active") return {
|
|
3461
|
-
...base,
|
|
3462
|
-
reasonCode: "boundary_goal_ref_stale",
|
|
3463
|
-
stopAllowed: false,
|
|
3464
|
-
resumeRequired: false
|
|
3465
|
-
};
|
|
3466
|
-
if (before.activation === "disarmed") return {
|
|
3467
|
-
...base,
|
|
3468
|
-
reasonCode: "boundary_already_disarmed",
|
|
3469
|
-
stopAllowed: true,
|
|
3470
|
-
resumeRequired: false
|
|
3471
|
-
};
|
|
3472
|
-
let firstReadback;
|
|
3473
|
-
try {
|
|
3474
|
-
firstReadback = await access.disarm();
|
|
3475
|
-
} catch {
|
|
3476
|
-
return {
|
|
3477
|
-
...base,
|
|
3478
|
-
reasonCode: "boundary_post_effect_unknown",
|
|
3479
|
-
stopAllowed: false,
|
|
3480
|
-
resumeRequired: true
|
|
4982
|
+
function bindingActionPlanProblem(projection, item, binding) {
|
|
4983
|
+
const plan = item.actionPlan ?? [];
|
|
4984
|
+
const closures = binding.actionBindings ?? [];
|
|
4985
|
+
if (closures.length !== plan.length) return {
|
|
4986
|
+
itemId: item.id,
|
|
4987
|
+
reason: `the clause orders ${plan.map((entry) => entry.action).join(" + ")}; ${closures.length} action closure(s) supplied`,
|
|
4988
|
+
reasonCode: "action_plan_incomplete",
|
|
4989
|
+
hint: closingHint(projection, item)
|
|
4990
|
+
};
|
|
4991
|
+
const ordered = [...closures].sort((a, b) => a.order - b.order);
|
|
4992
|
+
for (const [index, planned] of plan.entries()) {
|
|
4993
|
+
const closure = ordered[index];
|
|
4994
|
+
if (!closure || closure.action !== planned.action) return {
|
|
4995
|
+
itemId: item.id,
|
|
4996
|
+
reason: `action closure ${index + 1} must be '${planned.action}' in the clause's order`,
|
|
4997
|
+
reasonCode: "action_plan_order_mismatch"
|
|
3481
4998
|
};
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
resumeRequired: true
|
|
3488
|
-
};
|
|
3489
|
-
if (firstReadback.activation !== "disarmed") return {
|
|
3490
|
-
...base,
|
|
3491
|
-
reasonCode: "boundary_readback_still_armed",
|
|
3492
|
-
stopAllowed: false,
|
|
3493
|
-
resumeRequired: false
|
|
3494
|
-
};
|
|
3495
|
-
try {
|
|
3496
|
-
const independent = await access.get();
|
|
3497
|
-
if (!sameRef(independent, boundary.goalRef) || independent?.phase !== "active") return {
|
|
3498
|
-
...base,
|
|
3499
|
-
reasonCode: "boundary_post_effect_unknown",
|
|
3500
|
-
stopAllowed: false,
|
|
3501
|
-
resumeRequired: true
|
|
4999
|
+
if (planned.targetCaptureStatus !== "resolved") return {
|
|
5000
|
+
itemId: item.id,
|
|
5001
|
+
reason: `the clause does not identify an exact target for '${planned.action}'`,
|
|
5002
|
+
reasonCode: planned.targetCaptureReasonCode ?? "action_plan_target_missing",
|
|
5003
|
+
hint: closingHint(projection, item)
|
|
3502
5004
|
};
|
|
3503
|
-
if (
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
resumeRequired: false
|
|
5005
|
+
if (!tuplesEqual(planned.requestedTarget, closure.resolvedTarget)) return {
|
|
5006
|
+
itemId: item.id,
|
|
5007
|
+
reason: `the closure for '${planned.action}' resolves a different target than the clause captured`,
|
|
5008
|
+
reasonCode: "action_plan_target_mismatch"
|
|
3508
5009
|
};
|
|
3509
|
-
|
|
3510
|
-
return {
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
5010
|
+
const reused = closure.evidenceIds.filter((id) => closures.some((other) => other !== closure && other.evidenceIds.includes(id)));
|
|
5011
|
+
if (reused.length > 0) return {
|
|
5012
|
+
itemId: item.id,
|
|
5013
|
+
reason: `evidence cited for '${planned.action}' also closes another action`,
|
|
5014
|
+
reasonCode: "action_plan_evidence_reused",
|
|
5015
|
+
offendingEvidenceIds: reused
|
|
5016
|
+
};
|
|
5017
|
+
if (closure.evidenceIds.length === 0) return {
|
|
5018
|
+
itemId: item.id,
|
|
5019
|
+
reason: `no evidence cited for '${planned.action}'`,
|
|
5020
|
+
reasonCode: "action_plan_evidence_missing"
|
|
5021
|
+
};
|
|
5022
|
+
const cited = closure.evidenceIds.map((id) => projection.evidence.get(id));
|
|
5023
|
+
const missing = closure.evidenceIds.filter((id) => !projection.evidence.has(id));
|
|
5024
|
+
if (missing.length > 0) return {
|
|
5025
|
+
itemId: item.id,
|
|
5026
|
+
reason: `cited evidence for '${planned.action}' is missing`,
|
|
5027
|
+
reasonCode: "evidence_missing",
|
|
5028
|
+
offendingEvidenceIds: missing
|
|
3515
5029
|
};
|
|
5030
|
+
for (const [position, evidence] of cited.entries()) {
|
|
5031
|
+
if (!evidence) continue;
|
|
5032
|
+
if (evidence.epoch !== projection.epoch) return {
|
|
5033
|
+
itemId: item.id,
|
|
5034
|
+
reason: `evidence for '${planned.action}' belongs to another epoch`,
|
|
5035
|
+
reasonCode: "evidence_wrong_epoch",
|
|
5036
|
+
offendingEvidenceIds: [closure.evidenceIds[position]]
|
|
5037
|
+
};
|
|
5038
|
+
if (evidence.outcome !== "success") return {
|
|
5039
|
+
itemId: item.id,
|
|
5040
|
+
reason: `evidence for '${planned.action}' did not succeed`,
|
|
5041
|
+
reasonCode: "action_plan_evidence_not_successful",
|
|
5042
|
+
offendingEvidenceIds: [closure.evidenceIds[position]]
|
|
5043
|
+
};
|
|
5044
|
+
if (evidence.toolResultSeq < 0) return {
|
|
5045
|
+
itemId: item.id,
|
|
5046
|
+
reason: `evidence for '${planned.action}' predates the item`,
|
|
5047
|
+
reasonCode: "action_plan_evidence_predates_item",
|
|
5048
|
+
offendingEvidenceIds: [closure.evidenceIds[position]]
|
|
5049
|
+
};
|
|
5050
|
+
if (evidence.semanticAction && evidence.semanticAction !== planned.action) return {
|
|
5051
|
+
itemId: item.id,
|
|
5052
|
+
reason: `evidence for '${planned.action}' records '${evidence.semanticAction}'`,
|
|
5053
|
+
reasonCode: "action_plan_action_mismatch",
|
|
5054
|
+
offendingEvidenceIds: [closure.evidenceIds[position]]
|
|
5055
|
+
};
|
|
5056
|
+
}
|
|
3516
5057
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
stopAllowed: true,
|
|
3521
|
-
resumeRequired: false
|
|
3522
|
-
};
|
|
5058
|
+
}
|
|
5059
|
+
function openItems(projection) {
|
|
5060
|
+
return [...projection.items.values()].filter((item) => item.status === "pending" && item.kind !== "prohibition").map((item) => item.id);
|
|
3523
5061
|
}
|
|
3524
5062
|
|
|
3525
5063
|
//#endregion
|
|
3526
5064
|
//#region src/domain/contract-segment.ts
|
|
3527
5065
|
const REFERENCE_FRAME = /(?:以下|下面|下列|附上|粘贴|提供).{0,12}(?:报告|材料|内容|记录|日志).{0,12}(?:供参考|参考|如下)|(?:for reference|pasted|attached|following).{0,16}(?:report|material|log)/i;
|
|
3528
|
-
const INSTRUCTION_SIGNAL = /(
|
|
5066
|
+
const INSTRUCTION_SIGNAL = /(?:请|需要|必须|务必|禁止|不要|不得|运行|执行|修改|创建|读取|验证|检查|安装|应用|拉取|抓取|获取|同步|提交|推送|发布|重启|升级|更新)|\b(?:please|must|shall|do not|run|execute|modify|create|read|verify|check|install|apply|pull|fetch|commit|push|publish|restart|upgrade|update)\b/i;
|
|
3529
5067
|
const ADOPTION_SIGNAL = /(?:按照|依照|采用|执行).{0,16}(?:下面|以下|报告|材料|第\s*([0-9一二三四五六七八九十]+)\s*节).{0,16}(?:全部执行|执行|作为验收|作为要求)|(?:把|将).{0,16}(?:上一条|前述|上述).{0,8}(?:报告|材料).{0,12}第\s*([0-9一二三四五六七八九十]+)\s*节.{0,20}(?:执行|采用)|(?:adopt|follow|apply).{0,20}(?:section\s+(\d+)|below|report)/i;
|
|
3530
5068
|
const PREVIOUS_REFERENCE_ADOPTION = /(?:把|将).{0,16}(?:上一条|前述|上述).{0,8}(?:报告|材料).{0,12}第\s*[0-9一二三四五六七八九十]+\s*节.{0,20}(?:执行|采用)|(?:adopt|follow|apply).{0,16}(?:the\s+)?(?:previous|above).{0,12}(?:report|material).{0,12}section\s+\d+/i;
|
|
3531
5069
|
function chineseNumber(value) {
|
|
@@ -4049,14 +5587,531 @@ const RC1_HOST_PACKAGES = [
|
|
|
4049
5587
|
integrity
|
|
4050
5588
|
}));
|
|
4051
5589
|
|
|
5590
|
+
//#endregion
|
|
5591
|
+
//#region src/domain/rc015-host.ts
|
|
5592
|
+
/**
|
|
5593
|
+
* Exact 33-row DSH 0.1.5-rc.1 core graph.
|
|
5594
|
+
*
|
|
5595
|
+
* Provenance: every row is the npm registry `dist.integrity` of the exact
|
|
5596
|
+
* published tarball for the named version, read from
|
|
5597
|
+
* `https://registry.npmjs.org/<name>/0.1.5-rc.1` (and `4.0.2` for
|
|
5598
|
+
* `@deepseek-ai/cordis`, which is versioned independently of DSH). The single
|
|
5599
|
+
* resolver for this graph is an isolated DSH installation plus the repository
|
|
5600
|
+
* worktree lockfile, both installed from the public registry.
|
|
5601
|
+
*
|
|
5602
|
+
* This is a REGISTRY-DERIVED graph, not a natively audited one: the cohort
|
|
5603
|
+
* carries `auditedPlatforms: []` until a native macOS/Windows host audit runs,
|
|
5604
|
+
* and `auditProvenance: 'registry-derived-pending-native-audit'` is bound into
|
|
5605
|
+
* the host-lock digest so a certificate can never claim a native pass this round
|
|
5606
|
+
* did not produce. (`acceptedPlatforms` is the separate, wider gate: this cohort
|
|
5607
|
+
* accepts evaluation on both platforms while claiming an audit on neither.)
|
|
5608
|
+
* `dshmarket` is deliberately absent: market identity is verified independently
|
|
5609
|
+
* by the action adapter and never participates in the core lock.
|
|
5610
|
+
*
|
|
5611
|
+
* The row-name set is unchanged from the historical 0.1.2-rc.1 cohort's 33
|
|
5612
|
+
* core rows: no package entered or left the audited core graph, so a future
|
|
5613
|
+
* reader must not infer a graph change from the version bump alone. The count
|
|
5614
|
+
* is asserted from this list, never assumed.
|
|
5615
|
+
*/
|
|
5616
|
+
const RC015_HOST_PACKAGES = [
|
|
5617
|
+
[
|
|
5618
|
+
"@deepseek-ai/cordis",
|
|
5619
|
+
"4.0.2",
|
|
5620
|
+
"sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g=="
|
|
5621
|
+
],
|
|
5622
|
+
[
|
|
5623
|
+
"@deepseek-ai/dsh",
|
|
5624
|
+
"0.1.5-rc.1",
|
|
5625
|
+
"sha512-rmNmzQCg3oIc1z8xH7izRSOuy1TNzq+/NILyfM+7e8DKOyV+yBtg47WEsqR2SiIe1ATec3L/rUa1YhIcfQ2XEg=="
|
|
5626
|
+
],
|
|
5627
|
+
[
|
|
5628
|
+
"@deepseek-ai/dsh-agent",
|
|
5629
|
+
"0.1.5-rc.1",
|
|
5630
|
+
"sha512-obIPyTSjq1y0Yhasm3mLhK5BW6Ge0VQoRT8FBt0ooLsct2+pFAjgyd7GP3KzFaz7zaEhQJ/NNEmCaU0KOsYutg=="
|
|
5631
|
+
],
|
|
5632
|
+
[
|
|
5633
|
+
"@deepseek-ai/dsh-agent-loop",
|
|
5634
|
+
"0.1.5-rc.1",
|
|
5635
|
+
"sha512-FcpsiXMHR7M3UwZtC6CYzhmU9xhvbFuuEQisfUqW7c+G6oVhrX9kg8ytI50jXZempcdmVHZLyNeUcuDmgGtx7Q=="
|
|
5636
|
+
],
|
|
5637
|
+
[
|
|
5638
|
+
"@deepseek-ai/dsh-attachment",
|
|
5639
|
+
"0.1.5-rc.1",
|
|
5640
|
+
"sha512-uTBtB/LDlYgPI6i9Ac9jaK/bV1wN8cDTZBUkec80Yg3qMzW6K74wvBv5lPmoiXQgp4q3eOmDNEmodSS2ZxxVdg=="
|
|
5641
|
+
],
|
|
5642
|
+
[
|
|
5643
|
+
"@deepseek-ai/dsh-bash-sandbox",
|
|
5644
|
+
"0.1.5-rc.1",
|
|
5645
|
+
"sha512-mQ+/0Fo3LTIX+4k3m+P4y9e9IA6/BeNHrWW19U+Un4hBo5JtTwWhAaJN1nCdV6mG5tveXbIe2tpiv65GJdvt8w=="
|
|
5646
|
+
],
|
|
5647
|
+
[
|
|
5648
|
+
"@deepseek-ai/dsh-commands",
|
|
5649
|
+
"0.1.5-rc.1",
|
|
5650
|
+
"sha512-OMk0uVNbr2RdsItcIigp/2boGulqjei1uoQH3/DZYHB+aWWRXEYa6rk6vW3t82lnkd/6nvyQFV8eccQWZ2PQXQ=="
|
|
5651
|
+
],
|
|
5652
|
+
[
|
|
5653
|
+
"@deepseek-ai/dsh-fs",
|
|
5654
|
+
"0.1.5-rc.1",
|
|
5655
|
+
"sha512-F+loGiwsT09YpRONuFv0+bevEdfmbKBFjxxM8JkDOXpgwk1JXdGNSy7Kp4vXXVh3GcmYkd8VA7iB7NuVp51ytQ=="
|
|
5656
|
+
],
|
|
5657
|
+
[
|
|
5658
|
+
"@deepseek-ai/dsh-fs-local",
|
|
5659
|
+
"0.1.5-rc.1",
|
|
5660
|
+
"sha512-Qyqs9l+ZENq4PL7EwBpisvXcqJvCxGTrD/zH0Zj+S0eZee1kXrQjkY0gjW6dxAosGlLL7hZu0kdaD7TPxtKVcA=="
|
|
5661
|
+
],
|
|
5662
|
+
[
|
|
5663
|
+
"@deepseek-ai/dsh-fs-observation-policy",
|
|
5664
|
+
"0.1.5-rc.1",
|
|
5665
|
+
"sha512-TGu/2UrZS8KWr6x3sKLce7u0KoGrq5l+RK7ITd79n2WNzSCcAdzN5tsxUEo8JgfHij5S0QRgID0Q8DOx/6iQew=="
|
|
5666
|
+
],
|
|
5667
|
+
[
|
|
5668
|
+
"@deepseek-ai/dsh-fs-sandbox",
|
|
5669
|
+
"0.1.5-rc.1",
|
|
5670
|
+
"sha512-np+3EdQ86w609DwyaEUFGEHjSQ5i2NFypQxcM9sB+zX6DVSUR9sA2W/fmnStFJdsSzvgXYTCnJiBhlKnAXPf0g=="
|
|
5671
|
+
],
|
|
5672
|
+
[
|
|
5673
|
+
"@deepseek-ai/dsh-goal",
|
|
5674
|
+
"0.1.5-rc.1",
|
|
5675
|
+
"sha512-RF+cHqV0O7xkoqkhIst6NhMAwqXXAjTf7Z+H7FLHtZBvFAawU8zC7n7/tmS4P7rZYV+XYFYCDdNoI0GwPReCYA=="
|
|
5676
|
+
],
|
|
5677
|
+
[
|
|
5678
|
+
"@deepseek-ai/dsh-host-plugin-inventory",
|
|
5679
|
+
"0.1.5-rc.1",
|
|
5680
|
+
"sha512-xCOJ1nTW2s5etl18QhBBGpcOxiDfGxofe+4pd90/ZX+vW1vAhaHqyTYSRucOQVGyJb9zvdWCg7R3GcBYn6pUrQ=="
|
|
5681
|
+
],
|
|
5682
|
+
[
|
|
5683
|
+
"@deepseek-ai/dsh-host-webserver",
|
|
5684
|
+
"0.1.5-rc.1",
|
|
5685
|
+
"sha512-5kOu9kb0AuRN60/zwPTRcki801ozgnWAFwS1QtQ4ZNgCYIbAiU8gwJHY1//qEpUOuHS+26k+Tqq5/WCJmLGE6Q=="
|
|
5686
|
+
],
|
|
5687
|
+
[
|
|
5688
|
+
"@deepseek-ai/dsh-jobs",
|
|
5689
|
+
"0.1.5-rc.1",
|
|
5690
|
+
"sha512-0AWlZLcIpwdtV9A9fVeJ7b9jpXX0494fPL594gE/Kp1q9jHYyerIulrMHZa17kpu9W59cb1AJNPQy2xN6VDX7g=="
|
|
5691
|
+
],
|
|
5692
|
+
[
|
|
5693
|
+
"@deepseek-ai/dsh-jobs-local",
|
|
5694
|
+
"0.1.5-rc.1",
|
|
5695
|
+
"sha512-19sCxqUKduNO8E3YICSzOfajpBLPXbK/3p40GlxR6bGcOhxqhyH3TPdQS6UQjwNa5bUXHiW9GyB1xUWUUFGAJA=="
|
|
5696
|
+
],
|
|
5697
|
+
[
|
|
5698
|
+
"@deepseek-ai/dsh-llm",
|
|
5699
|
+
"0.1.5-rc.1",
|
|
5700
|
+
"sha512-KPKJFTNLjURphuF4NlS8DRK94CUYL/dKB8Hzg/22jtxAFO2paX3ifL0vhdPq9xPbcyplaF7LYlf0+3+pXFTnTg=="
|
|
5701
|
+
],
|
|
5702
|
+
[
|
|
5703
|
+
"@deepseek-ai/dsh-pwsh-sandbox",
|
|
5704
|
+
"0.1.5-rc.1",
|
|
5705
|
+
"sha512-QRD6PfcQuaRwUTn9EMIx15l1erRqk0erUamcAB3sxPyZOMWIFP7w2cp44Va8RGGDEUb9ZB1vRUH6cF4FAKSZRw=="
|
|
5706
|
+
],
|
|
5707
|
+
[
|
|
5708
|
+
"@deepseek-ai/dsh-sandbox",
|
|
5709
|
+
"0.1.5-rc.1",
|
|
5710
|
+
"sha512-xT+oTsSE7tRZVqqcj2qDZJARoK6A+Dmhf3CWPqlaFG/83Zh41kwcW2YWE4sA+vCt2pI2iqLYRw1SftSGawOtVQ=="
|
|
5711
|
+
],
|
|
5712
|
+
[
|
|
5713
|
+
"@deepseek-ai/dsh-sandbox-policy",
|
|
5714
|
+
"0.1.5-rc.1",
|
|
5715
|
+
"sha512-jLeny81NVsAiEWW8+MqmtkXJfu8CSFDPiuR8W4I/bZ7TNHrPTN7jPuk1/JlphkU4bq1N1a3iLvsDyHK+56ZLVA=="
|
|
5716
|
+
],
|
|
5717
|
+
[
|
|
5718
|
+
"@deepseek-ai/dsh-session",
|
|
5719
|
+
"0.1.5-rc.1",
|
|
5720
|
+
"sha512-0YBBrzkCbVEJolS/OpD0DZMSozmYxUTZopiG76MXInjOFBW9J4ca1a8WUjJjqelP1nJTmWGKXp92HcvNEny0Dg=="
|
|
5721
|
+
],
|
|
5722
|
+
[
|
|
5723
|
+
"@deepseek-ai/dsh-shell",
|
|
5724
|
+
"0.1.5-rc.1",
|
|
5725
|
+
"sha512-8V7iGmfsXDFMyftwQXemh1QLqcUysvy+bYWXr620/1B/sMn3oU8dhXvT8i4uD6VkMy2rds3XScUQ3XBl7ByMLA=="
|
|
5726
|
+
],
|
|
5727
|
+
[
|
|
5728
|
+
"@deepseek-ai/dsh-shell-env",
|
|
5729
|
+
"0.1.5-rc.1",
|
|
5730
|
+
"sha512-OO4AmGqqHUWRPK1PSroO/TJG3rNwoAgIMx56S3aiM7v4cUtNmUtMQ71lv9kOezcW+2VlHxfup5jhzpYIQAIiSw=="
|
|
5731
|
+
],
|
|
5732
|
+
[
|
|
5733
|
+
"@deepseek-ai/dsh-subprocess-local",
|
|
5734
|
+
"0.1.5-rc.1",
|
|
5735
|
+
"sha512-TKcqaIf1fJzjraXhwmSAQAqkPMvIjS0Y7b9fC4n7+G8eQpb3gaF/eJXn6Tx4OgFSDV5R/NLUqHaU/ogxTjdWhQ=="
|
|
5736
|
+
],
|
|
5737
|
+
[
|
|
5738
|
+
"@deepseek-ai/dsh-system-prompt",
|
|
5739
|
+
"0.1.5-rc.1",
|
|
5740
|
+
"sha512-RAdO9biQoga1vAVTQY9J7THexiOE1FOd1Nli021MQt+Zf73c83BSd2DwXb0WDilLaMeSxZPaIRRqoXpJlpmLIA=="
|
|
5741
|
+
],
|
|
5742
|
+
[
|
|
5743
|
+
"@deepseek-ai/dsh-tool-bash",
|
|
5744
|
+
"0.1.5-rc.1",
|
|
5745
|
+
"sha512-BfZ4R40I7AJFcjHgMkzh3unrp5S7mZT+szUUz4tkbMrAkgHfOdkIHhQ/BTgxWBuFzD18OfAMJ/RnegrBCFVKWw=="
|
|
5746
|
+
],
|
|
5747
|
+
[
|
|
5748
|
+
"@deepseek-ai/dsh-tool-fs",
|
|
5749
|
+
"0.1.5-rc.1",
|
|
5750
|
+
"sha512-BWLWJCJxCECFHmS8gHbnyNJlSTG+KVbVMz73Qduoo+ABeDvWj6cFVXTewAUA9jFEIS3xzJcNilsV1g5DGaEnPw=="
|
|
5751
|
+
],
|
|
5752
|
+
[
|
|
5753
|
+
"@deepseek-ai/dsh-tool-goal",
|
|
5754
|
+
"0.1.5-rc.1",
|
|
5755
|
+
"sha512-5NCniCOoCeXYXMZNPCmGlrOYIGjnGddTJVOCXNqqAcwxtOxHAoEHTtphaohIa/4Vy7mmRIHgO1KUii443ky1Bw=="
|
|
5756
|
+
],
|
|
5757
|
+
[
|
|
5758
|
+
"@deepseek-ai/dsh-tool-jobs",
|
|
5759
|
+
"0.1.5-rc.1",
|
|
5760
|
+
"sha512-SIgxnjQHl6KE+kpt7VjYCI3aw5DCeztCKGxuJzpLdqJkSoVtewp1Ofz0/Pg1R4DIIaGR8unRyqpZH8qf8uIpzA=="
|
|
5761
|
+
],
|
|
5762
|
+
[
|
|
5763
|
+
"@deepseek-ai/dsh-tool-pwsh",
|
|
5764
|
+
"0.1.5-rc.1",
|
|
5765
|
+
"sha512-UmWePfsJIUfFVj2UFyh8wacqxSXYrABGoBHXWjYFlfqpXt7rfJL31rOl8N1+uYypAVCxe4O2IquuJxfYAVhBLA=="
|
|
5766
|
+
],
|
|
5767
|
+
[
|
|
5768
|
+
"@deepseek-ai/dsh-tools",
|
|
5769
|
+
"0.1.5-rc.1",
|
|
5770
|
+
"sha512-I5AUxKTqUrC0nvRO4UcpU+f65P+nKs5BUrS2nqZehhFZ2rVxhUAJ7YdORcY6pVkBTB15nPr5gK0WPgwyfS217w=="
|
|
5771
|
+
],
|
|
5772
|
+
[
|
|
5773
|
+
"@deepseek-ai/dsh-user-approval",
|
|
5774
|
+
"0.1.5-rc.1",
|
|
5775
|
+
"sha512-fSxEBvHQnozIh5HV31t2BNodYzyv7iE2srna7p5k0Y/3R9c6DdoZyQZ9momcN9gloraq8HK5+cvrYHzgc4LYPQ=="
|
|
5776
|
+
],
|
|
5777
|
+
[
|
|
5778
|
+
"@deepseek-ai/dsh-web-app",
|
|
5779
|
+
"0.1.5-rc.1",
|
|
5780
|
+
"sha512-9V2GPqEs0A+LFJVVPt7FQK//U8oM9S0TDhl6MqO7zQftimfnH8ruQZkEZXg9zXXEWULCW0vY1DtsPjvMepA8Mw=="
|
|
5781
|
+
]
|
|
5782
|
+
].map(([name, version, integrity]) => ({
|
|
5783
|
+
name,
|
|
5784
|
+
version,
|
|
5785
|
+
integrity
|
|
5786
|
+
}));
|
|
5787
|
+
|
|
5788
|
+
//#endregion
|
|
5789
|
+
//#region src/domain/rc015-rc2-host.ts
|
|
5790
|
+
/** Exact npm registry identities for DSH 0.1.5-rc.2 (Cordis 4.0.2).
|
|
5791
|
+
* Native acceptance is recorded separately; these rows are registry-derived.
|
|
5792
|
+
*/
|
|
5793
|
+
const RC015_RC2_HOST_PACKAGES = [
|
|
5794
|
+
{
|
|
5795
|
+
"name": "@deepseek-ai/cordis",
|
|
5796
|
+
"version": "4.0.2",
|
|
5797
|
+
"integrity": "sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g=="
|
|
5798
|
+
},
|
|
5799
|
+
{
|
|
5800
|
+
"name": "@deepseek-ai/dsh",
|
|
5801
|
+
"version": "0.1.5-rc.2",
|
|
5802
|
+
"integrity": "sha512-8Xc8hCQHcIWRmTCVU/xZdp6/qMsWMeAd2ObChKDEsfhUPJFXx6H0lgeb1DxUMD86HZrrVN+1bCvn1ppjZ/fOxw=="
|
|
5803
|
+
},
|
|
5804
|
+
{
|
|
5805
|
+
"name": "@deepseek-ai/dsh-agent",
|
|
5806
|
+
"version": "0.1.5-rc.2",
|
|
5807
|
+
"integrity": "sha512-SlUL1riZmVLwMUR3jo9CP/R1cxov9dHkCJDh6JQW3fSZJVCIdPygBRlAweCUDvHxAEmPpFHxE/U3NmSUbX+vQQ=="
|
|
5808
|
+
},
|
|
5809
|
+
{
|
|
5810
|
+
"name": "@deepseek-ai/dsh-agent-loop",
|
|
5811
|
+
"version": "0.1.5-rc.2",
|
|
5812
|
+
"integrity": "sha512-24wvqVlqFmdqJ2Bhcku/vKeNy+qWSmeEeN7lvcUB+WkhcF0/Ra2G3WwcVmnahJA4+w0AKdW3W7v+YYcO/jKJpg=="
|
|
5813
|
+
},
|
|
5814
|
+
{
|
|
5815
|
+
"name": "@deepseek-ai/dsh-attachment",
|
|
5816
|
+
"version": "0.1.5-rc.2",
|
|
5817
|
+
"integrity": "sha512-S6b8/WjqzGw+dMDLRXnq+tbijDGkQh38yE+zpQytX2/w/mPR3VzGj5r6McS01WwD76vXR8WFoheSCLyCAro8WQ=="
|
|
5818
|
+
},
|
|
5819
|
+
{
|
|
5820
|
+
"name": "@deepseek-ai/dsh-bash-sandbox",
|
|
5821
|
+
"version": "0.1.5-rc.2",
|
|
5822
|
+
"integrity": "sha512-y8vwK6jf4gPq8mG71zWure803NjfqJ66Nvzr1FTB8An+IF68hT2eU+hHJCByvP0u6FuiLPRptuX/im2CxcNo1g=="
|
|
5823
|
+
},
|
|
5824
|
+
{
|
|
5825
|
+
"name": "@deepseek-ai/dsh-commands",
|
|
5826
|
+
"version": "0.1.5-rc.2",
|
|
5827
|
+
"integrity": "sha512-ODc9h2Jig+Lo4XLdxqHpHjSXsBFeUCth6Y/rToor4KVRWQMedUARlq5otPyB1lYHyQh2DonNs2uf7g3mvag57A=="
|
|
5828
|
+
},
|
|
5829
|
+
{
|
|
5830
|
+
"name": "@deepseek-ai/dsh-fs",
|
|
5831
|
+
"version": "0.1.5-rc.2",
|
|
5832
|
+
"integrity": "sha512-6DHTquXPbpYdykGayqYaXSI9t668tDCoswH/bDezV+nwj4LxjrfY/smEtgp7nC4ubyoKf1hmjpNfUonGC9e7aA=="
|
|
5833
|
+
},
|
|
5834
|
+
{
|
|
5835
|
+
"name": "@deepseek-ai/dsh-fs-local",
|
|
5836
|
+
"version": "0.1.5-rc.2",
|
|
5837
|
+
"integrity": "sha512-akUTz9D/N0ruSOzytZ8SZ330SdzG75fd7DzHsJ7KJzSif/QM0Y+RpOmGMnjlJzit4HDV2A4Etn80wSzstyp82A=="
|
|
5838
|
+
},
|
|
5839
|
+
{
|
|
5840
|
+
"name": "@deepseek-ai/dsh-fs-observation-policy",
|
|
5841
|
+
"version": "0.1.5-rc.2",
|
|
5842
|
+
"integrity": "sha512-AntY5dfkTL8WugNGHkJxCEffaTFHqdXaYm7ZrerexnLiZQscDRRjf9zI00JwOmlFM/IrioCuLf5cERfhZN5GYw=="
|
|
5843
|
+
},
|
|
5844
|
+
{
|
|
5845
|
+
"name": "@deepseek-ai/dsh-fs-sandbox",
|
|
5846
|
+
"version": "0.1.5-rc.2",
|
|
5847
|
+
"integrity": "sha512-eUxNsnM+TsjGw5OleOIcAhMnFhmQ4OAZoBYeiRMSeOMuCKWEjhxUGN8S8Hg1HxPaZVeIrUV7qFsNQzhehKj7wg=="
|
|
5848
|
+
},
|
|
5849
|
+
{
|
|
5850
|
+
"name": "@deepseek-ai/dsh-goal",
|
|
5851
|
+
"version": "0.1.5-rc.2",
|
|
5852
|
+
"integrity": "sha512-atFJaoijwAz5yZ119f82I7jMx3tGCwXOz6qoY0Likb2c5DpumWZTJgs5L19OhKbhvEW+r2MAC4MYKaUxtrbb0Q=="
|
|
5853
|
+
},
|
|
5854
|
+
{
|
|
5855
|
+
"name": "@deepseek-ai/dsh-host-plugin-inventory",
|
|
5856
|
+
"version": "0.1.5-rc.2",
|
|
5857
|
+
"integrity": "sha512-U7RTRLRs+O18ru6KzUy7LvMk/IgXnkWSalogg0abfA+QU020XZi+UpwZqM567RzC7MtpxijQk8bulGunu0lifw=="
|
|
5858
|
+
},
|
|
5859
|
+
{
|
|
5860
|
+
"name": "@deepseek-ai/dsh-host-webserver",
|
|
5861
|
+
"version": "0.1.5-rc.2",
|
|
5862
|
+
"integrity": "sha512-lFgGm9wDrHiTBANzsdoWzdfPSjWYuDFwCoNQ4Uko57Fo5XASL2unfRHGm1xZ828rwEYuGwRvJHMOuoP/17VmlA=="
|
|
5863
|
+
},
|
|
5864
|
+
{
|
|
5865
|
+
"name": "@deepseek-ai/dsh-jobs",
|
|
5866
|
+
"version": "0.1.5-rc.2",
|
|
5867
|
+
"integrity": "sha512-C3rBEuWhtDBlxMeKykFvSfBwjSPxkLsvKCFq8BrFdjDmZC1lI9GooMjPZkPxXVbogVrcOBaYVtJdOYJ4+rIpQg=="
|
|
5868
|
+
},
|
|
5869
|
+
{
|
|
5870
|
+
"name": "@deepseek-ai/dsh-jobs-local",
|
|
5871
|
+
"version": "0.1.5-rc.2",
|
|
5872
|
+
"integrity": "sha512-PCDLSktONJ+If3QCcPlRskVLXIa8hG/l0pBIgKNllz4mb7CmYlJ5w1H9pWyrFZllMO0Wm94ZS9aP3SnUcJ8R1g=="
|
|
5873
|
+
},
|
|
5874
|
+
{
|
|
5875
|
+
"name": "@deepseek-ai/dsh-llm",
|
|
5876
|
+
"version": "0.1.5-rc.2",
|
|
5877
|
+
"integrity": "sha512-Z7BVsBkK24SE4EItQeow8PHms/9GP0DSTi337vTAa/RY7tNg2Snz3INcXUj6CPZfvntQr1in9op9wLI+rfNsqA=="
|
|
5878
|
+
},
|
|
5879
|
+
{
|
|
5880
|
+
"name": "@deepseek-ai/dsh-pwsh-sandbox",
|
|
5881
|
+
"version": "0.1.5-rc.2",
|
|
5882
|
+
"integrity": "sha512-AYTAiy8wQwVoHO47e+hnG73GvHdmg+bKy3bFnrPDANCqnRZh9TJIbkxHxtJnfpBnNCcs2fhhCo6yHtGFYl7KHg=="
|
|
5883
|
+
},
|
|
5884
|
+
{
|
|
5885
|
+
"name": "@deepseek-ai/dsh-sandbox",
|
|
5886
|
+
"version": "0.1.5-rc.2",
|
|
5887
|
+
"integrity": "sha512-OTOR6Jj9cey5YkhALG0TBwZ/Z3t986aczH6fLbzoIIegix+gwNaEBOqCWs+exVJ0Z2QuN/ItXzn+xHxW8Y0dcA=="
|
|
5888
|
+
},
|
|
5889
|
+
{
|
|
5890
|
+
"name": "@deepseek-ai/dsh-sandbox-policy",
|
|
5891
|
+
"version": "0.1.5-rc.2",
|
|
5892
|
+
"integrity": "sha512-QyQSCyLFxljkvmsVWJG0xUrYTiXr1DVOxHWURf7EHnbmvYZgg2B+nFcI58+IeUB2qbB35B1ClW5NQsKjOgDm2g=="
|
|
5893
|
+
},
|
|
5894
|
+
{
|
|
5895
|
+
"name": "@deepseek-ai/dsh-session",
|
|
5896
|
+
"version": "0.1.5-rc.2",
|
|
5897
|
+
"integrity": "sha512-y+klWiGAWR4m4cc4ylurA0cW63673B4N8cr2ANMimweDZAfxL4XVBC7WiD/5DT2DtIhYmVZhz/niyS/WbniUTA=="
|
|
5898
|
+
},
|
|
5899
|
+
{
|
|
5900
|
+
"name": "@deepseek-ai/dsh-shell",
|
|
5901
|
+
"version": "0.1.5-rc.2",
|
|
5902
|
+
"integrity": "sha512-BfmNN6X0NHN2XleW0fCbtFXedXEppeDQ2oa3WsOTuhvnBftl6QWMWWMM59weE4xXker5fzqOnoJdeBR8D9qR9Q=="
|
|
5903
|
+
},
|
|
5904
|
+
{
|
|
5905
|
+
"name": "@deepseek-ai/dsh-shell-env",
|
|
5906
|
+
"version": "0.1.5-rc.2",
|
|
5907
|
+
"integrity": "sha512-fFSrfhxfvVYfDxsOuV0cjAeC/PWweW+86uOJWT+2paHXOSgM6MSDe3eTd5DHRDYnjb5XutTYu32pR49cVBHMug=="
|
|
5908
|
+
},
|
|
5909
|
+
{
|
|
5910
|
+
"name": "@deepseek-ai/dsh-subprocess-local",
|
|
5911
|
+
"version": "0.1.5-rc.2",
|
|
5912
|
+
"integrity": "sha512-DmG3lcQlAh8bTKfeyYM44cfRyJktbLL8drXrVfOvGrFk0zSs5eTSLcfhIKLT3jFJ5CKFriHYZPLdRT6Alvyy4w=="
|
|
5913
|
+
},
|
|
5914
|
+
{
|
|
5915
|
+
"name": "@deepseek-ai/dsh-system-prompt",
|
|
5916
|
+
"version": "0.1.5-rc.2",
|
|
5917
|
+
"integrity": "sha512-VtmZVKqBMJ7kzskHu0jY+Jth7jSuKMG8QB3MBPzJe9M0LL4YPMWsGKt9gGky9RXolk/ugAy4YZEv1gUqouVofA=="
|
|
5918
|
+
},
|
|
5919
|
+
{
|
|
5920
|
+
"name": "@deepseek-ai/dsh-tool-bash",
|
|
5921
|
+
"version": "0.1.5-rc.2",
|
|
5922
|
+
"integrity": "sha512-f4LmiZkZSfJfvBcEzV4q5J83VL79l/+ncLkwnJyHzsjvJbW5qFxzCy4p5FXfY/CflG0taG14/p42UJzb9qEhrQ=="
|
|
5923
|
+
},
|
|
5924
|
+
{
|
|
5925
|
+
"name": "@deepseek-ai/dsh-tool-fs",
|
|
5926
|
+
"version": "0.1.5-rc.2",
|
|
5927
|
+
"integrity": "sha512-/3AUx+V1UxVfl24fm10hwRbJnHwpBkRDHniOeocGknMvASheRiKnHpnnj9EszFRLYSpe2PRFSMuyuhXgYyd3MQ=="
|
|
5928
|
+
},
|
|
5929
|
+
{
|
|
5930
|
+
"name": "@deepseek-ai/dsh-tool-goal",
|
|
5931
|
+
"version": "0.1.5-rc.2",
|
|
5932
|
+
"integrity": "sha512-nZ0NkUxvtAsrPAd9NMXt+4kS7WPn8xIvXCwVunKfBwbrnGnFCNmIsTQoHF44J6q9VbMeSCN2eyj07w6ej3Hf+g=="
|
|
5933
|
+
},
|
|
5934
|
+
{
|
|
5935
|
+
"name": "@deepseek-ai/dsh-tool-jobs",
|
|
5936
|
+
"version": "0.1.5-rc.2",
|
|
5937
|
+
"integrity": "sha512-v4y56H3FsVBF2jUJLGLHdPeKaKJxLT4Zx/oii45UmJv5t8Bp3uCTVUoq/mfSFVuFY66q6rHJwgIOKWc4ZO3ySw=="
|
|
5938
|
+
},
|
|
5939
|
+
{
|
|
5940
|
+
"name": "@deepseek-ai/dsh-tool-pwsh",
|
|
5941
|
+
"version": "0.1.5-rc.2",
|
|
5942
|
+
"integrity": "sha512-rnHM3Jqlr7rthwfPYysXPq6zH+K8JTDqbE+xzjkbo1WIBKBZeSGr8kPjqdCZa7eUtWhmV/RFBP1qmBgDjEZaWg=="
|
|
5943
|
+
},
|
|
5944
|
+
{
|
|
5945
|
+
"name": "@deepseek-ai/dsh-tools",
|
|
5946
|
+
"version": "0.1.5-rc.2",
|
|
5947
|
+
"integrity": "sha512-k2yZuJJtszaU9lzr2aBtdeFMINrkdlk4ellbtrMokA2oySVqJmAM8dv+u9RtzduD3aRRqyr2i2hWrTACz0qOrA=="
|
|
5948
|
+
},
|
|
5949
|
+
{
|
|
5950
|
+
"name": "@deepseek-ai/dsh-user-approval",
|
|
5951
|
+
"version": "0.1.5-rc.2",
|
|
5952
|
+
"integrity": "sha512-8UpMEnEyMo6mYEVELBo0DC2iG7aJJfFMTNkU+DKD3c5Ut7ySRDzt1iRr1qni/CzqlTIpQBAkGaG77qk4q/v1bg=="
|
|
5953
|
+
},
|
|
5954
|
+
{
|
|
5955
|
+
"name": "@deepseek-ai/dsh-web-app",
|
|
5956
|
+
"version": "0.1.5-rc.2",
|
|
5957
|
+
"integrity": "sha512-Ng7YVDt9txh2BlLmu6B+V677c1bihbh/rq3EOtZK4b0JWtgdNgYb1KPIED8+aK2i+FghrCrIy6X60AyJ45eOvw=="
|
|
5958
|
+
}
|
|
5959
|
+
];
|
|
5960
|
+
|
|
5961
|
+
//#endregion
|
|
5962
|
+
//#region src/domain/host-version.ts
|
|
5963
|
+
/**
|
|
5964
|
+
* DSH host version support policy.
|
|
5965
|
+
*
|
|
5966
|
+
* Context Guard 0.5.1 supports **DSH >= 0.1.5-rc.1** and nothing older. The
|
|
5967
|
+
* policy is one value with one comparison, used by the install entry
|
|
5968
|
+
* (`peerDependencies`), by the runtime host readback, and by the decision tests
|
|
5969
|
+
* — so the advertised range and the enforced range cannot drift apart.
|
|
5970
|
+
*
|
|
5971
|
+
* ## Why a range is not enough on its own
|
|
5972
|
+
*
|
|
5973
|
+
* npm's SemVer prerelease rule is narrower than "0.1.5-rc.1 or newer": a
|
|
5974
|
+
* version carrying a prerelease satisfies a comparator set only when some
|
|
5975
|
+
* comparator in that set names the SAME `major.minor.patch` tuple and itself
|
|
5976
|
+
* carries a prerelease. For the range `>=0.1.5-rc.1` that means:
|
|
5977
|
+
*
|
|
5978
|
+
* | Candidate | Satisfies `>=0.1.5-rc.1` | Why |
|
|
5979
|
+
* | --- | --- | --- |
|
|
5980
|
+
* | `0.1.5-rc.1` | yes | the bound itself |
|
|
5981
|
+
* | `0.1.5-rc.2` | yes | same tuple, comparator has a prerelease |
|
|
5982
|
+
* | `0.1.5` | yes | a release is ordered after its own prereleases |
|
|
5983
|
+
* | `0.1.6`, `0.2.0` | yes | higher release |
|
|
5984
|
+
* | `0.1.6-rc.1` | **no** | prerelease of a DIFFERENT tuple |
|
|
5985
|
+
* | `0.2.0-rc.1` | **no** | prerelease of a DIFFERENT tuple |
|
|
5986
|
+
* | `0.1.4`, `0.1.5-alpha.9` | no | below the bound |
|
|
5987
|
+
*
|
|
5988
|
+
* No finite SemVer range expresses "every future prerelease at any base", and
|
|
5989
|
+
* an unconditional `*` would drop the lower bound entirely. The range is
|
|
5990
|
+
* therefore the honest, conservative install-time statement, and this module is
|
|
5991
|
+
* the explicit runtime/decision path for the policy itself: {@link
|
|
5992
|
+
* compareHostVersions} accepts a future different-base RC by the documented
|
|
5993
|
+
* policy while {@link evaluateMinimumHostVersion} still refuses anything below
|
|
5994
|
+
* the minimum. An unobserved new-base RC remains `unverified` for host-lock
|
|
5995
|
+
* purposes — the version policy never substitutes for the exact-graph host
|
|
5996
|
+
* audit.
|
|
5997
|
+
*/
|
|
5998
|
+
/** Lowest supported DSH host version. DSH packages version independently of Cordis. */
|
|
5999
|
+
const MIN_SUPPORTED_HOST_VERSION = "0.1.5-rc.1";
|
|
6000
|
+
/**
|
|
6001
|
+
* The exact npm range published in `peerDependencies`. It is deliberately the
|
|
6002
|
+
* plain lower bound plus the documented prerelease caveat above.
|
|
6003
|
+
*/
|
|
6004
|
+
const SUPPORTED_HOST_RANGE = `>=${MIN_SUPPORTED_HOST_VERSION}`;
|
|
6005
|
+
const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6006
|
+
function parseHostVersion(value) {
|
|
6007
|
+
const match = VERSION_PATTERN.exec(value.trim());
|
|
6008
|
+
if (!match) return void 0;
|
|
6009
|
+
const parts = [
|
|
6010
|
+
match[1],
|
|
6011
|
+
match[2],
|
|
6012
|
+
match[3]
|
|
6013
|
+
].map(Number);
|
|
6014
|
+
if (parts.some((part) => !Number.isSafeInteger(part) || part < 0)) return void 0;
|
|
6015
|
+
const prerelease = match[4] ? match[4].split(".") : [];
|
|
6016
|
+
if (prerelease.some((identifier) => identifier.length === 0)) return void 0;
|
|
6017
|
+
return {
|
|
6018
|
+
major: parts[0],
|
|
6019
|
+
minor: parts[1],
|
|
6020
|
+
patch: parts[2],
|
|
6021
|
+
prerelease
|
|
6022
|
+
};
|
|
6023
|
+
}
|
|
6024
|
+
function comparePrerelease(a, b) {
|
|
6025
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
6026
|
+
if (a.length === 0) return 1;
|
|
6027
|
+
if (b.length === 0) return -1;
|
|
6028
|
+
const length = Math.max(a.length, b.length);
|
|
6029
|
+
for (let index = 0; index < length; index += 1) {
|
|
6030
|
+
const left = a[index];
|
|
6031
|
+
const right = b[index];
|
|
6032
|
+
if (left === void 0) return -1;
|
|
6033
|
+
if (right === void 0) return 1;
|
|
6034
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
6035
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
6036
|
+
if (leftNumeric && rightNumeric) {
|
|
6037
|
+
const difference = Number(left) - Number(right);
|
|
6038
|
+
if (difference !== 0) return difference < 0 ? -1 : 1;
|
|
6039
|
+
continue;
|
|
6040
|
+
}
|
|
6041
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
6042
|
+
if (left !== right) return left < right ? -1 : 1;
|
|
6043
|
+
}
|
|
6044
|
+
return 0;
|
|
6045
|
+
}
|
|
6046
|
+
/**
|
|
6047
|
+
* SemVer precedence comparison, including the prerelease rules. Returns
|
|
6048
|
+
* `undefined` for a value that is not a version this module can order, so an
|
|
6049
|
+
* unparseable host version fails closed rather than sorting as "newer".
|
|
6050
|
+
*/
|
|
6051
|
+
function compareHostVersions(a, b) {
|
|
6052
|
+
const left = parseHostVersion(a);
|
|
6053
|
+
const right = parseHostVersion(b);
|
|
6054
|
+
if (!left || !right) return void 0;
|
|
6055
|
+
if (left.major !== right.major) return left.major < right.major ? -1 : 1;
|
|
6056
|
+
if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
|
|
6057
|
+
if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
|
|
6058
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
6059
|
+
}
|
|
6060
|
+
/** Decide the version-policy half of host support. Never a substitute for the graph lock. */
|
|
6061
|
+
function evaluateMinimumHostVersion(version, minimum = MIN_SUPPORTED_HOST_VERSION) {
|
|
6062
|
+
const comparison = compareHostVersions(version, minimum);
|
|
6063
|
+
if (comparison === void 0) return {
|
|
6064
|
+
status: "unparseable",
|
|
6065
|
+
version,
|
|
6066
|
+
minimum,
|
|
6067
|
+
reasonCode: "host_version_unparseable"
|
|
6068
|
+
};
|
|
6069
|
+
return comparison < 0 ? {
|
|
6070
|
+
status: "below_minimum",
|
|
6071
|
+
version,
|
|
6072
|
+
minimum,
|
|
6073
|
+
reasonCode: "host_version_below_minimum"
|
|
6074
|
+
} : {
|
|
6075
|
+
status: "supported",
|
|
6076
|
+
version,
|
|
6077
|
+
minimum,
|
|
6078
|
+
reasonCode: "host_version_supported"
|
|
6079
|
+
};
|
|
6080
|
+
}
|
|
6081
|
+
/**
|
|
6082
|
+
* Whether npm's own range resolution would admit this version for
|
|
6083
|
+
* {@link SUPPORTED_HOST_RANGE}. Used by the decision tests to keep the
|
|
6084
|
+
* documented prerelease table true, and by diagnostics to explain why an
|
|
6085
|
+
* install did not resolve.
|
|
6086
|
+
*/
|
|
6087
|
+
function satisfiesSupportedHostRange(version) {
|
|
6088
|
+
const candidate = parseHostVersion(version);
|
|
6089
|
+
const bound = parseHostVersion(MIN_SUPPORTED_HOST_VERSION);
|
|
6090
|
+
if (!candidate) return false;
|
|
6091
|
+
if (compareHostVersions(version, MIN_SUPPORTED_HOST_VERSION) < 0) return false;
|
|
6092
|
+
if (candidate.prerelease.length === 0) return true;
|
|
6093
|
+
return candidate.major === bound.major && candidate.minor === bound.minor && candidate.patch === bound.patch;
|
|
6094
|
+
}
|
|
6095
|
+
|
|
4052
6096
|
//#endregion
|
|
4053
6097
|
//#region src/domain/host-lock.ts
|
|
4054
6098
|
/**
|
|
4055
|
-
* Capability expectations shared by every
|
|
4056
|
-
*
|
|
4057
|
-
*
|
|
4058
|
-
*
|
|
4059
|
-
*
|
|
6099
|
+
* Capability expectations shared by every registered cohort.
|
|
6100
|
+
*
|
|
6101
|
+
* Every row is a host contract Guard actually consumes, re-checked against the
|
|
6102
|
+
* 0.1.5-rc.1 package surfaces: `ctx.sessions.flush()` still returns whether a
|
|
6103
|
+
* durability listener participated; `tools.guard()` is still a monotonic
|
|
6104
|
+
* post-policy denial; the Goal service still exposes `get`/`disarm` with a
|
|
6105
|
+
* disarming `pause`; the `update_goal` tool is still the pinned pre-commit gate;
|
|
6106
|
+
* `ctx.jobs.get()` still yields the `dsh.jobs.v1` status vocabulary; and
|
|
6107
|
+
* `dsh-tool-fs` still registers `read`/`write`/`edit` with the same parameter
|
|
6108
|
+
* and result contract (`dsh.fs-tools.v1`).
|
|
6109
|
+
*
|
|
6110
|
+
* What is NOT a row, because it changed rather than stayed compatible: the
|
|
6111
|
+
* Session event API and vocabulary. Guard 0.5.1 supports only V3
|
|
6112
|
+
* `snapshotEvents()` and refuses a session that does not expose it, so a V2
|
|
6113
|
+
* host is rejected by the cohort's exact package rows before any capability row
|
|
6114
|
+
* is consulted.
|
|
4060
6115
|
*/
|
|
4061
6116
|
const AUDITED_CAPABILITY_ROWS = [
|
|
4062
6117
|
{
|
|
@@ -4116,20 +6171,32 @@ const AUDITED_CAPABILITY_ROWS = [
|
|
|
4116
6171
|
}
|
|
4117
6172
|
}))
|
|
4118
6173
|
];
|
|
4119
|
-
function defineCohort(id, supportedGoalVersions, auditedPlatforms, packages) {
|
|
6174
|
+
function defineCohort(id, supportedGoalVersions, auditedPlatforms, packages, auditProvenance = "native-audited", acceptedPlatforms = auditedPlatforms) {
|
|
4120
6175
|
return {
|
|
4121
6176
|
id,
|
|
4122
6177
|
manifestVersion: 1,
|
|
4123
6178
|
supportedGoalVersions,
|
|
4124
6179
|
auditedPlatforms,
|
|
6180
|
+
acceptedPlatforms,
|
|
6181
|
+
auditProvenance,
|
|
4125
6182
|
packages,
|
|
4126
|
-
capabilities: [
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
6183
|
+
capabilities: [
|
|
6184
|
+
{
|
|
6185
|
+
name: "host_cohort",
|
|
6186
|
+
value: {
|
|
6187
|
+
k: "s",
|
|
6188
|
+
v: id
|
|
6189
|
+
}
|
|
6190
|
+
},
|
|
6191
|
+
{
|
|
6192
|
+
name: "host_audit_provenance",
|
|
6193
|
+
value: {
|
|
6194
|
+
k: "s",
|
|
6195
|
+
v: auditProvenance
|
|
6196
|
+
}
|
|
6197
|
+
},
|
|
6198
|
+
...AUDITED_CAPABILITY_ROWS
|
|
6199
|
+
]
|
|
4133
6200
|
};
|
|
4134
6201
|
}
|
|
4135
6202
|
/**
|
|
@@ -4325,8 +6392,8 @@ const ALPHA2_DSHMARKET_139_HOST_PACKAGES = ALPHA2_HOST_PACKAGES.map((row) => row
|
|
|
4325
6392
|
/**
|
|
4326
6393
|
* Historical audited host cohort registry. Every entry keeps the exact package
|
|
4327
6394
|
* identities audited natively for a past Guard release (CG-DSH-001 whole-graph
|
|
4328
|
-
* contracts). These are historical verification facts only: since 0.5.
|
|
4329
|
-
* active support
|
|
6395
|
+
* contracts). These are historical verification facts only: since 0.5.1 the
|
|
6396
|
+
* active support targets are `0.1.5-rc.1` and `0.1.5-rc.2`, so an installed graph from any of
|
|
4330
6397
|
* these cohorts — including previous RCs and alphas — is no longer an active
|
|
4331
6398
|
* support entry and fails closed in `evaluateHostLock`.
|
|
4332
6399
|
*/
|
|
@@ -4506,15 +6573,22 @@ const LEGACY_HOST_COHORTS = [
|
|
|
4506
6573
|
defineCohort("dsh-0.1.2-alpha.2", ["0.1.2-alpha.2"], ["posix", "windows"], ALPHA2_HOST_PACKAGES),
|
|
4507
6574
|
defineCohort("dsh-0.1.2-alpha.2-dshmarket-1.39.0", ["0.1.2-alpha.2"], ["posix", "windows"], ALPHA2_DSHMARKET_139_HOST_PACKAGES),
|
|
4508
6575
|
defineCohort("dsh-0.1.2-alpha.3", ["0.1.2-alpha.3"], ["posix", "windows"], ALPHA3_HOST_PACKAGES),
|
|
4509
|
-
defineCohort("dsh-0.1.2-rc.1", ["0.1.2-rc.1"], ["posix", "windows"], RC1_HOST_PACKAGES)
|
|
6576
|
+
defineCohort("dsh-0.1.2-rc.1", ["0.1.2-rc.1"], ["posix", "windows"], RC1_HOST_PACKAGES),
|
|
6577
|
+
defineCohort("dsh-0.1.5-rc.1", ["0.1.5-rc.1"], [], RC015_HOST_PACKAGES, "registry-derived-pending-native-audit", ["posix", "windows"])
|
|
4510
6578
|
];
|
|
6579
|
+
/** Baseline cohort retained for callers that need a default fixture. */
|
|
6580
|
+
const ACTIVE_HOST_COHORT_ID = "dsh-0.1.5-rc.1";
|
|
6581
|
+
const ACTIVE_HOST_COHORT_IDS = [ACTIVE_HOST_COHORT_ID, "dsh-0.1.5-rc.2"];
|
|
4511
6582
|
/** Core-lock/v1 separates optional market identity from the audited DSH graph.
|
|
4512
|
-
* The active support
|
|
6583
|
+
* The active support targets are the exact registered rc.1 and rc.2 graphs:
|
|
4513
6584
|
* historical cohorts stay in `LEGACY_HOST_COHORTS` as verification data but are
|
|
4514
6585
|
* never silently re-labelled as accepted active locks, and an installed
|
|
4515
|
-
* historical graph fails closed under `evaluateHostLock`.
|
|
6586
|
+
* historical graph fails closed under `evaluateHostLock`. The version policy
|
|
6587
|
+
* (`>=0.1.5-rc.1`) and the graph lock are separate judgments: a newer host that
|
|
6588
|
+
* has not been registered here is "unverified / pending audit", never
|
|
6589
|
+
* supported by range alone.
|
|
4516
6590
|
*/
|
|
4517
|
-
const HOST_COHORTS = LEGACY_HOST_COHORTS
|
|
6591
|
+
const HOST_COHORTS = [...LEGACY_HOST_COHORTS, defineCohort("dsh-0.1.5-rc.2", ["0.1.5-rc.2"], [], RC015_RC2_HOST_PACKAGES, "registry-derived-pending-native-audit", ["posix", "windows"])].filter((cohort) => ACTIVE_HOST_COHORT_IDS.includes(cohort.id)).map((cohort) => ({
|
|
4518
6592
|
...cohort,
|
|
4519
6593
|
id: `${cohort.id}-core-v1`,
|
|
4520
6594
|
manifestVersion: 2,
|
|
@@ -4534,17 +6608,30 @@ const HOST_COHORTS = LEGACY_HOST_COHORTS.filter((cohort) => cohort.id === "dsh-0
|
|
|
4534
6608
|
v: "dsh-core/v1"
|
|
4535
6609
|
}
|
|
4536
6610
|
},
|
|
6611
|
+
{
|
|
6612
|
+
name: "host_audit_provenance",
|
|
6613
|
+
value: {
|
|
6614
|
+
k: "s",
|
|
6615
|
+
v: cohort.auditProvenance
|
|
6616
|
+
}
|
|
6617
|
+
},
|
|
4537
6618
|
...AUDITED_CAPABILITY_ROWS
|
|
4538
6619
|
]
|
|
4539
6620
|
}));
|
|
4540
6621
|
/**
|
|
4541
|
-
*
|
|
4542
|
-
*
|
|
4543
|
-
*
|
|
4544
|
-
*
|
|
4545
|
-
*
|
|
6622
|
+
* Baseline fixture package identities (DSH 0.1.5-rc.1). The cohort
|
|
6623
|
+
* is an atomic whole-graph contract (CG-DSH-001): any drifted, duplicated,
|
|
6624
|
+
* unknown-version, unbound, OR MISSING row fails the whole lock closed
|
|
6625
|
+
* (`host_lock_missing`); no capability inherits independence from a partially
|
|
6626
|
+
* present graph.
|
|
4546
6627
|
*/
|
|
4547
6628
|
const EXPECTED_HOST_PACKAGES = HOST_COHORTS[0].packages;
|
|
6629
|
+
/**
|
|
6630
|
+
* The `@deepseek-ai/dsh` launcher version of the baseline fixture, read from the
|
|
6631
|
+
* cohort rows rather than hardcoded, so a cohort bump cannot leave a stale
|
|
6632
|
+
* literal behind in the target-inspection path.
|
|
6633
|
+
*/
|
|
6634
|
+
const ACTIVE_HOST_LAUNCHER_VERSION = EXPECTED_HOST_PACKAGES.find((row) => row.name === "@deepseek-ai/dsh")?.version;
|
|
4548
6635
|
const packageNames = (...names) => new Set(names);
|
|
4549
6636
|
const BASE_HOST_PACKAGES = packageNames("@deepseek-ai/cordis", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-tools");
|
|
4550
6637
|
const GOAL_HOST_PACKAGES = packageNames("@deepseek-ai/dsh-goal", "@deepseek-ai/dsh-tool-goal");
|
|
@@ -4559,6 +6646,16 @@ const HOST_CAPABILITY_PACKAGE_GROUPS = {
|
|
|
4559
6646
|
filesystem: packageNames("@deepseek-ai/dsh-tool-fs", "@deepseek-ai/dsh-fs", "@deepseek-ai/dsh-fs-local", "@deepseek-ai/dsh-fs-sandbox", "@deepseek-ai/dsh-fs-observation-policy", "@deepseek-ai/dsh-sandbox", "@deepseek-ai/dsh-sandbox-policy", "@deepseek-ai/dsh-user-approval", "@deepseek-ai/dsh-attachment", "@deepseek-ai/dsh-system-prompt")
|
|
4560
6647
|
};
|
|
4561
6648
|
/**
|
|
6649
|
+
* The host version a package graph records, for the version-policy decision.
|
|
6650
|
+
*
|
|
6651
|
+
* Every DSH package versions with the host, so the graph's own `dsh` row is the
|
|
6652
|
+
* version the caller is running. A graph without that row leaves the version
|
|
6653
|
+
* unknown, and an unknown version is not treated as supported.
|
|
6654
|
+
*/
|
|
6655
|
+
function hostVersionFromPackages(rows) {
|
|
6656
|
+
return rows.find((row) => row.name === "@deepseek-ai/dsh")?.version;
|
|
6657
|
+
}
|
|
6658
|
+
/**
|
|
4562
6659
|
* Atomically select the audited cohort for one supplied package graph. A
|
|
4563
6660
|
* graph matches a cohort only when every row carries version and integrity,
|
|
4564
6661
|
* each exactly equals that cohort's audited row, and the graph covers the
|
|
@@ -4587,7 +6684,7 @@ function selectHostCohort(rows, platform) {
|
|
|
4587
6684
|
const candidates = HOST_COHORTS.filter((cohort) => identityMatches.every((matches) => matches.includes(cohort)));
|
|
4588
6685
|
const consistentCohort = candidates.filter((cohort) => cohort.packages.length === rows.length && cohort.packages.every((expected) => rows.filter((row) => row.name === expected.name).length === 1))[0] ?? candidates[0];
|
|
4589
6686
|
if (consistentCohort !== void 0 && unboundCount === 0) {
|
|
4590
|
-
if (platform && !consistentCohort.
|
|
6687
|
+
if (platform && !consistentCohort.acceptedPlatforms.includes(platform)) return {
|
|
4591
6688
|
cohort: consistentCohort,
|
|
4592
6689
|
consistent: false,
|
|
4593
6690
|
reasonCode: "host_cohort_platform_not_audited"
|
|
@@ -4702,13 +6799,17 @@ function evaluateHostLock(rows, context = {}) {
|
|
|
4702
6799
|
const missingPackages = cohort.packages.map((row) => row.name).filter((name) => (counts.get(name) ?? 0) === 0).sort((a, b) => a.localeCompare(b));
|
|
4703
6800
|
const registryNames = new Set(HOST_COHORTS.flatMap((entry) => entry.packages.map((row) => row.name)));
|
|
4704
6801
|
const unknown = supplied.find((row) => !registryNames.has(row.name));
|
|
6802
|
+
const hostVersionValue = context.hostVersion ?? hostVersionFromPackages(supplied);
|
|
6803
|
+
const hostVersion = hostVersionValue === void 0 ? void 0 : evaluateMinimumHostVersion(hostVersionValue);
|
|
4705
6804
|
const baseResult = {
|
|
4706
6805
|
digest: digest$1,
|
|
4707
6806
|
goalAvailable,
|
|
4708
6807
|
packages: supplied,
|
|
4709
6808
|
capabilities,
|
|
4710
6809
|
cohortId: cohort.id,
|
|
6810
|
+
auditProvenance: cohort.auditProvenance,
|
|
4711
6811
|
missingPackages,
|
|
6812
|
+
...hostVersion ? { hostVersion } : {},
|
|
4712
6813
|
...context.platform ? { platform: context.platform } : {},
|
|
4713
6814
|
...context.profileKind ? { profileKind: context.profileKind } : {}
|
|
4714
6815
|
};
|
|
@@ -4784,6 +6885,12 @@ function evaluateHostLock(rows, context = {}) {
|
|
|
4784
6885
|
reasonCode: failure.reasonCode
|
|
4785
6886
|
};
|
|
4786
6887
|
}
|
|
6888
|
+
if (hostVersion?.status === "below_minimum" || hostVersion?.status === "unparseable") return {
|
|
6889
|
+
...baseResult,
|
|
6890
|
+
status: "unsupported",
|
|
6891
|
+
goalAvailable: false,
|
|
6892
|
+
reasonCode: hostVersion.status === "below_minimum" ? "host_lock_version_below_minimum" : "host_lock_version_unparseable"
|
|
6893
|
+
};
|
|
4787
6894
|
return {
|
|
4788
6895
|
...baseResult,
|
|
4789
6896
|
status: "supported"
|
|
@@ -4998,52 +7105,6 @@ function bindExecutableIdentity(resolution, effect) {
|
|
|
4998
7105
|
}
|
|
4999
7106
|
const DEFAULT_HOST_LOCK = evaluateHostLock(EXPECTED_HOST_PACKAGES);
|
|
5000
7107
|
|
|
5001
|
-
//#endregion
|
|
5002
|
-
//#region src/domain/goal-gate.ts
|
|
5003
|
-
function hasCurrentCertificate(projection) {
|
|
5004
|
-
const checkpoint = projection.checkpoints.at(-1);
|
|
5005
|
-
let reason;
|
|
5006
|
-
if (projection.integrity !== "valid") reason = "integrity_invalid";
|
|
5007
|
-
else if (projection.hostStatus !== "supported") reason = "host_lock_unsupported";
|
|
5008
|
-
else if (!checkpoint || checkpoint.result !== "certified") reason = "certificate_missing";
|
|
5009
|
-
else if (checkpoint.epoch !== projection.epoch) reason = "stale_epoch";
|
|
5010
|
-
else if (checkpoint.sessionRefDigest !== projection.sessionRefDigest) reason = "foreign_session";
|
|
5011
|
-
else if (checkpoint.hostLockDigest !== projection.hostLockDigest) reason = "stale_host_lock";
|
|
5012
|
-
else if (checkpoint.contractRevision !== projection.contractRevision) reason = "stale_contract_revision";
|
|
5013
|
-
else if (projection.currentGoalRef ? checkpoint.goalRef?.id !== projection.currentGoalRef.id || checkpoint.goalRef.revision !== projection.currentGoalRef.revision : checkpoint.goalRef !== void 0) reason = "stale_goal_ref";
|
|
5014
|
-
projection.certificateStatusReason = reason;
|
|
5015
|
-
return reason === void 0;
|
|
5016
|
-
}
|
|
5017
|
-
/**
|
|
5018
|
-
* Denies `update_goal(action=complete)` while the guard is enabled and no
|
|
5019
|
-
* current completion certificate exists. The gate itself has no bypass; a
|
|
5020
|
-
* workflow that genuinely finished but cannot certify (for example a contract
|
|
5021
|
-
* polluted by session-layer talk, or evidence that lives in another session)
|
|
5022
|
-
* has three explicit remediation routes:
|
|
5023
|
-
*
|
|
5024
|
-
* 1. `/context-guard off` disables the guard, so completion is no longer
|
|
5025
|
-
* gated. Use only after the user confirms the work is actually done.
|
|
5026
|
-
* 2. `/context-guard clear` supersedes every pending requirement and
|
|
5027
|
-
* acceptance under a `CLEAR:<revision>` sentinel (prohibitions are
|
|
5028
|
-
* retained) and bumps the contract revision; an empty-binding checkpoint
|
|
5029
|
-
* can then certify while the guard stays enabled.
|
|
5030
|
-
* 3. `update_goal(action=blocked)` records the blocker truthfully, which is
|
|
5031
|
-
* never denied by this gate.
|
|
5032
|
-
*/
|
|
5033
|
-
function goalCompletionDenial(projection, toolName, argumentsValue, configuredToolName = "update_goal") {
|
|
5034
|
-
if (toolName !== configuredToolName || typeof argumentsValue !== "object" || argumentsValue === null) return void 0;
|
|
5035
|
-
if (argumentsValue.action !== "complete") return void 0;
|
|
5036
|
-
if (!projection.enabled) return void 0;
|
|
5037
|
-
const args = argumentsValue;
|
|
5038
|
-
if (projection.hostStatus !== "supported") return `Context Guard denial [stale_host]: host lock is unsupported or unavailable (${projection.hostReasonCode ?? "unknown_host"}).`;
|
|
5039
|
-
if (!projection.currentGoalRef) return "Context Guard denial [no_goal]: no current Goal reference is available.";
|
|
5040
|
-
if (args.goal_id !== projection.currentGoalRef.id || args.revision !== projection.currentGoalRef.revision) return "Context Guard denial [stale_goal_ref]: update_goal must use the exact current goal_id and revision.";
|
|
5041
|
-
if (hasCurrentCertificate(projection)) return void 0;
|
|
5042
|
-
if (projection.certificateStatusReason === "stale_host_lock") return "Context Guard denial [stale_host]: the completion certificate belongs to a different host identity.";
|
|
5043
|
-
if (projection.certificateStatusReason === "stale_goal_ref") return "Context Guard denial [stale_goal_ref]: the completion certificate belongs to a different Goal reference.";
|
|
5044
|
-
return projection.integrity === "valid" ? "Context Guard denial [certificate_missing]: a current completion certificate is required." : "Context Guard denial [certificate_missing]: integrity is unknown or corrupt, so no current certificate is usable.";
|
|
5045
|
-
}
|
|
5046
|
-
|
|
5047
7108
|
//#endregion
|
|
5048
7109
|
//#region src/domain/shell-parse.ts
|
|
5049
7110
|
const TWO_CHAR_OPS = new Set([
|
|
@@ -5781,13 +7842,19 @@ function structuredTerminalFacts(meta) {
|
|
|
5781
7842
|
const rawSignal = record.signal;
|
|
5782
7843
|
if (rawSignal !== void 0 && rawSignal !== null) return {
|
|
5783
7844
|
exitCode: typeof rawExit === "number" ? rawExit : void 0,
|
|
5784
|
-
negative: true
|
|
7845
|
+
negative: true,
|
|
7846
|
+
marked: true
|
|
5785
7847
|
};
|
|
5786
7848
|
if (typeof rawExit === "number") return {
|
|
5787
7849
|
exitCode: rawExit,
|
|
5788
|
-
negative: false
|
|
7850
|
+
negative: false,
|
|
7851
|
+
marked: true
|
|
5789
7852
|
};
|
|
5790
7853
|
}
|
|
7854
|
+
/** `[exit code: N]`, `[shell exited: code N]`, `[Command finished with exit code N]`. */
|
|
7855
|
+
const TERMINAL_EXIT_MARKER = /^\[(?:exit code|shell exited: code|command finished with exit code)\s*:?\s*(\d+)\]$/;
|
|
7856
|
+
/** Negative markers with no exit code of their own. */
|
|
7857
|
+
const TERMINAL_NEGATIVE_MARKER = /^\[(?:timed out[^\]]*|sandbox[^\]]*|killed by signal[^\]]*|shell killed by signal[^\]]*|shell exited|command timed out or oom|interrupted[^\]]*)\]$/;
|
|
5791
7858
|
function extractTerminalFacts(textContent) {
|
|
5792
7859
|
const lines = textContent.split(/\r?\n/);
|
|
5793
7860
|
let index = lines.length - 1;
|
|
@@ -5800,19 +7867,23 @@ function extractTerminalFacts(textContent) {
|
|
|
5800
7867
|
const timeoutIntroAtHead = resetStripped && lines.length > 0 && PERSISTENT_TIMEOUT_INTRO.test(lines[0].trim());
|
|
5801
7868
|
let exitCode;
|
|
5802
7869
|
let negative = timeoutIntroAtHead;
|
|
7870
|
+
let marked = timeoutIntroAtHead;
|
|
5803
7871
|
while (index >= 0) {
|
|
5804
|
-
const line = lines[index].trim();
|
|
5805
|
-
const exitMatch = line.match(
|
|
5806
|
-
const negativeLine = /^\[(?:timed out|sandbox[^\]]*|killed by signal[^\]]*|shell killed by signal[^\]]*|shell exited|interrupted[^\]]*)[^\]]*\]$/i.test(line);
|
|
7872
|
+
const line = lines[index].trim().toLowerCase();
|
|
7873
|
+
const exitMatch = line.match(TERMINAL_EXIT_MARKER);
|
|
5807
7874
|
if (exitMatch) {
|
|
5808
7875
|
if (exitCode === void 0) exitCode = Number(exitMatch[1]);
|
|
5809
|
-
|
|
5810
|
-
else
|
|
7876
|
+
marked = true;
|
|
7877
|
+
} else if (TERMINAL_NEGATIVE_MARKER.test(line)) {
|
|
7878
|
+
negative = true;
|
|
7879
|
+
marked = true;
|
|
7880
|
+
} else break;
|
|
5811
7881
|
index -= 1;
|
|
5812
7882
|
}
|
|
5813
7883
|
return {
|
|
5814
7884
|
exitCode,
|
|
5815
|
-
negative
|
|
7885
|
+
negative,
|
|
7886
|
+
marked
|
|
5816
7887
|
};
|
|
5817
7888
|
}
|
|
5818
7889
|
function metaUrls(meta) {
|
|
@@ -6030,7 +8101,8 @@ function extractToolSubject(call, result, defaultCwd, hostLock) {
|
|
|
6030
8101
|
const commandCwd = typeof args.workdir === "string" ? args.workdir : defaultCwd;
|
|
6031
8102
|
const action = structured?.semanticAction ?? semanticActionFromCommand(command);
|
|
6032
8103
|
const deterministic = commandDetails.status === "supported" && !backgrounded && isDeterministicCheck(command);
|
|
6033
|
-
const
|
|
8104
|
+
const unmarkedSuccessAllowed = (call.name === "bash" || call.name === "pwsh") && !terminal.marked;
|
|
8105
|
+
const outcome = backgrounded ? "unknown" : result.error || terminal.negative ? "failure" : terminal.exitCode === void 0 ? unmarkedSuccessAllowed ? "success" : "unknown" : terminal.exitCode === 0 ? "success" : "failure";
|
|
6034
8106
|
const subject = {
|
|
6035
8107
|
capabilities: ["shell", ...deterministic ? ["deterministic-check"] : []],
|
|
6036
8108
|
subjects: unique(commandDetails.subjects),
|
|
@@ -6241,14 +8313,22 @@ function resolveArtifact(path$1, scope) {
|
|
|
6241
8313
|
if (/^[A-Za-z]:[\\/]/.test(path$1) || path$1.startsWith("/") || path$1.startsWith("\\")) return path$1;
|
|
6242
8314
|
return `${scope.cwd.replace(/[\\/]+$/, "")}/${path$1}`;
|
|
6243
8315
|
}
|
|
6244
|
-
/**
|
|
8316
|
+
/**
|
|
8317
|
+
* Capture one canonical root text through the authority-block segmentation.
|
|
6245
8318
|
* `prefix` keeps the historical `m<seq>` source identity; a remainder uses
|
|
6246
|
-
* `m<seq>:r` so confirmation follow-ups stay traceable to their message.
|
|
6247
|
-
|
|
8319
|
+
* `m<seq>:r` so confirmation follow-ups stay traceable to their message.
|
|
8320
|
+
*
|
|
8321
|
+
* `legacy` marks a message that predates the first protocol boundary in this
|
|
8322
|
+
* log. Capture semantics are now version-independent (see `domain/semantics.ts`),
|
|
8323
|
+
* but a pre-boundary message keeps the historical authority relabelling rule:
|
|
8324
|
+
* an item whose action/target could not be derived deterministically stays
|
|
8325
|
+
* `legacy_authority_unclassified` instead of being retroactively authorized.
|
|
8326
|
+
*/
|
|
8327
|
+
function captureRootText(projection, text, seq, scope, legacy, priorRootMessages, prefix = `m${seq}`, coordinationSplit = true) {
|
|
6248
8328
|
const blocks = segmentAuthorityBlocks(text, priorRootMessages);
|
|
6249
8329
|
for (const block$1 of blocks) {
|
|
6250
8330
|
if (!block$1.capture) continue;
|
|
6251
|
-
insertItems(projection, block$1.text, `${prefix}:${block$1.blockId}`, scope, block$1.authority === "root_adoption" ? "root_adoption" : "root_instruction",
|
|
8331
|
+
insertItems(projection, block$1.text, `${prefix}:${block$1.blockId}`, scope, block$1.authority === "root_adoption" ? "root_adoption" : "root_instruction", legacy, block$1.kind === "instruction" || block$1.authority === "root_adoption", coordinationSplit);
|
|
6252
8332
|
}
|
|
6253
8333
|
priorRootMessages.push(text);
|
|
6254
8334
|
if (priorRootMessages.length > 16) priorRootMessages.shift();
|
|
@@ -6259,16 +8339,30 @@ function captureRootText(projection, text, seq, scope, protocolBoundarySeq, capt
|
|
|
6259
8339
|
* item, so evidence for one file cannot close a message that also covers other
|
|
6260
8340
|
* files or embeds prohibitions.
|
|
6261
8341
|
*/
|
|
6262
|
-
function insertItems(projection, text, sourceMessageId, scope, authority = "root_instruction", legacy = false, legacyAuthorityProven = false,
|
|
8342
|
+
function insertItems(projection, text, sourceMessageId, scope, authority = "root_instruction", legacy = false, legacyAuthorityProven = false, coordinationSplit = true) {
|
|
6263
8343
|
const before = new Set(projection.items.keys());
|
|
6264
|
-
for (const segment of segmentClauses(text,
|
|
8344
|
+
for (const segment of segmentClauses(text, { coordinationSplit })) {
|
|
6265
8345
|
if (classifyUserInteraction(segment.body) === "conversational") continue;
|
|
6266
8346
|
if (segment.kind === "requirement" && segment.paths.length === 0 && isInstructionFraming(segment.body)) continue;
|
|
6267
|
-
if (segment.
|
|
6268
|
-
insert(projection, segment
|
|
8347
|
+
if (segment.paths.length === 0) {
|
|
8348
|
+
insert(projection, segment, sourceMessageId, scope.cwd || "scope", "scope");
|
|
6269
8349
|
continue;
|
|
6270
8350
|
}
|
|
6271
|
-
for (const path$1 of segment.paths) insert(projection, segment
|
|
8351
|
+
for (const path$1 of segment.paths) insert(projection, segment, sourceMessageId, resolveArtifact(path$1, scope), "artifact");
|
|
8352
|
+
}
|
|
8353
|
+
for (const [id, item] of projection.items) {
|
|
8354
|
+
if (before.has(id)) continue;
|
|
8355
|
+
if (item.kind !== "requirement" || item.waitAuthorization || item.authorityDisposition === "conditional_wait") continue;
|
|
8356
|
+
for (const [otherId, other] of projection.items) {
|
|
8357
|
+
if (otherId === id || other.status !== "pending") continue;
|
|
8358
|
+
if (!other.waitAuthorization || other.kind !== "requirement") continue;
|
|
8359
|
+
if (other.semanticAction !== item.semanticAction) continue;
|
|
8360
|
+
const action = item.semanticAction;
|
|
8361
|
+
if (!action || !isStatefulAction(action)) continue;
|
|
8362
|
+
if (!requestedTargetMatchesResolved(action, other.requestedTarget, item.requestedTarget)) continue;
|
|
8363
|
+
supersedeItem(projection.items, otherId, item);
|
|
8364
|
+
break;
|
|
8365
|
+
}
|
|
6272
8366
|
}
|
|
6273
8367
|
for (const [id, item] of projection.items) {
|
|
6274
8368
|
if (before.has(id)) continue;
|
|
@@ -6283,11 +8377,13 @@ function insertItems(projection, text, sourceMessageId, scope, authority = "root
|
|
|
6283
8377
|
else item.authority = authority;
|
|
6284
8378
|
}
|
|
6285
8379
|
}
|
|
6286
|
-
function insert(projection,
|
|
8380
|
+
function insert(projection, segment, sourceMessageId, subject, surface) {
|
|
6287
8381
|
const revision = projection.contractRevision + 1;
|
|
6288
|
-
const id = nextId(projection.items, kind);
|
|
6289
|
-
const
|
|
6290
|
-
const
|
|
8382
|
+
const id = nextId(projection.items, segment.kind);
|
|
8383
|
+
const method = extractMethod(segment.body);
|
|
8384
|
+
const operation = extractOperation(segment.body);
|
|
8385
|
+
const item = captureItem(segment.kind, segment.body, sourceMessageId, id, revision, subject, surface, method, operation, segment.interpretation);
|
|
8386
|
+
const duplicate = [...projection.items.values()].find((existing) => existing.kind === segment.kind && existing.status === "pending" && existing.textSha256 === item.textSha256 && existing.verification.subject === subject);
|
|
6291
8387
|
if (duplicate) supersedeItem(projection.items, duplicate.id, item);
|
|
6292
8388
|
else projection.items.set(id, item);
|
|
6293
8389
|
projection.contractRevision = item.revision;
|
|
@@ -6296,7 +8392,7 @@ function insert(projection, kind, body, sourceMessageId, subject, surface, captu
|
|
|
6296
8392
|
* Pure, deterministic re-derivation of the guard projection from the DSH
|
|
6297
8393
|
* native event log. Context Guard never writes custom session events, so every
|
|
6298
8394
|
* piece of state is derived from `command/run`, `user/message`, `tool/call`,
|
|
6299
|
-
* `tool/result`, `tool/
|
|
8395
|
+
* `tool/result`, `tool/ptc-dispatch-start`, `tool/ptc-dispatch`, and
|
|
6300
8396
|
* `compaction/summary`.
|
|
6301
8397
|
*/
|
|
6302
8398
|
function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLock = DEFAULT_HOST_LOCK) {
|
|
@@ -6348,19 +8444,63 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
|
|
|
6348
8444
|
compacted = true;
|
|
6349
8445
|
lastCompactionSeq = event.seq;
|
|
6350
8446
|
break;
|
|
8447
|
+
case "turn/start": {
|
|
8448
|
+
const started = asRecord(event.data);
|
|
8449
|
+
if (typeof started?.turn === "number" && Number.isSafeInteger(started.turn)) projection.hostTurn = started.turn;
|
|
8450
|
+
break;
|
|
8451
|
+
}
|
|
6351
8452
|
case "user/message": {
|
|
6352
8453
|
if (isProtocolBoundaryNotice(event) || isProtocolBoundaryNotice(event, CAPTURE_V042_NOTICE) || isProtocolBoundaryNotice(event, PROTOCOL_V4_NOTICE)) break;
|
|
8454
|
+
{
|
|
8455
|
+
const record = asRecord(event.data);
|
|
8456
|
+
const recordSource = asRecord(record?.source);
|
|
8457
|
+
const recordText = extractTextContent(record?.content ?? []);
|
|
8458
|
+
if (recordSource?.kind === "plugin" && recordSource.plugin === "context-guard" && recordText.startsWith(NO_PROGRESS_RECORD_PREFIX)) {
|
|
8459
|
+
const parsed = asRecord(parseArguments(recordText.slice(NO_PROGRESS_RECORD_PREFIX.length)));
|
|
8460
|
+
const fingerprint$1 = typeof parsed?.fingerprint === "string" ? parsed.fingerprint : void 0;
|
|
8461
|
+
const attempt = typeof parsed?.attempt === "number" && Number.isSafeInteger(parsed.attempt) && parsed.attempt > 0 ? parsed.attempt : void 0;
|
|
8462
|
+
const boundaryKey = typeof parsed?.boundaryKey === "string" ? parsed.boundaryKey : void 0;
|
|
8463
|
+
if (fingerprint$1 && attempt !== void 0 && boundaryKey !== void 0) {
|
|
8464
|
+
const claims = projection.noProgressClaims.get(fingerprint$1) ?? /* @__PURE__ */ new Map();
|
|
8465
|
+
if (!claims.has(boundaryKey)) claims.set(boundaryKey, attempt);
|
|
8466
|
+
projection.noProgressClaims.set(fingerprint$1, claims);
|
|
8467
|
+
}
|
|
8468
|
+
break;
|
|
8469
|
+
}
|
|
8470
|
+
if (recordSource?.kind === "plugin" && recordSource.plugin === "context-guard" && recordText.startsWith(CONTROL_RECORD_PREFIX)) {
|
|
8471
|
+
const parsed = asRecord(parseArguments(recordText.slice(CONTROL_RECORD_PREFIX.length)));
|
|
8472
|
+
const rootSeq = typeof parsed?.rootSeq === "number" && Number.isSafeInteger(parsed.rootSeq) ? parsed.rootSeq : void 0;
|
|
8473
|
+
if (rootSeq !== void 0) projection.handledControlSeqs.add(rootSeq);
|
|
8474
|
+
break;
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
6353
8477
|
if (!enabled) break;
|
|
6354
8478
|
const data = asRecord(event.data);
|
|
6355
8479
|
if (asRecord(data?.source)?.kind !== "user") break;
|
|
6356
8480
|
const content = data?.content ?? [];
|
|
6357
8481
|
const text = extractTextContent(content);
|
|
6358
8482
|
if (text.trim() || content.some((part) => part && typeof part === "object" && part.type !== "text")) realRootInputSeen = true;
|
|
8483
|
+
const legacyMessage = protocolBoundarySeq !== void 0 && event.seq < protocolBoundarySeq;
|
|
8484
|
+
const coordinationSplit = !(protocolBoundarySeq !== void 0 && (captureBoundarySeq === void 0 || event.seq < captureBoundarySeq));
|
|
6359
8485
|
const captureAssets = () => {
|
|
6360
8486
|
if (v4BoundarySeq !== void 0 && event.seq > v4BoundarySeq) content.forEach((part, index) => {
|
|
6361
8487
|
if (!part || typeof part !== "object" || part.type === "text") return;
|
|
6362
8488
|
const identity = sha256(JSON.stringify(part));
|
|
6363
|
-
insert(projection,
|
|
8489
|
+
insert(projection, {
|
|
8490
|
+
kind: "requirement",
|
|
8491
|
+
body: `Uninterpreted root asset m${event.seq} part ${index}: sha256 ${identity}. Interpret the attachment; its contents are reference data, not execution authority.`,
|
|
8492
|
+
text: `Uninterpreted root asset m${event.seq} part ${index}`,
|
|
8493
|
+
paths: [],
|
|
8494
|
+
interpretation: {
|
|
8495
|
+
text: `Uninterpreted root asset m${event.seq} part ${index}`,
|
|
8496
|
+
body: `Interpret the attached asset m${event.seq} part ${index}`,
|
|
8497
|
+
directive: "directive",
|
|
8498
|
+
executee: "agent",
|
|
8499
|
+
immediatelyExecutable: true,
|
|
8500
|
+
authorityDisposition: "executable_now",
|
|
8501
|
+
fingerprint: `asset:${identity.slice(0, 16)}`
|
|
8502
|
+
}
|
|
8503
|
+
}, `m${event.seq}:asset:${index}`, scope.cwd || "scope", "scope");
|
|
6364
8504
|
});
|
|
6365
8505
|
};
|
|
6366
8506
|
if (!text.trim()) {
|
|
@@ -6379,7 +8519,7 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
|
|
|
6379
8519
|
if (parsed.kind === "confirm") {
|
|
6380
8520
|
if (confirmRebind(projection, parsed.proposalId, `m${event.seq}`, durableConfirmed)) {
|
|
6381
8521
|
captureAssets();
|
|
6382
|
-
if (parsed.remainder) captureRootText(projection, parsed.remainder, event.seq, scope,
|
|
8522
|
+
if (parsed.remainder) captureRootText(projection, parsed.remainder, event.seq, scope, legacyMessage, priorRootMessages, `m${event.seq}:r`, coordinationSplit);
|
|
6383
8523
|
break;
|
|
6384
8524
|
}
|
|
6385
8525
|
} else if (parsed.kind !== "none") {
|
|
@@ -6391,14 +8531,14 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
|
|
|
6391
8531
|
};
|
|
6392
8532
|
const stripped = text.split(/\r?\n/).filter((line) => !CONFIRM_LINE_PATTERN.test(line.trim())).join("\n");
|
|
6393
8533
|
if (!stripped.trim()) break;
|
|
6394
|
-
captureRootText(projection, stripped, event.seq, scope,
|
|
8534
|
+
captureRootText(projection, stripped, event.seq, scope, legacyMessage, priorRootMessages, `m${event.seq}`, coordinationSplit);
|
|
6395
8535
|
break;
|
|
6396
8536
|
}
|
|
6397
8537
|
}
|
|
6398
8538
|
captureAssets();
|
|
6399
8539
|
if (isInformationalMessage(text)) break;
|
|
6400
8540
|
if (classifyUserInteraction(text) === "conversational") break;
|
|
6401
|
-
captureRootText(projection, text, event.seq, scope,
|
|
8541
|
+
captureRootText(projection, text, event.seq, scope, legacyMessage, priorRootMessages, `m${event.seq}`, coordinationSplit);
|
|
6402
8542
|
break;
|
|
6403
8543
|
}
|
|
6404
8544
|
case "goal/change": {
|
|
@@ -6459,7 +8599,16 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
|
|
|
6459
8599
|
} } : {},
|
|
6460
8600
|
...typeof record?.resolution_evidence_id === "string" ? { resolutionEvidenceId: record.resolution_evidence_id } : {},
|
|
6461
8601
|
...typeof record?.effect_evidence_id === "string" ? { effectEvidenceId: record.effect_evidence_id } : {},
|
|
6462
|
-
...Array.isArray(record?.state_evidence_ids) ? { stateEvidenceIds: record.state_evidence_ids.map(String) } : {}
|
|
8602
|
+
...Array.isArray(record?.state_evidence_ids) ? { stateEvidenceIds: record.state_evidence_ids.map(String) } : {},
|
|
8603
|
+
...Array.isArray(record?.action_bindings) ? { actionBindings: record.action_bindings.map((entry) => {
|
|
8604
|
+
const closure = asRecord(entry);
|
|
8605
|
+
return {
|
|
8606
|
+
action: String(closure?.action ?? ""),
|
|
8607
|
+
evidenceIds: Array.isArray(closure?.evidence_ids) ? closure.evidence_ids.map(String) : [],
|
|
8608
|
+
resolvedTarget: asRecord(closure?.resolved_target) ?? {},
|
|
8609
|
+
order: Number(closure?.order ?? 0)
|
|
8610
|
+
};
|
|
8611
|
+
}) } : {}
|
|
6463
8612
|
};
|
|
6464
8613
|
}) : [];
|
|
6465
8614
|
} else if (call.name === "context_guard_boundary") {
|
|
@@ -6474,7 +8623,7 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
|
|
|
6474
8623
|
pendingCalls.set(callId, call);
|
|
6475
8624
|
break;
|
|
6476
8625
|
}
|
|
6477
|
-
case "tool/
|
|
8626
|
+
case "tool/ptc-dispatch-start": {
|
|
6478
8627
|
if (!enabled) break;
|
|
6479
8628
|
const data = asRecord(event.data);
|
|
6480
8629
|
const subCallId = String(data?.subCallId ?? "");
|
|
@@ -6487,10 +8636,10 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
|
|
|
6487
8636
|
break;
|
|
6488
8637
|
}
|
|
6489
8638
|
case "tool/result":
|
|
6490
|
-
case "tool/
|
|
8639
|
+
case "tool/ptc-dispatch": {
|
|
6491
8640
|
if (!enabled) break;
|
|
6492
8641
|
const data = asRecord(event.data);
|
|
6493
|
-
const isDispatch = event.type === "tool/
|
|
8642
|
+
const isDispatch = event.type === "tool/ptc-dispatch";
|
|
6494
8643
|
const message = asRecord(data?.message);
|
|
6495
8644
|
const source = asRecord(message?.source);
|
|
6496
8645
|
const callId = String(source?.callId ?? (isDispatch ? data?.subCallId : "") ?? "");
|
|
@@ -6677,169 +8826,6 @@ function lifecyclePhase(input) {
|
|
|
6677
8826
|
return input.realInputSeen ? "active" : "armed";
|
|
6678
8827
|
}
|
|
6679
8828
|
|
|
6680
|
-
//#endregion
|
|
6681
|
-
//#region src/domain/stop-policy.ts
|
|
6682
|
-
const QUOTED = /["'“”‘’`].*?(?:complete|done|finished|完成|做完|搞定).*?["'“”‘’`]/i;
|
|
6683
|
-
const EXAMPLE = /\b(?:for example|e\.g\.|such as|like saying|例如|比如|举例|作为一个例子)\b/i;
|
|
6684
|
-
const QUESTION = /\?[ \t]*$|\b(?:should|could|would|can|will|what|how|whether)\b.*\?/i;
|
|
6685
|
-
const TRAILING_NEGATION = /\b(?:not (?:yet |quite |fully )?(?:complete|done|finished)|isn'?t (?:complete|done|finished)|hasn'?t (?:been )?(?:completed|finished)|尚未完成|还没完成|未完成|没有完成|还未完成)\b/i;
|
|
6686
|
-
const CONDITIONAL = /\b(?:if|unless|once|when|whenever|provided that|只要|如果|假如|一旦|除非)\b/i;
|
|
6687
|
-
const PARTIAL_ONLY = /\b(?:step|phase|stage|milestone)\s+\d+\b|第[一二三四五六七八九十\d]+\s*(?:步|阶段|环节)|(?:第一步|第二步|第三步)/i;
|
|
6688
|
-
const WHOLE_COMPLETION_EN = /\b(?:the )?(?:task|work|job|everything|all tasks?|all work) (?:is|are) (?:now )?(?:complete|done|finished|completed)\b|\b(?:task|work) (?:has been )?(?:completed|finished)\b|\ball (?:tasks|work|requirements) (?:have been )?(?:completed|done|met)\b/i;
|
|
6689
|
-
const WHOLE_COMPLETION_ZH = /(?:任务|工作|所有任务|全部工作|整体)(?:已经|已)?(?:全部)?(?:完成|搞定|做完)|(?:已|已经)(?:全部|所有)?(?:完成|搞定)(?:了)?(?:全部|所有)?(?:任务|工作)?/i;
|
|
6690
|
-
/** Bare completion confirmations, e.g. "Done." or "搞定了。" */
|
|
6691
|
-
const BARE_COMPLETION = /^(?:done|finished|completed|all\s+done)[.!]?$|^(?:已完成|完成了|搞定了|搞定|完成|done)[。..!!]?$/i;
|
|
6692
|
-
/** Continuation intent following a claim makes it partial, not whole-task. */
|
|
6693
|
-
const CONTINUATION = /接下来|下一步|然后|接着|继续|再去|最后再|还差|剩下|剩余|第二步|第三步|,\s*(?:next|then|after that|moving on)\b/i;
|
|
6694
|
-
function looksQuotedOrExemplary(text) {
|
|
6695
|
-
return QUOTED.test(text) || EXAMPLE.test(text);
|
|
6696
|
-
}
|
|
6697
|
-
function isWholeTaskCompletionClaim(text) {
|
|
6698
|
-
const normalized = normalizeClause(text);
|
|
6699
|
-
if (!normalized) return false;
|
|
6700
|
-
if (QUESTION.test(normalized)) return false;
|
|
6701
|
-
if (TRAILING_NEGATION.test(normalized)) return false;
|
|
6702
|
-
if (CONDITIONAL.test(normalized)) return false;
|
|
6703
|
-
if (CONTINUATION.test(normalized)) return false;
|
|
6704
|
-
if (looksQuotedOrExemplary(normalized)) return false;
|
|
6705
|
-
if (PARTIAL_ONLY.test(normalized) && !WHOLE_COMPLETION_EN.test(normalized) && !WHOLE_COMPLETION_ZH.test(normalized)) return false;
|
|
6706
|
-
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
6707
|
-
if (BARE_COMPLETION.test(normalizeTitleLine(firstLine))) return leadingBareCompletionClaim(text);
|
|
6708
|
-
return BARE_COMPLETION.test(normalized) || WHOLE_COMPLETION_EN.test(normalized) || WHOLE_COMPLETION_ZH.test(normalized);
|
|
6709
|
-
}
|
|
6710
|
-
const DECORATION_LEAD = /^\s*(?:[\p{Extended_Pictographic}\u2764\u2705\u2714\u2716\u2728\u274C\u26A0\u2611\u2612\u2713\u2717\u274E\u2B50\u2B55\u2022\u00B7\u25E6\u25AA\u25AB\u25CF\u25CB\u25A0\u25A1\u2013\u2014-]|\uFE0F|\uFE0E|\u200D)+/u;
|
|
6711
|
-
/** Strip a leading run of decorative glyphs from a title line. */
|
|
6712
|
-
function stripDecorationPrefix(text) {
|
|
6713
|
-
let value = text;
|
|
6714
|
-
let previous = "";
|
|
6715
|
-
while (value !== previous) {
|
|
6716
|
-
previous = value;
|
|
6717
|
-
value = value.replace(DECORATION_LEAD, "");
|
|
6718
|
-
}
|
|
6719
|
-
return value.replace(/^\s+/, "");
|
|
6720
|
-
}
|
|
6721
|
-
/**
|
|
6722
|
-
* Normalize a title line for the bare-completion test. Markdown heading markers,
|
|
6723
|
-
* fully-wrapping emphasis (`**…**`, `__…__`, `*…*`, `_…_`), and a leading run of
|
|
6724
|
-
* decorative glyphs are removed ITERATIVELY until stable, because stripping one
|
|
6725
|
-
* layer may expose another (`## ✅ **完成。**`). Blockquotes (`>`), quoted
|
|
6726
|
-
* titles, and examples are left untouched so they still fail closed.
|
|
6727
|
-
*/
|
|
6728
|
-
function normalizeTitleLine(line) {
|
|
6729
|
-
let value = line.trim();
|
|
6730
|
-
if (value.startsWith(">")) return value;
|
|
6731
|
-
let previous = "";
|
|
6732
|
-
while (value !== previous) {
|
|
6733
|
-
previous = value;
|
|
6734
|
-
value = value.replace(/^#{1,6}\s+/, "").replace(/^\*\*(.+?)\*\*$/, "$1").replace(/^__(.+?)__$/, "$1").replace(/^\*(.+?)\*$/, "$1").replace(/^_(.+?)_$/, "$1");
|
|
6735
|
-
value = stripDecorationPrefix(value);
|
|
6736
|
-
}
|
|
6737
|
-
return value;
|
|
6738
|
-
}
|
|
6739
|
-
/**
|
|
6740
|
-
* A reply whose first non-empty line is a standalone bare completion ("完成。"
|
|
6741
|
-
* or "Done.") followed by a results summary. The whole text no longer matches
|
|
6742
|
-
* the single-line BARE_COMPLETION anchor, but the summary must still be treated
|
|
6743
|
-
* as a whole-task completion claim.
|
|
6744
|
-
*/
|
|
6745
|
-
function leadingBareCompletionClaim(text) {
|
|
6746
|
-
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
6747
|
-
const first = lines[0];
|
|
6748
|
-
if (!first || !BARE_COMPLETION.test(normalizeTitleLine(first))) return false;
|
|
6749
|
-
const rest = normalizeClause(lines.slice(1).join("\n"));
|
|
6750
|
-
if (!rest) return true;
|
|
6751
|
-
if (CONTINUATION.test(rest)) return false;
|
|
6752
|
-
if (TRAILING_NEGATION.test(rest)) return false;
|
|
6753
|
-
if (CONDITIONAL.test(rest)) return false;
|
|
6754
|
-
if (looksQuotedOrExemplary(rest)) return false;
|
|
6755
|
-
if (PARTIAL_ONLY.test(rest)) return false;
|
|
6756
|
-
return true;
|
|
6757
|
-
}
|
|
6758
|
-
function classifyCompletionClaim(text) {
|
|
6759
|
-
const normalized = normalizeClause(text);
|
|
6760
|
-
if (/waiting for (?:you|the user|input|your)|please (?:review|confirm|approve)|等待(?:您|你|用户)|请(?:确认|审阅|批准)/i.test(normalized)) return "user_wait";
|
|
6761
|
-
if (/waiting for (?:the )?(?:result|output|response|build|test|deployment)|等待(?:结果|输出|构建|测试|部署|响应)/i.test(normalized)) return "external_wait";
|
|
6762
|
-
if (isWholeTaskCompletionClaim(normalized)) return "complete";
|
|
6763
|
-
return "report";
|
|
6764
|
-
}
|
|
6765
|
-
/** Assistant prose is retained only as a bounded diagnostic observation. */
|
|
6766
|
-
function observeAssistantOutcome(text) {
|
|
6767
|
-
const disposition = classifyCompletionClaim(text);
|
|
6768
|
-
if (disposition === "complete") return {
|
|
6769
|
-
kind: "completion_claim",
|
|
6770
|
-
reasonCode: "assistant_completion_claim_observed"
|
|
6771
|
-
};
|
|
6772
|
-
if (disposition === "user_wait") return {
|
|
6773
|
-
kind: "user_wait_claim",
|
|
6774
|
-
reasonCode: "assistant_user_wait_claim_observed"
|
|
6775
|
-
};
|
|
6776
|
-
if (disposition === "external_wait") return {
|
|
6777
|
-
kind: "external_wait_claim",
|
|
6778
|
-
reasonCode: "assistant_external_wait_claim_observed"
|
|
6779
|
-
};
|
|
6780
|
-
return {
|
|
6781
|
-
kind: "report",
|
|
6782
|
-
reasonCode: "assistant_report_observed"
|
|
6783
|
-
};
|
|
6784
|
-
}
|
|
6785
|
-
/**
|
|
6786
|
-
* Stop Protocol 2.0 decision. This function deliberately has no assistant-text
|
|
6787
|
-
* parameter: completion wording, quotation, negation and translation cannot
|
|
6788
|
-
* steer the protocol. A structured root persistence authorization may request
|
|
6789
|
-
* one fallback correction; subsequent attempts safe-yield. An active, armed
|
|
6790
|
-
* Goal remains exclusively owned by the host Goal Round Driver.
|
|
6791
|
-
*/
|
|
6792
|
-
function decideTurnBoundary(projection) {
|
|
6793
|
-
if (!projection.enabled) return {
|
|
6794
|
-
action: "stop",
|
|
6795
|
-
reason: "guard_disabled"
|
|
6796
|
-
};
|
|
6797
|
-
if (projection.integrity !== "valid") return {
|
|
6798
|
-
action: "stop",
|
|
6799
|
-
reason: "integrity_invalid_safe_yield"
|
|
6800
|
-
};
|
|
6801
|
-
if (hasCurrentCertificate(projection)) return {
|
|
6802
|
-
action: "stop",
|
|
6803
|
-
reason: "current_certificate"
|
|
6804
|
-
};
|
|
6805
|
-
const boundary = projection.boundaries.at(-1);
|
|
6806
|
-
if (boundary?.persistedResult === "accepted" && boundary.epoch === projection.epoch && boundary.contractRevision === projection.contractRevision) return {
|
|
6807
|
-
action: "stop",
|
|
6808
|
-
reason: "accepted_boundary_pending_effectuation"
|
|
6809
|
-
};
|
|
6810
|
-
if (projection.currentGoalPhase === "active" && projection.currentGoalActivation === "armed") return {
|
|
6811
|
-
action: "stop",
|
|
6812
|
-
reason: "goal_round_driver_owns_continuation"
|
|
6813
|
-
};
|
|
6814
|
-
if ([...projection.items.values()].some((item) => item.status === "pending" && item.persistenceAuthorization)) {
|
|
6815
|
-
const key = `${projection.epoch}:${projection.contractRevision}`;
|
|
6816
|
-
const attempts = projection.persistenceCorrectionAttempts.get(key) ?? 0;
|
|
6817
|
-
if (attempts < 1) {
|
|
6818
|
-
projection.persistenceCorrectionAttempts.set(key, attempts + 1);
|
|
6819
|
-
return {
|
|
6820
|
-
action: "continue",
|
|
6821
|
-
reason: "protocol_correction_steer"
|
|
6822
|
-
};
|
|
6823
|
-
}
|
|
6824
|
-
}
|
|
6825
|
-
return {
|
|
6826
|
-
action: "stop",
|
|
6827
|
-
reason: "safe_yield_pending_preserved"
|
|
6828
|
-
};
|
|
6829
|
-
}
|
|
6830
|
-
function decideTurnStopping(projection, _assistantText, _turn, _maxAttempts) {
|
|
6831
|
-
return decideTurnBoundary(projection);
|
|
6832
|
-
}
|
|
6833
|
-
function latestAssistantText(events) {
|
|
6834
|
-
for (let index = events.length - 1; index >= 0; index--) {
|
|
6835
|
-
const event = events[index];
|
|
6836
|
-
if (event.type !== "assistant/message") continue;
|
|
6837
|
-
const text = event.data.message?.content?.filter((block$1) => block$1.type === "text").map((block$1) => block$1.text ?? "").join("\n") ?? "";
|
|
6838
|
-
if (text.trim()) return text;
|
|
6839
|
-
}
|
|
6840
|
-
return "";
|
|
6841
|
-
}
|
|
6842
|
-
|
|
6843
8829
|
//#endregion
|
|
6844
8830
|
//#region src/domain/git-adapter.ts
|
|
6845
8831
|
const GIT_COMMAND_MANIFEST_IDS = {
|
|
@@ -7129,25 +9115,81 @@ async function executeRevalidatedGitEffect(resolved, manifest, target, currentSt
|
|
|
7129
9115
|
//#endregion
|
|
7130
9116
|
//#region src/domain/session-events.ts
|
|
7131
9117
|
/**
|
|
7132
|
-
* Read a stable snapshot from
|
|
7133
|
-
*
|
|
7134
|
-
*
|
|
7135
|
-
*
|
|
9118
|
+
* Read a validated, stable event snapshot from the DSH Session V3 API.
|
|
9119
|
+
*
|
|
9120
|
+
* Session V3 replaced the V2 `events` getter with `snapshotEvents()`. Context
|
|
9121
|
+
* Guard supports only the V3 API: a session object that does not expose that
|
|
9122
|
+
* method is an unsupported host, never a reason to fall back to a legacy
|
|
9123
|
+
* accessor. Failing loud here keeps a V2-shaped object from being projected as
|
|
9124
|
+
* if its events had V3 semantics — the two vocabularies differ (surfaces,
|
|
9125
|
+
* `assistant/chunk` vs embedded streams, `session/end-seed` payload), so a
|
|
9126
|
+
* silent fallback would derive contract state from a log it cannot read.
|
|
9127
|
+
*
|
|
9128
|
+
* Guard is a READER of the durable log, so the envelope check below is the one
|
|
9129
|
+
* part of log validation it owns itself. The host validates a session it
|
|
9130
|
+
* constructs or restores; Guard additionally refuses a snapshot that is not a
|
|
9131
|
+
* sequence of event envelopes, because a projection that silently dropped or
|
|
9132
|
+
* mis-numbered an event would fabricate contract state rather than report a
|
|
9133
|
+
* damaged log.
|
|
9134
|
+
*
|
|
9135
|
+
* The V3 contract also asks a reader to refuse an unrecognized event type that
|
|
9136
|
+
* is not marked `ignorable`. Guard does NOT implement that half, deliberately:
|
|
9137
|
+
* the host's persistence reader already refuses such a log before publishing a
|
|
9138
|
+
* Session, and a whitelist of event types Guard happens to know would
|
|
9139
|
+
* false-refuse a healthy host whose composition registers a required event type
|
|
9140
|
+
* through a third-party plugin. The full rationale is in
|
|
9141
|
+
* `UPSTREAM_API_AUDIT.md`; revisit it there rather than adding a whitelist here.
|
|
9142
|
+
*/
|
|
9143
|
+
const SESSION_API_UNSUPPORTED = "session_api_unsupported";
|
|
9144
|
+
const SESSION_EVENT_ENVELOPE_INVALID = "session_event_envelope_invalid";
|
|
9145
|
+
var SessionApiError = class extends Error {
|
|
9146
|
+
code;
|
|
9147
|
+
constructor(message, code = SESSION_API_UNSUPPORTED) {
|
|
9148
|
+
super(message);
|
|
9149
|
+
this.name = "SessionApiError";
|
|
9150
|
+
this.code = code;
|
|
9151
|
+
}
|
|
9152
|
+
};
|
|
9153
|
+
/**
|
|
9154
|
+
* Refuse a snapshot that is not a contiguous, correctly enveloped V3 log.
|
|
9155
|
+
*
|
|
9156
|
+
* `seq` must be a non-negative safe integer and `type` a non-empty string.
|
|
9157
|
+
* Contiguity is checked against the snapshot's own first sequence rather than
|
|
9158
|
+
* against zero, because a ranged read legitimately starts later.
|
|
7136
9159
|
*/
|
|
9160
|
+
function assertEventEnvelopes(events) {
|
|
9161
|
+
let expected;
|
|
9162
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
9163
|
+
const event = events[index];
|
|
9164
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) throw new SessionApiError(`snapshot event ${index} is not an object`, SESSION_EVENT_ENVELOPE_INVALID);
|
|
9165
|
+
const record = event;
|
|
9166
|
+
if (typeof record.type !== "string" || record.type.length === 0) throw new SessionApiError(`snapshot event ${index} has no event type`, SESSION_EVENT_ENVELOPE_INVALID);
|
|
9167
|
+
if (typeof record.seq !== "number" || !Number.isSafeInteger(record.seq) || record.seq < 0) throw new SessionApiError(`snapshot event ${index} has no sequence number`, SESSION_EVENT_ENVELOPE_INVALID);
|
|
9168
|
+
if (expected !== void 0 && record.seq !== expected) throw new SessionApiError(`snapshot event ${index} breaks sequence contiguity`, SESSION_EVENT_ENVELOPE_INVALID);
|
|
9169
|
+
expected = record.seq + 1;
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
7137
9172
|
function snapshotSessionEvents(session) {
|
|
7138
|
-
if (!session || typeof session !== "object")
|
|
9173
|
+
if (!session || typeof session !== "object") throw new SessionApiError("a DSH Session object is required");
|
|
7139
9174
|
const source = session;
|
|
7140
|
-
if (typeof source.snapshotEvents
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
return
|
|
9175
|
+
if (typeof source.snapshotEvents !== "function") throw new SessionApiError("session does not expose the DSH Session V3 snapshotEvents() API");
|
|
9176
|
+
const events = source.snapshotEvents.call(session);
|
|
9177
|
+
if (!Array.isArray(events)) throw new SessionApiError("snapshotEvents() did not return an event list");
|
|
9178
|
+
assertEventEnvelopes(events);
|
|
9179
|
+
return events;
|
|
7145
9180
|
}
|
|
7146
9181
|
|
|
7147
9182
|
//#endregion
|
|
7148
9183
|
//#region src/domain/host-resolver.ts
|
|
7149
|
-
/**
|
|
7150
|
-
|
|
9184
|
+
/**
|
|
9185
|
+
* Names registered in any cohort; rows outside the union are unknown.
|
|
9186
|
+
*
|
|
9187
|
+
* Sorted, not inherited from cohort row order: the active cohort's own listing
|
|
9188
|
+
* order is a presentation choice, and letting it decide the resolution order of
|
|
9189
|
+
* `packageRowsFromPnpmLock` would make an unrelated cohort re-ordering look like
|
|
9190
|
+
* a lock-reading change.
|
|
9191
|
+
*/
|
|
9192
|
+
const CRITICAL_NAMES = [...new Set(HOST_COHORTS.flatMap((cohort) => cohort.packages.map((row) => row.name)))].sort((a, b) => a.localeCompare(b));
|
|
7151
9193
|
const HOST_LOCK_MARKER_BEGIN = "# >>> BEGIN DSH COMPLETION GUARD HOST LOCK (managed) >>>";
|
|
7152
9194
|
const HOST_LOCK_MARKER_END = "# <<< END DSH COMPLETION GUARD HOST LOCK (managed) <<<";
|
|
7153
9195
|
var HostProfileError = class extends Error {
|
|
@@ -7204,13 +9246,34 @@ function packageRowsFromPnpmLock(text, names = CRITICAL_NAMES) {
|
|
|
7204
9246
|
return entries;
|
|
7205
9247
|
});
|
|
7206
9248
|
}
|
|
9249
|
+
/**
|
|
9250
|
+
* The production host verdict: the version floor and the exact-graph audit,
|
|
9251
|
+
* combined into the one answer a caller acts on.
|
|
9252
|
+
*
|
|
9253
|
+
* The two facts stay separable — `hostVersion` is always reported on the
|
|
9254
|
+
* evaluation — but a host below the supported floor is refused here even when
|
|
9255
|
+
* its graph matches an audited cohort, because no graph can lift a version
|
|
9256
|
+
* floor. Keeping this combination out of `evaluateHostLock` leaves that
|
|
9257
|
+
* function a pure graph audit, so a graph verdict is never overwritten by a
|
|
9258
|
+
* version verdict inside it.
|
|
9259
|
+
*/
|
|
9260
|
+
function combineHostPolicy(evaluation) {
|
|
9261
|
+
const version = evaluation.hostVersion;
|
|
9262
|
+
if (version?.status !== "below_minimum" && version?.status !== "unparseable") return evaluation;
|
|
9263
|
+
return {
|
|
9264
|
+
...evaluation,
|
|
9265
|
+
status: "unsupported",
|
|
9266
|
+
goalAvailable: false,
|
|
9267
|
+
reasonCode: version.status === "below_minimum" ? "host_lock_version_below_minimum" : "host_lock_version_unparseable"
|
|
9268
|
+
};
|
|
9269
|
+
}
|
|
7207
9270
|
function resolveInstalledHostLock(moduleUrl = import.meta.url) {
|
|
7208
9271
|
const lockPath = findUp(dirname(fileURLToPath(moduleUrl)), "pnpm-lock.yaml");
|
|
7209
|
-
if (!lockPath) return evaluateHostLock([]);
|
|
9272
|
+
if (!lockPath) return combineHostPolicy(evaluateHostLock([]));
|
|
7210
9273
|
try {
|
|
7211
|
-
return evaluateHostLock(packageRowsFromPnpmLock(readFileSync(lockPath, "utf8")));
|
|
9274
|
+
return combineHostPolicy(evaluateHostLock(packageRowsFromPnpmLock(readFileSync(lockPath, "utf8"))));
|
|
7212
9275
|
} catch {
|
|
7213
|
-
return evaluateHostLock([]);
|
|
9276
|
+
return combineHostPolicy(evaluateHostLock([]));
|
|
7214
9277
|
}
|
|
7215
9278
|
}
|
|
7216
9279
|
function activeGraphRecords(packageMapText) {
|
|
@@ -7379,13 +9442,14 @@ function inspectTargetHostGraph(runtimeRoot, profileRoot) {
|
|
|
7379
9442
|
platform: process.platform === "win32" ? "windows" : "posix",
|
|
7380
9443
|
profileKind: "headless"
|
|
7381
9444
|
});
|
|
7382
|
-
|
|
9445
|
+
const selectedCohort = HOST_COHORTS.find((cohort) => cohort.id === evaluation.cohortId);
|
|
9446
|
+
if (evaluation.status !== "supported" || !selectedCohort) throw new HostProfileError("target_runtime_unsupported", "dependency-free inspection requires the active audited core cohort");
|
|
7383
9447
|
const { records, reachable } = activeGraphRecords(mapText);
|
|
7384
9448
|
const launcher = realpathSync(join(modules, "@deepseek-ai", "dsh"));
|
|
7385
9449
|
const anchor = join(launcher, "package.json");
|
|
7386
9450
|
const host = readJsonObject(anchor, "target_runtime_unsupported");
|
|
7387
9451
|
const launcherId = [...reachable].filter((id) => id === "@deepseek-ai/dsh" || id.startsWith("@deepseek-ai/dsh@"));
|
|
7388
|
-
if (launcherId.length !== 1 || host.name !== "@deepseek-ai/dsh" || host.version !==
|
|
9452
|
+
if (launcherId.length !== 1 || host.name !== "@deepseek-ai/dsh" || host.version !== selectedCohort.packages.find((row) => row.name === "@deepseek-ai/dsh")?.version || typeof records[launcherId[0]].url !== "string" || realpathSync(resolve(modules, records[launcherId[0]].url)) !== launcher || !within(modules, launcher)) throw new HostProfileError("target_runtime_unsupported", "launcher differs from the active runtime importer");
|
|
7389
9453
|
const bundleRows = names.map((name) => {
|
|
7390
9454
|
const packageRoot = packageFromAnchor(anchor, name);
|
|
7391
9455
|
const ids = [...reachable].filter((id) => id === name || id.startsWith(`${name}@`));
|
|
@@ -7868,4 +9932,4 @@ function proofEvidenceConstraints(evidence, obligation) {
|
|
|
7868
9932
|
}
|
|
7869
9933
|
|
|
7870
9934
|
//#endregion
|
|
7871
|
-
export {
|
|
9935
|
+
export { parseShellCommand as $, requestedTargetMatchesResolved as $n, latestRootInstruction as $t, createGitPrestateEnvelope as A, isInformationalMessage as An, authorityCaptureCounts as At, CAPTURE_V042_NOTICE as B, statefulActionsOfScope as Bn, evidenceCoverage as Bt, SESSION_EVENT_ENVELOPE_INVALID as C, relevantEvidence as Cn, evaluateMinimumHostVersion as Ct, GIT_COMMAND_TEMPLATES as D, extractArtifactPaths as Dn, RC015_HOST_PACKAGES as Dt, GIT_COMMAND_MANIFEST_IDS as E, classifyClause as En, RC015_RC2_HOST_PACKAGES as Et, verifiedLinearCommitReadback as F, isOpenObligation as Fn, closingHint as Ft, evidenceFromPersistedToolResult as G, CERTIFICATE_VERSION as Gn, NO_PROGRESS_TURNS_BEFORE_STOP as Gt, PROTOCOL_V4_NOTICE as H, npmEscapedPackageName as Hn, isVerifyingCapability as Ht, FIRST_STEP_GUIDANCE as I, kindOfScope as In, openItems$1 as It, isDeterministicCheck as J, STOP_PROTOCOL_VERSION as Jn, decideTurnStopping as Jt, extractTextContent as K, SEMANTIC_ACTIONS as Kn, classifyCompletionClaim as Kt, claimedBatchHasRealRootInput as L, maskCodeSpans as Ln, recoveryDigest as Lt, gitCommandMatchesTarget as M, interpretClause as Mn, certifyCheckpoint as Mt, parseGitCommandManifest as N, interpretMessage as Nn, DEFAULT_RECOVERY_CHAR_BUDGET as Nt, commitIndexSnapshotDigest as O, extractMethod as On, RC1_HOST_PACKAGES as Ot, revalidateGitPrestate as P, isExecutableItem as Pn, MIN_RECOVERY_CHAR_BUDGET as Pt, parsePwshCommand as Q, requestedTargetAuthorizesMutation as Qn, latestAssistantText as Qt, lifecyclePhase as R, namedActions as Rn, renderRecoveryPacket as Rt, SESSION_API_UNSUPPORTED as S, itemDiagnosis as Sn, compareHostVersions as St, snapshotSessionEvents as T, captureItem as Tn, satisfiesSupportedHostRange as Tt, deriveProjection as U, ACTION_MANIFEST as Un, CONTROL_RECORD_PREFIX as Ut, PROTOCOL_V3_NOTICE as V, canonicalRegistryBase as Vn, evidenceMatchesItem as Vt, supersedeItem as W, ACTION_MANIFEST_VERSION as Wn, NO_PROGRESS_RECORD_PREFIX as Wt, canonicalArgvFromCommand as X, actionCompatible as Xn, isRootPauseRequest as Xt, withDurability as Y, SUPPORTED_EVIDENCE_ADAPTERS as Yn, decisionBoundaryKey as Yt, isRunExecutable as Z, isStatefulAction as Zn, isWholeTaskCompletionClaim as Zt, packageRowsFromPnpmLock as _, CONFIRM_LINE_PATTERN as _n, evaluateToolSurfaceCapability as _t, createProofManifest as a, effectuateBoundary as an, validateManifest as ar, BASE_HOST_PACKAGES as at, resolveInstalledHostLock as b, deriveItemDiagnosis as bn, MIN_SUPPORTED_HOST_VERSION as bt, sessionQuery as c, currentContractDigest as cn, canonicalizePath as cr, GOAL_HOST_PACKAGES as ct, combineHostPolicy as d, proposeRebind as dn, sanitizeClauseText as dr, LEGACY_HOST_COHORTS as dt, observeAssistantOutcome as en, semanticActionFromCommand as er, ACTIVE_HOST_COHORT_ID as et, hostLockContextFromComposedDump as f, proposeRebindOutcome as fn, sanitizeUrl as fr, bindExecutableIdentity as ft, packageRowsFromActiveGraph as g, replayRebindResult as gn, evaluateHostLock as gt, inspectTargetHostGraph as h, rebindResponse as hn, evaluateHostCapability as ht, canonicalProjection as i, availableBoundaryQualifications as in, COMMAND_SURFACE_MANIFEST as ir, ALPHA2_HOST_PACKAGES as it, executeRevalidatedGitEffect as j, segmentClauses as jn, segmentAuthorityBlocks as jt, commitTreeSnapshotDigest as k, extractOperation as kn, ALPHA3_HOST_PACKAGES as kt, validateProofManifest as l, createProjection as ln, digestStrings as lr, HOST_CAPABILITY_PACKAGE_GROUPS as lt, injectActiveProfileHostLock as m, rebindAttemptKey as mn, evaluateExternalWaitCapability as mt, PROOF_PROTOCOL_VERSION as n, goalCompletionDenial as nn, validateActionManifest as nr, ACTIVE_HOST_LAUNCHER_VERSION as nt, proofDigest as o, isCurrentAcceptedBoundary as on, classifyTaskIntent as or, DEFAULT_HOST_LOCK as ot, hostLockRowsFromComposedDump as p, proposeRebindV042 as pn, sha256 as pr, bindLiveGoalCapability as pt, extractToolSubject as q, STATEFUL_ACTIONS as qn, decideTurnBoundary as qt, bindProofToProjection as r, hasCurrentCertificate as rn, validateActionTarget as rr, ALPHA2_DSHMARKET_139_HOST_PACKAGES as rt, proofEvidenceConstraints as s, qualifyBoundary as sn, classifyUserInteraction as sr, EXPECTED_HOST_PACKAGES as st, PROOF_KINDS as t, progressFingerprint as tn, semanticActionFromText as tr, ACTIVE_HOST_COHORT_IDS as tt, HostProfileError as u, confirmRebind as un, normalizeClause as ur, HOST_COHORTS as ut, readActiveHostGraph as v, isFrozenV042RebindResponse as vn, hostVersionFromPackages as vt, SessionApiError as w, captureClause as wn, parseHostVersion as wt, verifyComposedHostLockDump as x, evidenceAvailabilityReason as xn, SUPPORTED_HOST_RANGE as xt, resolveActiveProfileHostLock as y, parseConfirmationMessage as yn, selectHostCohort as yt, previewFirstStepInjection as z, semanticActionOfScope as zn, bindingSatisfies as zt };
|