@entrorove/skill 0.1.69 → 0.1.71
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 +10 -0
- package/IDE-EXECUTION.md +5 -3
- package/SKILL.md +3 -3
- package/cloud-locale.d.mts +7 -0
- package/cloud-locale.mjs +59 -0
- package/cloud-task-client.mjs +21 -13
- package/cloud-watch-client.mjs +11 -3
- package/design-lint.mjs +20 -2
- package/ide-presentation.mjs +15 -4
- package/local-archguard-components.mjs +31 -4
- package/local-budget-reads.mjs +7 -2
- package/local-budget.mjs +3 -2
- package/local-delegated-modify-analysis.mjs +16 -7
- package/local-scope-build-data.mjs +73 -0
- package/local-scope-build-facts.mjs +33 -0
- package/local-scope-build-io.mjs +57 -0
- package/local-scope-build-read.mjs +22 -0
- package/local-scope-build-readset.mjs +49 -0
- package/local-scope-files.mjs +33 -8
- package/local-scope-read-set.mjs +66 -0
- package/local-scope-repository.mjs +96 -28
- package/local-scope-signature.mjs +50 -5
- package/local-scope-state.mjs +7 -5
- package/local-scope-uni-build.mjs +85 -0
- package/local-scope-uni-conditional.mjs +56 -0
- package/local-scope-uni-directives.mjs +96 -0
- package/local-scope-uni-expressions.mjs +106 -0
- package/local-scope-workspaces.mjs +17 -5
- package/manifest.json +1 -1
- package/package.json +16 -2
- package/release-source-groups.mjs +39 -0
- package/release-state.json +36 -22
- package/release.mjs +6 -41
- package/releases.json +12 -0
- package/scope-context-client.mjs +11 -27
- package/scope-context-journal.mjs +44 -0
- package/scope-context-observer.mjs +37 -11
- package/scope-context-release.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.71 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
- 通用分离源码依赖事实与未知构建行为:动态配置、宏及加载器保留真实哈希与只读来源,不执行项目代码,不把构建未知标为完整。原审批与独立 S/M 签名仅允许已知源码的非结构修改;隐私文件、路径、资源预算、未知目标及配置漂移继续明确拒绝,实际构建与测试仍需原流程验收。
|
|
6
|
+
- Separate raw source dependency facts from opaque build behavior across projects. Preserve actual hashes and read-only origins for dynamic configuration, macros and loaders without executing project code or claiming compiled completeness. Original approvals and separate S/M signatures permit only known-source nonstructural modifications; private files, unsafe paths, resource budgets, unknown targets and configuration drift remain explicit refusals. Actual builds and tests still require original-flow acceptance.
|
|
7
|
+
|
|
8
|
+
## 0.1.70 — 2026-10-20
|
|
9
|
+
|
|
10
|
+
- 云端沙箱固定中英文输出语言,界面、通知和 Blueprint 文档正文按对应语种显示,原始证据单列来源并保留旧流程精确版本。修复 uni-app 默认条件编译分支解析与长扫描后的原任务签名挑战衔接,核对真实读集并保留原审批、完整路径、快照及失败历史。
|
|
11
|
+
- Freeze cloud sandbox output language across presentation, notifications and Blueprint document prose; preserve original source evidence and exact historical runtimes. Fix default uni-app conditional branch inspection and renew the original signing challenge after long scans, rechecking actual reads while retaining approvals, complete paths, snapshots and failure history.
|
|
12
|
+
|
|
3
13
|
## 0.1.69 — 2026-10-20
|
|
4
14
|
|
|
5
15
|
- 新增首次范围门禁前的构建上下文纠正:原账号审核真实配置和构建证据,原 S 签名完整路径快照,通过服务挑战追加上下文权威记录;保留原 flow、绑定、旧上下文和失败历史,不授予写入或完成权限。
|
package/IDE-EXECUTION.md
CHANGED
|
@@ -33,9 +33,9 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
|
|
|
33
33
|
|
|
34
34
|
### IDE display and cloud planning entry
|
|
35
35
|
|
|
36
|
-
For a new engineering task, run `npx --yes --prefer-online @entrorove/skill@latest invoke` with `operation: "start"` and the actual task facts below. The CLI reads the authenticated account's sandbox preference before dispatch. An enabled account returns `status: "needs-input"` and a cloud choice, with no model call yet. This is a routing question, not an execution failure. Render it immediately with the host's question tool or native UI; if unavailable, display `interaction.chatFallback` and collect the answer. A collapsed command log is insufficient.
|
|
36
|
+
For a new engineering task, run `npx --yes --prefer-online @entrorove/skill@latest invoke` with `operation: "start"` and the actual task facts below. The CLI reads the authenticated account's sandbox preference before dispatch. An enabled account returns `status: "needs-input"` and a cloud choice, with no model call yet. This is a routing question, not an execution failure. Render it immediately with the host's question tool or native UI; if unavailable, display `interaction.chatFallback` and collect the answer. A collapsed command log is insufficient. Use the returned locale-specific labels for **Use cloud sandbox** and **Continue locally**. After confirmation call the returned `cloud-task` callback with its exact original payload and `sandboxChoice: "use"`; exit repeats the original `start` with top-level `sandboxChoice: "local"`. Preserve the original goal, risk, work type and requirements. A disabled account continues through its configured local workflow; unavailable preferences stop entry explicitly. Reuse a prior explicit choice while resuming this task. Silence never authorizes cloud consumption.
|
|
37
37
|
|
|
38
|
-
For an explicit cloud planning request without an existing start, `cloud-task` accepts `{"goal":"<actual goal>","ideTaskId":"<current IDE task ID, when available>"}` on stdin and returns the same choice. Do not start both routes for one task.
|
|
38
|
+
For an explicit cloud planning request without an existing start, `cloud-task` accepts `{"goal":"<actual goal>","ideTaskId":"<current IDE task ID, when available>","outputLocale":"en"}` on stdin and returns the same choice. Use `zh` for Chinese output. The creating page or explicit IDE request freezes this language; keep the original goal unchanged. A later viewer language switch changes presentation only. Cross-language historical output and raw evidence remain clearly labelled source details. New localized Blueprint documents use the frozen language for their prose and preserve original structured values in closed source appendices. Do not start both routes for one task.
|
|
39
39
|
|
|
40
40
|
Images attached to a cloud planning task use `images: [{"path":"<explicit absolute image path>","detail":"original"}]` or `images: [{"url":"data:image/png;base64,<canonical bytes>","detail":"auto"}]`. For ordinary `invoke` task entry, keep `images` at the envelope's top level, outside the immutable `input.task`; the original cloud-choice callback carries them forward. Render the returned choice before sending. File paths are read only after `use`, and a local choice retains the attachment references without reading or uploading them. Never scan for images, copy a local path into a remote URL, omit attachments silently, or describe a text-only classification as image analysis.
|
|
41
41
|
|
|
@@ -100,7 +100,7 @@ First inspect the actual host configurations and the real build source that copi
|
|
|
100
100
|
|
|
101
101
|
The SDK signs the exact proposal with the original S key. The service checks the original accepted owner, task, repository, signer, complete target set and current context revision, then issues an original-owner Confirm Protocol question showing the exact old/new mappings and observed evidence hashes. Show that unchanged question using the host question tool. Only the user's actual answer may call its original `interaction-answer` callback. Existing task authorization does not fabricate this exact context approval; silence remains pending. These configuration and build-evidence files remain read-only and hash-bound during later scope refreshes and file-role amendments; this approval cannot authorize changing their alias semantics or source bytes.
|
|
102
102
|
|
|
103
|
-
After approval, run `scope-context` with `{ "action": "apply", "flowId": "<original flow>", "proposalId": "<returned proposal>", "proposalDigest": "<returned digest>" }`. It
|
|
103
|
+
After approval, run `scope-context` with `{ "action": "apply", "flowId": "<original flow>", "proposalId": "<returned proposal>", "proposalDigest": "<returned digest>" }`. It prepares and independently inspects a real copied snapshot and import graph using all original target paths and the proposed contexts, then obtains a fresh server challenge before the original S signature. The short challenge lifetime remains unchanged. Its actual read set, source hashes, original missing resolver candidates, build evidence and snapshot are rechecked after that request; drift rejects signing. A local parser failure is archived, and retrying the identical approved input reuses its original snapshot only while the actual files still match. Unknown remote outcomes require the original GET recovery before any new challenge. Configuration or build-evidence drift, an unresolved actual alias, stale revision, expired/consumed nonce, foreign identity or missing approval rejects the attempt. No context approval grants writes or completes the task.
|
|
104
104
|
|
|
105
105
|
Successful application appends a server-signed context authority and local history while preserving the original binding bytes. Retry `scope-connect` with its returned complete `nextInput`, then resume the original gate request. Later ordinary scope refreshes and `scope-amend` use that effective context authority; their signed facts bind its digest and exact mappings. M snapshot preparation and independent guarded writes recover the same context authority from the original private history and snapshot without a caller-selected mapping or S private-key read; their separate original M approval remains required. Status uses `{ "action": "status", "flowId": "<original flow>" }`. An unknown POST must recover its original request with GET through the saved journal; never repeat a POST to guess its outcome. This recovery is unavailable after the first accepted scope facts, successful gate, frozen plan or execution; preserve that later flow's original authority instead. The existing strict handoff protocol does not transfer a corrected context head; do not choose another receiver to bypass this binding.
|
|
106
106
|
|
|
@@ -211,3 +211,5 @@ Feedback command example (replace the request ID with the original successful in
|
|
|
211
211
|
Ordinary Windows scope inspection resolves an absolute Git for Windows `git.exe` from its installation or absolute PATH directories, including paths containing spaces. It never invokes `/usr/bin/git` on Windows, a shell wrapper or a Git executable inside the inspected repository. Missing Git gives `ENTROROVE_GIT_UNAVAILABLE` and requires installing Git for Windows before reconnecting the original scope.
|
|
212
212
|
|
|
213
213
|
The fixed Git byte-metering adapter ships hash-pinned precompiled libraries for Darwin and glibc Linux on arm64/x64. It extracts the selected library on first use; it never invokes a C compiler on the user machine. Darwin requires Apple’s trusted Git (Xcode or Command Line Tools), xcrun and codesign; Linux requires the trusted `/usr/bin/git` and glibc. Native comparisons with enforced source-read budgets remain restricted to these platforms. An unsupported native budget adapter returns `ENTROROVE_GIT_READER_UNSUPPORTED`; do not bypass its budget or claim an ordinary Windows inspection verifies that separate capability.
|
|
214
|
+
|
|
215
|
+
Conditional uni-app Vue sources are inspected across every supported feasible default platform and version branch, retaining raw source hashes and physical diagnostic locations. Unknown macros, unsupported expressions, custom build overrides or environment files that could change conditional semantics are rejected explicitly. A passing fixture does not prove that another project’s full scope passed; retry that project’s original approved operation and retain its actual result.
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Execute a task selected for Entrorove through explicit SDK operatio
|
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.71.
|
|
9
9
|
|
|
10
10
|
Entrorove applies only to the task for which it is selected; reading these instructions, discovering capabilities or installing the package never selects or governs another task. Hooks are optional. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
|
|
11
11
|
|
|
@@ -19,10 +19,10 @@ Entrorove applies only to the task for which it is selected; reading these instr
|
|
|
19
19
|
3. Discover `capabilities`, then invoke `start` with the actual goal, task type, risk and scope facts. An enabled account returns a cloud choice before charging a model. Show that choice visibly using the host question tool. Confirmation calls its original callback; exit adopts the local route for this task. Never treat silence as permission, invent acceptance, or create both routes. Existing explicit authorization for this task persists.
|
|
20
20
|
With cloud sandbox disabled, the IDE performs its own planning and continues the original API flow. Do not require a cloud model call or create a sandbox task to perform local work. The service freezes eligible member plans with `verificationMode: member-local` and `runnerKeyId: null`; successful original IDE execution and tests satisfy local acceptance. This mode comes from the stored account preference and enrolled scope, never a caller flag or an unavailable validator key. An already frozen independent plan keeps its original acceptance contract.
|
|
21
21
|
When the model produces a task contract, preserve its `workType`, `risk`, `difficulty`, `crossModule`, `parallel`, `unclear`, `architectureContract`, optional `baselineRef`, and optional `interaction` as returned. Valid work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision`, and `diagnosis`. A `merge` requires a concrete `baselineRef` identifying the branch, tag, commit, or review baseline; keep that baseline unchanged through independent verification. A `decision` requires `interaction`, including a risk description for medium or high risk. Do not fill missing required fields by guessing.
|
|
22
|
-
4. After real cloud acceptance, immediately display
|
|
22
|
+
4. After real cloud acceptance, immediately display the returned entering status in the task’s selected language, including its **taskNumber**. Preserve `taskNumber` verbatim: it is the homepage sandbox tab number. Keep its UUID `taskId` separately. Run the returned `cloud-watch`, show each real public event and its cursor, and continue the original accepted flow from status. A command log alone is insufficient. No accepted cloud task means no cloud ID or model-execution claim.
|
|
23
23
|
5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or erase the old failure. If a fresh scope proof returns `ENTROROVE_SCOPE_PLANNING_REASSESSMENT_REQUIRED` before plan freeze, follow its explicit `workflow-scope-reassess` recovery and retry the original complete request; a local reassessment or snapshot refresh alone does not renew the accepted gate. If the terminal flow truly owns a stale task slot, use the explicit `flow-abandon` lifecycle operation described below before routing the original task again.
|
|
24
24
|
6. All governed mutations use the original authority: `local guarded-write`, `local guarded-delete`, `local guarded-move`, or the original frozen approved runner plan. Native patches, scripts, terminal writes, database mutations and unknown MCP operations must not substitute for the controlled entry. User-authorized task messages use the host's message tool and remain subject to host approval; sending a report does not change the flow or grant execution authority. **A failed gate or unknown outcome must never be replaced with another executor or reported as local success.** Query the original request read-only, repair the cause, and obtain the original flow's valid authority before proceeding. If the original scope challenge preparation is absent from authenticated status, `scope-amend` may use the discovered `scope-challenge-retire` lifecycle operation. The server validates the same approved proposal and receiver, atomically fences only an unadmitted challenge, and records a signed retirement without executing that challenge. Recover and acknowledge the original retirement result with GET before a fresh challenge attempt; retain both requests and all failures. An admitted request, issued nonce, scope application or mutation has no missing-challenge retirement path. An uncertain retirement itself permits only its original GET.
|
|
25
|
-
For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass
|
|
25
|
+
For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass. Its non-structural modify role requires known target source imports, preserves source imports and public contracts, and keeps unresolved origins, other observed dependencies and project configuration read only. Build scripts, macros and generated sources can remain opaque; source observations and this editing role never prove a complete compiled dependency graph or successful tests. After actual writes, refresh the original scope and run `mutation-settle`. Use the returned `previousPassId` for further edits. In a signed member-local plan, `workflow-observe-step` runs the original frozen command and tests without a validator private key. A server-signed `localAcceptance` advances local dependencies; a failed test does not. A frozen independent plan retains its independent validation requirement. Preserve the raw observation and its absent independent receipt. See [IDE-EXECUTION.md](IDE-EXECUTION.md#local-ide-acceptance).
|
|
26
26
|
7. Read [REUSE-DISCIPLINE.md](REUSE-DISCIPLINE.md) when modifying an existing system. The comparison baseline is the existing implementation in that same repository. It never means another project, another account, or copying external systems.
|
|
27
27
|
Never overwrite a newer package with older bytes, relabel an old archive as new, or roll back because the current state is uncertain. Package identity/version checks and immutable historical byte observations run in the guarded entries. A refusal requires preserved inputs and diagnosis; a `force`, `rollback`, timestamp or filename is not evidence. Intentional rollback uses the original independently signed merge rollback authorization, transaction journal, exact current/restore hashes and fresh original mutation pass. Preserve old failures, receipts and unknown effects; a successful rollback never makes the failed operation successful.
|
|
28
28
|
**Before preparing or writing any UI implementation, read [DESIGN-GATES.md](DESIGN-GATES.md).** Record the whole-site flow first, fetch current mainstream layout references and the specified framework's official rules, create actual mockups or prototypes using the host image/design tool, and show them to the user. Respect their choice of visual format; a prototype is a valid approval baseline. Obtain the original persisted design confirmation before UI code preparation. Carry that exact design approval through the original scope, plan and mutation gates. Neither a caller flag nor a text-only description substitutes for this sequence. Finish real functional tests, actual browser interaction/layout/style checks and original user visual acceptance before delivery. Keep Entrorove unchanged in every language.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type CloudOutputLocale = 'zh' | 'en'
|
|
2
|
+
export function cloudOutputLocale(value: unknown): CloudOutputLocale | undefined
|
|
3
|
+
export function cloudMessage(key: string, locale?: CloudOutputLocale): string
|
|
4
|
+
export function cloudProseMatches(text: unknown, locale: CloudOutputLocale): boolean
|
|
5
|
+
export function cloudEventDisplay(value: { content: string; kind?: string; metadata?: Record<string, unknown> }, locale: CloudOutputLocale): {
|
|
6
|
+
content: string; summary?: string; sourceContent?: string; sourceSummary?: string; sourceLocale: CloudOutputLocale | 'unknown'
|
|
7
|
+
}
|
package/cloud-locale.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const copy = {
|
|
2
|
+
choice: ['是否启用云端沙箱执行思考规划?', 'Use the cloud sandbox to plan this task?'],
|
|
3
|
+
use: ['确认', 'Use cloud sandbox'], local: ['退出,采用本地方案', 'Continue locally'],
|
|
4
|
+
useHint: ['为当前任务创建云端思考规划并显示真实进度', 'Plan this task in the cloud and show actual progress'],
|
|
5
|
+
localHint: ['继续当前任务,不改变账号开关', 'Continue this task without changing the account preference'],
|
|
6
|
+
risk: ['这是当前任务的执行位置选择,不改变账号级云端沙箱开关。', 'This chooses the execution location for this task without changing the account preference.'],
|
|
7
|
+
fallback: ['是否启用云端沙箱执行思考规划?\n1. 确认:当前任务使用云端沙箱\n2. 退出:自动采用本地方案,继续当前任务',
|
|
8
|
+
'Use the cloud sandbox to plan this task?\n1. Use cloud sandbox for this task\n2. Continue this task locally'],
|
|
9
|
+
skipped: ['已退出云端沙箱,采用本地方案继续当前任务。', 'Continue this task locally without entering the cloud sandbox.'],
|
|
10
|
+
enter: ['正在进入云端沙箱', 'Entering the cloud sandbox'], accepted: ['已进入', 'Accepted'],
|
|
11
|
+
task: ['云端任务', 'Cloud task'], output: ['云端沙箱输出完毕;实际验证状态以任务回执为准。', 'Cloud sandbox output is available; verification remains determined by the actual task receipt.'],
|
|
12
|
+
exit: ['云端沙箱正在退出本次进度订阅', 'Closing this cloud progress subscription'],
|
|
13
|
+
waiting: ['云端沙箱仍在执行,下次从当前进度继续读取', 'Cloud execution continues; resume reading from the current progress cursor'],
|
|
14
|
+
source: ['原始输出(保留来源,未作翻译)', 'Original output (source preserved; not translated)'],
|
|
15
|
+
sourceDetails: ['原始详情(未翻译)', 'Original details (not translated)'],
|
|
16
|
+
event: ['云端进度已更新;原文保留在来源详情。', 'Cloud progress updated; the original text is preserved in source details.'],
|
|
17
|
+
failed: ['云端任务失败;原始诊断保留在来源详情。', 'Cloud task failed; the original diagnostic is preserved in source details.'],
|
|
18
|
+
keepAlive: ['智能执行中,请勿关闭!', 'Execution in progress. Please keep this window open.'],
|
|
19
|
+
}
|
|
20
|
+
const events = {
|
|
21
|
+
'Cloud task accepted.': ['任务已进入', 'Task accepted'],
|
|
22
|
+
'Cloud workflow started.': ['云端沙箱任务已分配', 'Cloud sandbox task allocated'],
|
|
23
|
+
'Cloud model classification started.': ['云端沙箱正在分析任务', 'Cloud sandbox is analyzing the task'],
|
|
24
|
+
'Task classification completed.': ['云端沙箱分析已返回', 'Cloud sandbox analysis returned'],
|
|
25
|
+
'Workflow completed with verified evidence.': ['任务已核验', 'Task verified'],
|
|
26
|
+
'Workflow is blocked.': ['流程已阻断', 'Workflow blocked'],
|
|
27
|
+
'Workflow is incomplete.': ['流程待补全', 'Workflow incomplete'],
|
|
28
|
+
'Workflow failed.': ['流程执行失败', 'Workflow failed'],
|
|
29
|
+
'Workflow reported its result; verification is pending.': ['结果已返回,仍待实际核验', 'Result reported; actual verification remains pending'],
|
|
30
|
+
'Workflow is waiting for an explicit decision.': ['等待你明确确认', 'Awaiting your explicit decision'],
|
|
31
|
+
'Original cloud task stopped.': ['原云端任务已停止', 'Original cloud task stopped'],
|
|
32
|
+
'Model returned prose outside the frozen cloud output language': ['模型未按本任务固定语种返回说明', 'Model prose did not match the frozen task output language'],
|
|
33
|
+
}
|
|
34
|
+
export function cloudOutputLocale(value) {
|
|
35
|
+
if (value === undefined || value === null) return undefined;
|
|
36
|
+
if (!['zh', 'en'].includes(value)) throw new Error('Invalid frozen cloud output locale');
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
export function cloudMessage(key, locale) {
|
|
40
|
+
if (!Object.hasOwn(copy, key)) throw new Error('Unknown cloud presentation message');
|
|
41
|
+
return copy[key][locale === 'en' ? 1 : 0];
|
|
42
|
+
}
|
|
43
|
+
export function cloudProseMatches(text, locale) {
|
|
44
|
+
return typeof text === 'string' && (locale === 'en' ? !/\p{Script=Han}/u.test(text) : /\p{Script=Han}/u.test(text));
|
|
45
|
+
}
|
|
46
|
+
export function cloudEventDisplay(value, locale) {
|
|
47
|
+
const metadata = value.metadata ?? {}, content = value.content ?? '';
|
|
48
|
+
const fixed = Object.hasOwn(events, content) ? events[content][locale === 'en' ? 1 : 0]
|
|
49
|
+
: content.trim() === copy.keepAlive[0] ? cloudMessage('keepAlive', locale) : undefined;
|
|
50
|
+
const sourceContent = !fixed && !cloudProseMatches(content, locale) ? content : undefined;
|
|
51
|
+
const displayed = fixed ?? (sourceContent ? cloudMessage(value.kind === 'error' ? 'failed' : 'event', locale) : content);
|
|
52
|
+
const originalSummary = typeof metadata.summary === 'string' ? metadata.summary : undefined;
|
|
53
|
+
const translated = metadata.summaryTranslations?.[locale];
|
|
54
|
+
const candidate = typeof translated === 'string' && translated.trim() ? translated : originalSummary;
|
|
55
|
+
const summary = candidate && cloudProseMatches(candidate, locale) ? candidate : undefined;
|
|
56
|
+
return { content: displayed, ...(summary ? { summary } : {}),
|
|
57
|
+
...(sourceContent ? { sourceContent } : {}), ...(!summary && originalSummary ? { sourceSummary: originalSummary } : {}),
|
|
58
|
+
sourceLocale: cloudOutputLocale(metadata.outputLocale) ?? 'unknown' };
|
|
59
|
+
}
|
package/cloud-task-client.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { displayIdeEvent, ideClientHeaders, ideTaskIdentity } from './ide-presen
|
|
|
5
5
|
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
6
|
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
7
7
|
import { prepareCloudImages } from './cloud-task-images.mjs';
|
|
8
|
+
import { cloudEventDisplay, cloudMessage, cloudOutputLocale } from './cloud-locale.mjs';
|
|
8
9
|
|
|
9
10
|
const CLOUD_TASK_SCHEMA = 'entrorove.cloud-task/1.0';
|
|
10
11
|
const CLOUD_STATUS_SCHEMA = 'entrorove.cloud-status/1.0';
|
|
@@ -42,9 +43,11 @@ function validateInput(input) {
|
|
|
42
43
|
if (input.images !== undefined && (!Array.isArray(input.images) || input.images.length > 4)) {
|
|
43
44
|
throw new Error('cloud-task images must be an array of at most four images');
|
|
44
45
|
}
|
|
46
|
+
if (input.outputLocale !== undefined && !['zh', 'en'].includes(input.outputLocale)) throw new Error('cloud-task outputLocale must be zh or en');
|
|
47
|
+
const outputLocale = cloudOutputLocale(input.outputLocale);
|
|
45
48
|
return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }),
|
|
46
49
|
...(input.images === undefined ? {} : { images: input.images }),
|
|
47
|
-
...(input.task === undefined ? {} : { task: input.task }) };
|
|
50
|
+
...(input.task === undefined ? {} : { task: input.task }), ...(outputLocale ? { outputLocale } : {}) };
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function validateDependencies(dependencies) {
|
|
@@ -64,18 +67,19 @@ function authenticatedHeaders(token, headers = {}) {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export function sandboxChoiceInteraction(id, task = {}) {
|
|
70
|
+
const locale = cloudOutputLocale(task.outputLocale);
|
|
67
71
|
return {
|
|
68
72
|
schemaVersion: 'confirm.interaction/1.0', requestId: id, type: 'choice',
|
|
69
|
-
question: '
|
|
73
|
+
question: cloudMessage('choice', locale),
|
|
70
74
|
options: [
|
|
71
|
-
{ id: 'use', label: '
|
|
72
|
-
{ id: 'local', label: '
|
|
75
|
+
{ id: 'use', label: cloudMessage('use', locale), hint: cloudMessage('useHint', locale) },
|
|
76
|
+
{ id: 'local', label: cloudMessage('local', locale), hint: cloudMessage('localHint', locale) },
|
|
73
77
|
],
|
|
74
78
|
default: 'local', timeout: null, timeoutAction: 'local', risk: 'low',
|
|
75
|
-
riskDescription: '
|
|
79
|
+
riskDescription: cloudMessage('risk', locale),
|
|
76
80
|
rememberable: false, memoryKey: '',
|
|
77
81
|
callback: { operation: 'cloud-task', payload: { ...task } },
|
|
78
|
-
chatFallback: '
|
|
82
|
+
chatFallback: cloudMessage('fallback', locale),
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -103,9 +107,9 @@ export async function runCloudTask(input, dependencies) {
|
|
|
103
107
|
task, interaction: sandboxChoiceInteraction(id,task),
|
|
104
108
|
});
|
|
105
109
|
if (choice === 'local') return envelope(task, 'skipped', {
|
|
106
|
-
reason: 'task', executionIsolation: 'local', message: '
|
|
110
|
+
reason: 'task', executionIsolation: 'local', message: cloudMessage('skipped', task.outputLocale),
|
|
107
111
|
continuation: { goal: task.goal,...(task.ideTaskId ? { ideTaskId: task.ideTaskId } : {}),
|
|
108
|
-
...(task.images === undefined ? {} : { images: task.images }) },
|
|
112
|
+
...(task.images === undefined ? {} : { images: task.images }), ...(task.outputLocale ? { outputLocale: task.outputLocale } : {}) },
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
if (task.images !== undefined) task.images = await prepareCloudImages(task.images);
|
|
@@ -115,7 +119,7 @@ export async function runCloudTask(input, dependencies) {
|
|
|
115
119
|
const metadata = { kind: 'remote', operation: 'cloud-task', requestId: id };
|
|
116
120
|
await appendInvocation({ ...metadata, event: 'started' }, dependencies.home);
|
|
117
121
|
displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',requestId: id,kind: 'status',
|
|
118
|
-
phase: 'enter',message: '
|
|
122
|
+
phase: 'enter',message: cloudMessage('enter', task.outputLocale),separateLine: true },dependencies.onIdeEvent,{home:dependencies.home});
|
|
119
123
|
let response;
|
|
120
124
|
try {
|
|
121
125
|
response = await requestTransport(origin, '/api/cloud-task', {
|
|
@@ -142,9 +146,12 @@ export async function runCloudTask(input, dependencies) {
|
|
|
142
146
|
if (!response.ok) {
|
|
143
147
|
await appendInvocation({ ...metadata, event: response.status >= 500 ? 'outcome-unknown' : 'rejected' }, dependencies.home);
|
|
144
148
|
const code = typeof result?.error === 'string' ? result.error : 'cloud-task-rejected';
|
|
145
|
-
|
|
149
|
+
const rawMessage = typeof result?.message === 'string' ? result.message : code;
|
|
150
|
+
const display = task.outputLocale ? cloudEventDisplay({ kind: 'error',content: rawMessage },task.outputLocale) : undefined;
|
|
151
|
+
return envelope(task, response.status === 403 ? 'blocked' : 'failed', { code, message: display?.content ?? rawMessage,
|
|
152
|
+
...(display?.sourceContent ? { source: { label: cloudMessage('source',task.outputLocale),content: display.sourceContent } } : {}), httpStatus: response.status });
|
|
146
153
|
}
|
|
147
|
-
if (!result?.ok || typeof result.taskId !== 'string' || !UUID.test(result.taskId)
|
|
154
|
+
if (!result?.ok || result.outputLocale !== task.outputLocale || typeof result.taskId !== 'string' || !UUID.test(result.taskId)
|
|
148
155
|
|| result.taskNumber !== undefined && result.taskNumber !== sandboxTaskNumber(result.taskId)
|
|
149
156
|
|| result.afterId !== undefined && (typeof result.afterId !== 'string' || !/^[1-9][0-9]{0,18}$/u.test(result.afterId) || BigInt(result.afterId)>9223372036854775807n)) {
|
|
150
157
|
await appendInvocation({ ...metadata, event: 'outcome-unknown' }, dependencies.home);
|
|
@@ -153,7 +160,7 @@ export async function runCloudTask(input, dependencies) {
|
|
|
153
160
|
await appendInvocation({ ...metadata, event: 'completed' }, dependencies.home);
|
|
154
161
|
const taskNumber = sandboxTaskNumber(result.taskId);
|
|
155
162
|
const event = { schemaVersion: 'entrorove.ide-event/1.0',requestId: id,taskId: result.taskId,taskNumber,
|
|
156
|
-
kind: 'status',phase: 'accepted',message:
|
|
163
|
+
kind: 'status',phase: 'accepted',message: `${cloudMessage('task', task.outputLocale)} ${taskNumber} · ${cloudMessage('accepted', task.outputLocale)}`,separateLine: true };
|
|
157
164
|
displayIdeEvent(event,dependencies.onIdeEvent,{home:dependencies.home});
|
|
158
165
|
return envelope(task, 'started', { taskId: result.taskId,taskNumber,ideMessages: [event.message],
|
|
159
166
|
taskStatus: typeof result.status === 'string' ? result.status : 'running',
|
|
@@ -164,7 +171,7 @@ export async function runCloudTask(input, dependencies) {
|
|
|
164
171
|
function publicTask(task) {
|
|
165
172
|
if (!task || typeof task !== 'object' || Array.isArray(task)) return null;
|
|
166
173
|
const allowed = ['id', 'status', 'displayTitle', 'titleStatus', 'titleError', 'summary', 'ideTaskId',
|
|
167
|
-
'createdAt', 'updatedAt', 'completedAt'];
|
|
174
|
+
'createdAt', 'updatedAt', 'completedAt', 'outputLocale'];
|
|
168
175
|
return Object.fromEntries(allowed.filter(key => task[key] !== undefined).map(key => [key, task[key]]));
|
|
169
176
|
}
|
|
170
177
|
|
|
@@ -196,6 +203,7 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
196
203
|
code: typeof taskBody?.error === 'string' ? taskBody.error : taskResponse.status === 404 ? 'cloud-task-not-found' : 'cloud-status-unavailable', httpStatus: taskResponse.status };
|
|
197
204
|
}
|
|
198
205
|
const task = taskBody.task;
|
|
206
|
+
cloudOutputLocale(task.outputLocale);
|
|
199
207
|
const feedback = taskBody.feedback ? verifyAutomaticFeedback(taskBody.feedback,taskBody.feedback.requestId) : undefined;
|
|
200
208
|
let usage = [];
|
|
201
209
|
let usageError;
|
package/cloud-watch-client.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { automaticFeedbackIdeEvent, cloudIdeEvent, displayIdeEvent, ideClientHea
|
|
|
4
4
|
import { queryCloudTask } from './cloud-task-client.mjs';
|
|
5
5
|
import { acknowledgeAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
6
|
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
7
|
+
import { cloudMessage, cloudOutputLocale } from './cloud-locale.mjs';
|
|
7
8
|
|
|
8
9
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
10
|
function inputOptions(input) {
|
|
@@ -24,6 +25,7 @@ export async function watchCloudTask(input, dependencies) {
|
|
|
24
25
|
const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
|
|
25
26
|
const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent,{home:dependencies.home}); };
|
|
26
27
|
let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
|
|
28
|
+
let outputLocale;
|
|
27
29
|
try {
|
|
28
30
|
const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
|
|
29
31
|
redirect: 'error',signal: controller.signal,headers: { authorization: `Bearer ${token}`,
|
|
@@ -38,6 +40,9 @@ export async function watchCloudTask(input, dependencies) {
|
|
|
38
40
|
}
|
|
39
41
|
if (!/^[1-9][0-9]{0,18}$/u.test(frame.id ?? '') || frame.data?.id !== frame.id) throw new Error('Invalid cloud event cursor');
|
|
40
42
|
if (BigInt(frame.id) <= BigInt(afterId)) return;
|
|
43
|
+
const eventLocale = cloudOutputLocale(frame.data.metadata?.outputLocale);
|
|
44
|
+
if (outputLocale && eventLocale !== outputLocale) throw new Error('Cloud stream changed its original output language');
|
|
45
|
+
if (eventLocale) outputLocale = eventLocale;
|
|
41
46
|
const event = cloudIdeEvent(frame.data,taskId); afterId = frame.id; emit(event);
|
|
42
47
|
if (typeof frame.data.metadata?.flowId === 'string') flowId = frame.data.metadata.flowId;
|
|
43
48
|
settled = event.phase === 'workflow' && ['reported','verified','blocked','failed'].includes(event.state);
|
|
@@ -48,15 +53,18 @@ export async function watchCloudTask(input, dependencies) {
|
|
|
48
53
|
if (!controller.signal.aborted) throw cause;
|
|
49
54
|
} finally { clearTimeout(deadline); controller.abort(); }
|
|
50
55
|
if (outputAvailable) emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'output',
|
|
51
|
-
message: '
|
|
56
|
+
message: cloudMessage('output', outputLocale),separateLine: true });
|
|
52
57
|
const current = await queryCloudTask(taskId,dependencies);
|
|
53
58
|
if (current.status === 'observed') taskStatus = current.taskStatus;
|
|
59
|
+
const currentLocale = cloudOutputLocale(current.task?.outputLocale);
|
|
60
|
+
if (outputLocale && currentLocale !== outputLocale) throw new Error('Cloud status changed its original output language');
|
|
61
|
+
outputLocale = currentLocale ?? outputLocale;
|
|
54
62
|
if (current.feedback) {
|
|
55
63
|
current.feedback = await acknowledgeAutomaticFeedback(current.feedback,dependencies.apiOrigin(),token,ideClientHeaders());
|
|
56
|
-
emit({ ...automaticFeedbackIdeEvent(current.feedback),taskId,taskNumber:sandboxTaskNumber(taskId) });
|
|
64
|
+
emit({ ...automaticFeedbackIdeEvent(current.feedback, outputLocale),taskId,taskNumber:sandboxTaskNumber(taskId) });
|
|
57
65
|
}
|
|
58
66
|
emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
|
|
59
|
-
message: settled || taskStatus !== 'running' ? '
|
|
67
|
+
message: cloudMessage(settled || taskStatus !== 'running' ? 'exit' : 'waiting', outputLocale),separateLine: true });
|
|
60
68
|
return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskNumber: sandboxTaskNumber(taskId),taskStatus,afterId,outputAvailable,
|
|
61
69
|
events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
|
|
62
70
|
nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
|
package/design-lint.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import postcss from 'postcss';
|
|
|
3
3
|
import { parse as parseVue } from '@vue/compiler-sfc';
|
|
4
4
|
import { designFail, validateDesignPolicy } from './design-policy.mjs';
|
|
5
5
|
import { designUiAsset } from './design-ui.mjs';
|
|
6
|
+
import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
|
|
6
7
|
|
|
7
8
|
export { designUiCandidate } from './design-ui.mjs';
|
|
8
9
|
const issue = (rule, detail) => ({ rule, detail });
|
|
@@ -65,7 +66,7 @@ function scriptIssues(source, name, rules) {
|
|
|
65
66
|
walk(tree);
|
|
66
67
|
return { found, imports };
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
function lintSource(name, source, rules) {
|
|
69
70
|
if (designUiAsset(name)) return [];
|
|
70
71
|
if (typeof source !== 'string' || Buffer.byteLength(source) > 2_097_152) designFail('LINT_LIMIT', 'UI lint requires bounded UTF-8 source');
|
|
71
72
|
if (/\.(?:scss|sass|less|styl|wxml|wxss|axml|acss)$/i.test(name)) return [issue('unsupported-ui-language', name)];
|
|
@@ -85,10 +86,27 @@ export function designLint(name, source, rules) {
|
|
|
85
86
|
if (styles.length) found.push(issue('central-style-source-required', name));
|
|
86
87
|
return found;
|
|
87
88
|
}
|
|
89
|
+
export function designLint(name, source, rules) {
|
|
90
|
+
if (!/\.vue$/i.test(name)) return lintSource(name, source, rules);
|
|
91
|
+
if (typeof source !== 'string' || Buffer.byteLength(source) > 2_097_152) designFail('LINT_LIMIT', 'UI lint requires bounded UTF-8 source');
|
|
92
|
+
try {
|
|
93
|
+
const conditional = uniConditionalVariants(source, name);
|
|
94
|
+
if (!conditional.directives.length) return lintSource(name, source, rules);
|
|
95
|
+
return conditional.variants.flatMap(variant => variant.source.trim()
|
|
96
|
+
? lintSource(name, variant.source, rules).map(problem => ({ ...problem, conditionalBranch: variant.key })) : []);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!error.code?.startsWith('ENTROROVE_')) throw error;
|
|
99
|
+
return [issue('vue-conditional', error.message)];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
88
102
|
export function assertDesignLint(policy, name, before, after, approvedBefore = before) {
|
|
89
103
|
validateDesignPolicy(policy);
|
|
90
104
|
const retained = new Map();
|
|
91
|
-
const inspect = source =>
|
|
105
|
+
const inspect = source => {
|
|
106
|
+
const problems = [...designLint(name, source, policy.rules), ...privateFieldIssues(source, name, policy)];
|
|
107
|
+
if (problems.some(problem => problem.rule === 'vue-conditional')) designFail('LINT', 'Conditional UI source requires complete supported branch inspection');
|
|
108
|
+
return problems;
|
|
109
|
+
};
|
|
92
110
|
const approved = new Map();
|
|
93
111
|
for (const problem of inspect(approvedBefore)) { const key = JSON.stringify(problem); approved.set(key, (approved.get(key) ?? 0) + 1); }
|
|
94
112
|
for (const problem of inspect(before)) { const key = JSON.stringify(problem); retained.set(key, Math.min((retained.get(key) ?? 0) + 1, approved.get(key) ?? 0)); }
|
package/ide-presentation.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const UUID = /^[0-9a-f-]{36}$/i;
|
|
2
2
|
import { notifyIdeEvent } from './native-notification.mjs';
|
|
3
3
|
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
4
|
+
import { cloudEventDisplay, cloudMessage, cloudOutputLocale } from './cloud-locale.mjs';
|
|
4
5
|
export function ideClientHeaders(environment = process.env) {
|
|
5
6
|
const ide = environment.ENTROROVE_IDE ?? (environment.CODEX_THREAD_ID ? 'Codex'
|
|
6
7
|
: environment.CURSOR_TRACE_ID ? 'Cursor' : environment.CLAUDE_CODE_ENTRYPOINT ? 'Claude Code' : 'Entrorove CLI');
|
|
@@ -27,17 +28,23 @@ export function cloudIdeEvent(value, taskId) {
|
|
|
27
28
|
throw new Error('Cloud event lost its original task identity');
|
|
28
29
|
}
|
|
29
30
|
const metadata = value.metadata ?? {};
|
|
31
|
+
const locale = cloudOutputLocale(metadata.outputLocale);
|
|
32
|
+
const displayed = locale ? cloudEventDisplay(value, locale) : undefined;
|
|
30
33
|
let message = text(value.content), phase = String(metadata.stepId ?? 'workflow');
|
|
31
|
-
if (message === 'Cloud task accepted.') { message =
|
|
34
|
+
if (message === 'Cloud task accepted.') { message = `${cloudMessage('task', locale)} ${sandboxTaskNumber(taskId)} · ${cloudMessage('accepted', locale)}`; phase = 'accepted'; }
|
|
32
35
|
else if (message === 'Cloud workflow started.') message = '云端沙箱任务已分配';
|
|
33
36
|
else if (message === 'Cloud model classification started.') message = '云端沙箱正在思考中';
|
|
34
37
|
else if (message === 'Task classification completed.') message = '云端沙箱思考正在输出';
|
|
38
|
+
if (displayed && phase !== 'accepted') message = displayed.content;
|
|
35
39
|
const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,taskNumber: sandboxTaskNumber(taskId),eventId: value.id,kind: value.kind,
|
|
36
40
|
state: metadata.state ?? 'running',phase,message,separateLine: true };
|
|
37
41
|
if (metadata.requiresUserInput === true || phase === 'confirm' && event.state === 'pending') event.requiresConfirmation = true;
|
|
38
42
|
if (metadata.errorCode === 'model-balance-insufficient') event.errorCode = metadata.errorCode;
|
|
39
43
|
if (typeof metadata.summary === 'string' && metadata.summary !== value.content
|
|
40
|
-
&& value.content !== 'Cloud model classification started.') event.detail = text(metadata.summary);
|
|
44
|
+
&& value.content !== 'Cloud model classification started.') event.detail = text(displayed?.summary ?? (locale ? '' : metadata.summary));
|
|
45
|
+
if (displayed?.sourceContent || displayed?.sourceSummary) event.source = {
|
|
46
|
+
label: cloudMessage('source', locale), ...(displayed.sourceContent ? { content: text(displayed.sourceContent) } : {}),
|
|
47
|
+
...(displayed.sourceSummary ? { summary: text(displayed.sourceSummary) } : {}), locale: displayed.sourceLocale };
|
|
41
48
|
if (value.kind === 'notice' && metadata.url !== undefined) {
|
|
42
49
|
let url;
|
|
43
50
|
try { url = new URL(metadata.url); } catch { throw new Error('Cloud promotion link is invalid'); }
|
|
@@ -63,11 +70,15 @@ export function presentInvocationNotices(result, sink, notifications) {
|
|
|
63
70
|
}
|
|
64
71
|
return messages.length ? { ...result,ideMessages: messages } : result;
|
|
65
72
|
}
|
|
66
|
-
export function automaticFeedbackIdeEvent(feedback) {
|
|
73
|
+
export function automaticFeedbackIdeEvent(feedback, outputLocale) {
|
|
67
74
|
const disposition = feedback.evaluation.disposition;
|
|
68
75
|
const status = disposition === 'good' ? '当前调用验证通过' : disposition === 'bad'
|
|
69
76
|
? '当前调用被阻断或验证失败' : '交付验证尚未完成,暂不评分';
|
|
77
|
+
const english = outputLocale === 'en';
|
|
78
|
+
const englishStatus = disposition === 'good' ? 'this invocation passed verification' : disposition === 'bad'
|
|
79
|
+
? 'this invocation was blocked or failed verification' : 'delivery verification remains incomplete; no score is assigned';
|
|
70
80
|
return { schemaVersion: 'entrorove.ide-event/1.0',requestId: feedback.requestId,receiptId: feedback.receiptId,
|
|
71
81
|
kind: 'evaluation',phase: 'feedback',disposition,
|
|
72
|
-
message: `${feedback.acknowledgement?.stored === true ? '
|
|
82
|
+
message: english ? `${feedback.acknowledgement?.stored === true ? 'The server acknowledged this invocation evaluation' : 'The server saved this invocation evaluation'}: ${englishStatus}.`
|
|
83
|
+
: `${feedback.acknowledgement?.stored === true ? '本次调用评价已回传至服务器' : '服务器已保存本次调用评价'}:${status}。`,separateLine: true };
|
|
73
84
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { parse as parseVue, compileTemplate } from '@vue/compiler-sfc';
|
|
1
|
+
import { parse as parseVue, parseCache, compileTemplate } from '@vue/compiler-sfc';
|
|
2
2
|
import { parse as parseSvelte } from 'svelte/compiler';
|
|
3
3
|
import { analyzeArchguardLanguage } from './local-archguard-language-ast.mjs';
|
|
4
|
+
import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
|
|
4
5
|
|
|
5
6
|
export function archguardComponentLanguage(path) {
|
|
6
7
|
return new Map([['vue', 'vue'], ['svelte', 'svelte'], ['wxml', 'wxml']]).get(path.split('.').at(-1)) ?? null;
|
|
@@ -70,6 +71,7 @@ function addStyle(target, source, rules, content, base, lineOffsets, property =
|
|
|
70
71
|
function vueSections(file) {
|
|
71
72
|
const parsed = parseVue(file.content, { filename: file.path, sourceMap: false,
|
|
72
73
|
templateParseOptions: { expressionPlugins: ['typescript'] } });
|
|
74
|
+
parseCache.clear();
|
|
73
75
|
if (parsed.errors.length) return { errors: parsed.errors };
|
|
74
76
|
const descriptor = parsed.descriptor;
|
|
75
77
|
if (descriptor.customBlocks.length || descriptor.template?.lang) throw new Error('ARCH-AST-EMBEDDED-LANGUAGE-UNSUPPORTED');
|
|
@@ -137,11 +139,13 @@ function wxmlSections(file) {
|
|
|
137
139
|
transformAssetUrls: false, compilerOptions: { comments: true } });
|
|
138
140
|
return { template: compiled.ast, scripts: [], styles: [], errors: compiled.errors };
|
|
139
141
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const target = { selected: new Map(rules.filter(rule => rule.engine === 'ast').map(rule => [rule.id, { rule, locations: [] }])),
|
|
142
|
+
function componentResult(rules) {
|
|
143
|
+
return { selected: new Map(rules.filter(rule => rule.engine === 'ast').map(rule => [rule.id, { rule, locations: [] }])),
|
|
143
144
|
builtin: new Map(['no-eval', 'no-debug', 'no-inline-style', 'no-hardcoded-color', 'no-magic-number'].map(id => [id, []])),
|
|
144
145
|
dependencies: [], maxFunctionLines: 0, maxNestingDepth: 0, animations: 0, transitions: 0, diagnostics: [] };
|
|
146
|
+
}
|
|
147
|
+
function analyzeComponentVariant(file, rules, analyzeScript) {
|
|
148
|
+
const language = archguardComponentLanguage(file.path), lineOffsets = offsets(file.content), target = componentResult(rules);
|
|
145
149
|
try {
|
|
146
150
|
let sections; let ranges;
|
|
147
151
|
if (language === 'svelte') {
|
|
@@ -175,3 +179,26 @@ export function analyzeArchguardComponent(file, rules, analyzeScript) {
|
|
|
175
179
|
target.diagnostics.push(syntaxError(error, language, lineOffsets)); return target;
|
|
176
180
|
}
|
|
177
181
|
}
|
|
182
|
+
function unionLocations(left, right) {
|
|
183
|
+
return [...new Map([...left, ...right].map(item => [JSON.stringify(item), item])).values()];
|
|
184
|
+
}
|
|
185
|
+
export function analyzeArchguardComponent(file, rules, analyzeScript) {
|
|
186
|
+
if (archguardComponentLanguage(file.path) !== 'vue') return analyzeComponentVariant(file, rules, analyzeScript);
|
|
187
|
+
const target = componentResult(rules);
|
|
188
|
+
try {
|
|
189
|
+
const conditional = uniConditionalVariants(file.content, file.path);
|
|
190
|
+
if (!conditional.directives.length) return analyzeComponentVariant(file, rules, analyzeScript);
|
|
191
|
+
const inspected = new Set();
|
|
192
|
+
for (const variant of conditional.variants) {
|
|
193
|
+
if (!variant.source.trim() || inspected.has(variant.source)) continue;
|
|
194
|
+
inspected.add(variant.source);
|
|
195
|
+
const part = analyzeComponentVariant({ ...file, content: variant.source }, rules, analyzeScript);
|
|
196
|
+
for (const [id, locations] of part.builtin) target.builtin.set(id, unionLocations(target.builtin.get(id), locations));
|
|
197
|
+
for (const [id, selected] of part.selected) target.selected.get(id).locations = unionLocations(target.selected.get(id).locations, selected.locations);
|
|
198
|
+
for (const field of ['dependencies', 'diagnostics']) target[field] = unionLocations(target[field], part[field]);
|
|
199
|
+
for (const field of ['maxFunctionLines', 'maxNestingDepth', 'animations', 'transitions']) target[field] = Math.max(target[field], part[field]);
|
|
200
|
+
}
|
|
201
|
+
if (!inspected.size) throw new Error('Conditional component has no inspectable branch');
|
|
202
|
+
} catch (error) { target.diagnostics.push(syntaxError(error, 'uni-conditional', offsets(file.content))); }
|
|
203
|
+
return target;
|
|
204
|
+
}
|
package/local-budget-reads.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export async function readBudgetedProjectFile(input, target, options = {}) {
|
|
|
102
102
|
const root = await fs.realpath(input.repositoryRoot);
|
|
103
103
|
const sourcePath = options.sourcePath ?? path.relative(root, target).split(path.sep).join('/');
|
|
104
104
|
const before = await fs.lstat(target, { bigint: true });
|
|
105
|
-
const result = await budgetReadBuffer({ ...binding, path: sourcePath }, { target });
|
|
105
|
+
const result = await budgetReadBuffer({ ...binding, path: sourcePath }, { target, verifyReadTarget: options.verifyReadTarget });
|
|
106
106
|
const after = await fs.lstat(target, { bigint: true });
|
|
107
107
|
if (!sameSourceStat(before, after)) fail('ENTROROVE_SCOPE_CHANGED', 'budgeted project source changed during its bound observation');
|
|
108
108
|
projectBuffers.set(result.content, Object.freeze({ home: await fs.realpath(binding.home ?? os.homedir()),
|
|
@@ -110,7 +110,12 @@ export async function readBudgetedProjectFile(input, target, options = {}) {
|
|
|
110
110
|
return options.encoding ? result.content.toString(options.encoding) : result.content;
|
|
111
111
|
}
|
|
112
112
|
const handle = await fs.open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
113
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
const descriptor = await handle.stat();
|
|
115
|
+
if (!descriptor.isFile()) fail('ENTROROVE_FILE_INVALID', 'source must be a regular file');
|
|
116
|
+
if (options.verifyReadTarget) await options.verifyReadTarget({ target: await fs.realpath(target), descriptor });
|
|
117
|
+
return await handle.readFile(options.encoding ? { encoding: options.encoding } : undefined);
|
|
118
|
+
}
|
|
114
119
|
finally { await handle.close(); }
|
|
115
120
|
}
|
|
116
121
|
|
package/local-budget.mjs
CHANGED
|
@@ -91,7 +91,7 @@ function assertReadAllowed(state, relative, tokens, now = Date.now()) {
|
|
|
91
91
|
if (tokens > state.maxTokenEstimate - state.tokenEstimate) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read token budget exhausted');
|
|
92
92
|
if (now - Date.parse(state.startedAt) >= state.maxDurationMs) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read duration budget exhausted');
|
|
93
93
|
}
|
|
94
|
-
async function readSource(location, state, relative, target, received) {
|
|
94
|
+
async function readSource(location, state, relative, target, received, verifyReadTarget) {
|
|
95
95
|
const binding = { home: location.home, repositoryRoot: location.root, flowId: location.flowId, path: relative };
|
|
96
96
|
await assertBudgetHandoffReadAllowed(binding, state);
|
|
97
97
|
const actualTarget = await budgetSourceTarget(location, relative, target);
|
|
@@ -101,6 +101,7 @@ async function readSource(location, state, relative, target, received) {
|
|
|
101
101
|
try {
|
|
102
102
|
const stat = await handle.stat(); if (!stat.isFile()) fail('ENTROROVE_FILE_INVALID', 'budget source must be a regular file');
|
|
103
103
|
await assertDescriptorTarget(target, actualTarget, stat);
|
|
104
|
+
if (verifyReadTarget) await verifyReadTarget({ target: actualTarget, descriptor: stat });
|
|
104
105
|
assertReadAllowed(state, relative, Math.ceil(stat.size / 4));
|
|
105
106
|
const cap = state.enforced ? Math.max(0, (state.maxTokenEstimate - state.tokenEstimate) * 4) : Number.MAX_SAFE_INTEGER;
|
|
106
107
|
const chunks = []; let size = 0;
|
|
@@ -131,7 +132,7 @@ export async function budgetReadBuffer(input, options = {}) {
|
|
|
131
132
|
if (state.grant?.purpose === 'handoff-read') requireLiveBudget(budgetHandoffReadValidationState(state));
|
|
132
133
|
else state = consumeBudgetRenewal({ state });
|
|
133
134
|
const target = options.target ?? path.join(location.root, relative);
|
|
134
|
-
const content = await readSource(location, state, relative, target, received); const tokens = Math.ceil(content.length / 4);
|
|
135
|
+
const content = await readSource(location, state, relative, target, received, options.verifyReadTarget); const tokens = Math.ceil(content.length / 4);
|
|
135
136
|
const updated = { ...state, files: state.files.includes(relative) ? state.files : [...state.files, relative], readCalls: state.readCalls + 1, tokenEstimate: state.tokenEstimate + tokens };
|
|
136
137
|
const committed = await commitBudget(location, loaded, updated, 'budget-read', { path: relative, tokens, bytes: content.length,
|
|
137
138
|
readId, renewedIntervals: (state.autoRenew?.renewalsUsed ?? 0) - (loaded.state.autoRenew?.renewalsUsed ?? 0), reason: state.autoRenew?.lastRenewalReason ?? null });
|
|
@@ -3,6 +3,7 @@ import { parse as parseVue, parseCache } from '@vue/compiler-sfc';
|
|
|
3
3
|
import { canonical } from './local-integrity.mjs';
|
|
4
4
|
import { createScopeImportResolver } from './local-scope-import-expressions.mjs';
|
|
5
5
|
import { isDynamicFunctionReference, isTypeOnlyReference } from './local-typescript-function-reference.mjs';
|
|
6
|
+
import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
|
|
6
7
|
|
|
7
8
|
const fail = () => { throw Object.assign(new Error('Preserved modification changed an existing definition, public contract, dependency, or unsupported source boundary'),
|
|
8
9
|
{ code: 'ENTROROVE_MUTATION_DELEGATION_CHANGE' }); };
|
|
@@ -216,17 +217,25 @@ function parts(path, content) {
|
|
|
216
217
|
function retained(before, after) {
|
|
217
218
|
for (const [key, count] of before) if ((after.get(key) ?? 0) < count) fail();
|
|
218
219
|
}
|
|
220
|
+
function compareSource(path, before, after) {
|
|
221
|
+
if (!before.trim() && !after.trim()) return;
|
|
222
|
+
const original = parts(path, before), candidate = parts(path, after);
|
|
223
|
+
if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
|
|
224
|
+
for (let index = 0; index < original.scripts.length; index++) {
|
|
225
|
+
const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
|
|
226
|
+
if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
|
|
227
|
+
retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
219
230
|
export function assertDelegatedSourceModification(input) {
|
|
220
231
|
const { path, before, after } = input ?? {};
|
|
221
232
|
if (typeof path !== 'string' || typeof before !== 'string' || typeof after !== 'string'
|
|
222
233
|
|| Buffer.byteLength(before) > 1_048_576 || Buffer.byteLength(after) > 1_048_576) fail();
|
|
223
234
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
|
|
230
|
-
}
|
|
235
|
+
if (!/\.vue$/i.test(path)) return compareSource(path, before, after);
|
|
236
|
+
const original = uniConditionalVariants(before, path), candidate = uniConditionalVariants(after, path);
|
|
237
|
+
if (canonical(original.directives) !== canonical(candidate.directives)
|
|
238
|
+
|| canonical(original.variants.map(value => value.key)) !== canonical(candidate.variants.map(value => value.key))) fail();
|
|
239
|
+
for (let index = 0; index < original.variants.length; index++) compareSource(path, original.variants[index].source, candidate.variants[index].source);
|
|
231
240
|
} catch { fail(); }
|
|
232
241
|
}
|