@entrorove/skill 0.1.64 → 0.1.67
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/CAPABILITY-MAP.md +23 -0
- package/CHANGELOG.md +15 -0
- package/DELIVERY-BOUNDARIES.md +2 -0
- package/DESIGN-GATES.md +160 -0
- package/REUSE-DISCIPLINE.md +15 -1
- package/SKILL.md +4 -2
- package/blueprint-criterion-runner.mjs +1 -1
- package/cli.mjs +11 -4
- package/design-browser-checks.mjs +86 -0
- package/design-browser-identity.mjs +13 -0
- package/design-browser-state.mjs +41 -0
- package/design-browser.mjs +181 -0
- package/design-context.mjs +34 -0
- package/design-contracts.d.mts +4 -0
- package/design-contracts.mjs +77 -0
- package/design-declaration.mjs +17 -0
- package/design-frameworks.mjs +4 -0
- package/design-gate.mjs +19 -0
- package/design-lint.mjs +115 -0
- package/design-local-contracts.mjs +15 -0
- package/design-operations.mjs +81 -0
- package/design-policy.d.mts +5 -0
- package/design-policy.mjs +137 -0
- package/design-release.mjs +25 -0
- package/design-research.mjs +99 -0
- package/design-runner.mjs +31 -0
- package/design-store.mjs +116 -0
- package/design-ui.mjs +24 -0
- package/install-identity.mjs +16 -0
- package/local-cli.mjs +3 -1
- package/local-contracts.mjs +2 -0
- package/local-discipline-actions.mjs +5 -0
- package/local-discipline-batch.mjs +12 -3
- package/local-discipline-contracts.mjs +15 -3
- package/local-discipline-gate.mjs +9 -2
- package/local-error.mjs +18 -1
- package/local-execution.mjs +2 -1
- package/local-mutation-authority.mjs +2 -0
- package/local-runner-discipline.mjs +17 -3
- package/local-scope-zip.mjs +21 -0
- package/manifest.json +1 -1
- package/package-archive.mjs +54 -0
- package/package-replacement.mjs +48 -0
- package/package.json +34 -6
- package/release-state.json +50 -24
- package/release.mjs +3 -1
- package/releases.json +18 -0
- package/reuse-discipline-release.mjs +3 -1
- package/rollback-history.mjs +51 -0
- package/rollback-runner.mjs +77 -0
- package/version.mjs +20 -3
- package/workflow-local-runner.mjs +9 -0
package/CAPABILITY-MAP.md
CHANGED
|
@@ -139,4 +139,27 @@ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快
|
|
|
139
139
|
|
|
140
140
|
## 发布与校验边界
|
|
141
141
|
|
|
142
|
+
### 界面设计门禁(0.1.65 新增)
|
|
143
|
+
|
|
144
|
+
先 `design-flow` 全站流程,再 `design-research` 实际互联网布局与框架官方规范,
|
|
145
|
+
以 `design-propose` 提交用户选择的效果图或原型图、组件/令牌/交互规则,
|
|
146
|
+
通过原账号 `workflow-design-review` 真实确认后才准备 UI 实现。
|
|
147
|
+
原 scope、签名 plan、M 和敏感变更审批继续生效;guarded-write/batch/delete/move
|
|
148
|
+
及原批准 runner 检查基线与新增规则。原功能测试完成后,
|
|
149
|
+
`workflow-design-context` → 实际 Chromium `design-verify` → 原用户
|
|
150
|
+
`workflow-design-acceptance`,最终 verdict 再核对源码、测试与视觉确认。
|
|
151
|
+
保存必须真实回读并刷新,提前成功、编辑提交、私有字段和布局/样式偏差不能通过。
|
|
152
|
+
详见 [DESIGN-GATES.md](DESIGN-GATES.md)。对应反例和真实链测试为
|
|
153
|
+
`design-gates.test.mjs`、`design-browser.test.mjs`、`design-gates.integration.test.ts`;
|
|
154
|
+
具体发布和部署状态以该版本真实交付证据为准,不改本表历史结论。
|
|
155
|
+
|
|
142
156
|
本文件记录的是仓库对账,不代表 npm 发布件已验证。`@entrorove/skill@0.1.25` 必须在干净目录安装后重新执行对应 AC,未执行项单独列出,不能计入通过。生产服务、数据库和其他项目不属于本表的运行时依赖。
|
|
157
|
+
|
|
158
|
+
## 包替换与凭证回滚门禁
|
|
159
|
+
|
|
160
|
+
受管安装先比较版本和真实运行包摘要;同版本异内容、旧版本覆新版本拒绝。
|
|
161
|
+
受管写入和批量写入读取普通 ZIP/tar/tgz 内部身份及 SemVer,未知、损坏和冲突不能靠文件名或 force 放行。
|
|
162
|
+
实际历史快照字节回退及可识别的直接 Git 回滚被阻断;有意回滚继续使用原独立签名、原事务和精确前后镜像的 rollback 入口。
|
|
163
|
+
普通进程中的包管理器安装需要原冻结清单和锁文件,禁止传入旧包和目的地覆盖参数。
|
|
164
|
+
测试为 `package-replacement.test.mjs`、`rollback-guard.test.mjs`、`install-version-guard.test.mjs` 及真实签名合并/回滚链。
|
|
165
|
+
检查覆盖 SDK 受管入口;任意外部脚本不是 OS 沙箱,发现违规副作用后保留失败及未知影响,不自动反向覆盖。
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.67 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
- 更新了包降级、版本冲突与凭证回滚门禁。
|
|
6
|
+
- Updated package downgrade, version conflict and evidence-bound rollback guards.
|
|
7
|
+
|
|
8
|
+
## 0.1.66 — 2026-10-19
|
|
9
|
+
|
|
10
|
+
- 更新了字体与 ZIP 完整结构核验、复用与变更一致性门禁、IDE 执行与复用纪律文档、包降级、版本冲突与凭证回滚门禁、复用纪律版本追踪、技能运行和受管安装、版本安全检查、IDE 工作流程说明、能力覆盖映射、安装包配置、统一本地错误信封、原任务短时写入授权与文件快照隔离。
|
|
11
|
+
- Updated font and ZIP structural verification, reuse and change consistency checks, IDE execution and reuse discipline documentation, package downgrade, version conflict and evidence-bound rollback guards, reuse discipline release tracking, skill runtime and managed installation, version safety checks, IDE workflow instructions, capability coverage map, package configuration, unified local error envelopes, Original task short-lived mutation authority and file snapshot isolation.
|
|
12
|
+
|
|
13
|
+
## 0.1.65 — 2026-10-19
|
|
14
|
+
|
|
15
|
+
- 新增强制界面设计门禁:先全站流程图、实际布局与框架官方规范研究,再以用户选择的效果图或原型图确认设计;原写入权限继续约束实现,功能测试、真实浏览器回读刷新及视觉确认完成后才允许交付。保留原失败和未知记录。
|
|
16
|
+
- Add enforced UI design gates: whole-site flow, actual layout and official framework research, then original user approval of selected mockups or prototypes before implementation. Original write authority remains mandatory; delivery requires functional tests, actual browser state read-back and reload checks, and user visual acceptance. Preserve failures and unknown outcomes.
|
|
17
|
+
|
|
3
18
|
## 0.1.64 — 2026-10-19
|
|
4
19
|
|
|
5
20
|
- 统一交付原合同连续执行与失败恢复修复:受限完整响应、原请求结算恢复、过期挑战与未用授权处理、已批准依赖与二进制核验、系统元数据观察;新增图片输入并适配 DeepSeek V4.1 Flash,完善中英文明确退出及无 IDE 记录的离线退出。保留范围、签名、预算及失败和未知记录;缺少进程终局证据的观察仍拒绝重跑。
|
package/DELIVERY-BOUNDARIES.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Delivery boundaries and cost visibility
|
|
2
2
|
|
|
3
|
+
UI delivery requires the [design gates](DESIGN-GATES.md) in addition to the original delivery boundary: actual source-bound functional tests, browser state read-back and reload checks, real light/dark screenshots, and the original user's visual acceptance. Generated mockups, a successful HTTP response, a success toast or `passed: true` cannot replace that evidence.
|
|
4
|
+
|
|
3
5
|
Contract for claiming stage delivery and displaying measured consumption. Each section is marked **live** (implemented in this package) or **planned** (documented, not yet wired).
|
|
4
6
|
|
|
5
7
|
## Stop only at a boundary (live)
|
package/DESIGN-GATES.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# UI design and acceptance gates
|
|
2
|
+
|
|
3
|
+
Use this sequence for every governed UI implementation or change. Keep the name
|
|
4
|
+
Entrorove unchanged in every language. Read the actual operation schemas through
|
|
5
|
+
`local capabilities` and server discovery; never invent operations or signatures.
|
|
6
|
+
|
|
7
|
+
## 1. Whole-site flow before UI code
|
|
8
|
+
|
|
9
|
+
Invoke `local design-flow` with the original repository, home, flow, design ID,
|
|
10
|
+
revision and `site`. Declare original requirement IDs, roles, pages, routes,
|
|
11
|
+
entry points, public and internal fields, states, and page-to-page actions.
|
|
12
|
+
Actions distinguish open, edit, input, validation, navigation, save, cancel and
|
|
13
|
+
delete. Every page must be reachable; mutation pages need failure and cancel
|
|
14
|
+
paths. The SDK freezes a Mermaid flow diagram in private design storage.
|
|
15
|
+
|
|
16
|
+
Present that whole-site diagram before designing screens. Do not use a few
|
|
17
|
+
isolated page screenshots as the site's information architecture. Preserve
|
|
18
|
+
the existing implementation when extending a system, and preserve the original
|
|
19
|
+
task scope and checklist order.
|
|
20
|
+
|
|
21
|
+
## 2. Research layouts and official framework rules
|
|
22
|
+
|
|
23
|
+
Invoke `local design-research` after recording the flow. Supply two to eight
|
|
24
|
+
public HTTPS layout references from independent sites and a concrete reason
|
|
25
|
+
each pattern fits the actual product. Look up current, widely used structures
|
|
26
|
+
for the relevant page type: navigation, list/filter/detail, editing, confirmation,
|
|
27
|
+
empty/loading/error states and responsive behavior. Fetch the actual references;
|
|
28
|
+
an unsupported claim that a layout is popular does not count as research.
|
|
29
|
+
|
|
30
|
+
When the user specifies a framework or the nearest UI package declares one,
|
|
31
|
+
use its official documentation. Bind `packageName`, exact locked `version`,
|
|
32
|
+
`packagePath`, `lockPath` and `officialUrls`. The SDK checks recognized official
|
|
33
|
+
hosts, dependency declarations and package/lock hashes. Use the framework's
|
|
34
|
+
standard controls, navigation conventions and theme integration. Do not replace
|
|
35
|
+
a declared component library with arbitrary native controls or another library.
|
|
36
|
+
Unsupported frameworks or lock formats require explicit implementation support;
|
|
37
|
+
never silently relabel them as a supported framework.
|
|
38
|
+
|
|
39
|
+
References are bounded, credential-free public reads. They grant no business
|
|
40
|
+
source, account, database, runtime, deployment or write access.
|
|
41
|
+
|
|
42
|
+
## 3. Create mockups or prototypes and get actual user confirmation
|
|
43
|
+
|
|
44
|
+
Respect the user's choice: an effect mockup (`kind: mockup`) or a prototype /
|
|
45
|
+
wireframe (`kind: prototype`) is a valid visual baseline. Use the host's actual
|
|
46
|
+
image-generation or design/prototyping tool after research. Present several
|
|
47
|
+
directions when undecided, then export real PNG views of the chosen pages and
|
|
48
|
+
required states at the target viewport sizes. A structural prototype can be
|
|
49
|
+
simple; bind theme colors, typography, spacing and components separately in its
|
|
50
|
+
rules so implementation cannot invent them. The same prototype may represent
|
|
51
|
+
both theme layouts when the approved tokens specify the theme differences.
|
|
52
|
+
Fix layout and interactive states before implementation. If no suitable tool
|
|
53
|
+
is available, report the missing prerequisite and wait; never fabricate tool
|
|
54
|
+
results or a user's approval. High-fidelity effect images are not mandatory.
|
|
55
|
+
|
|
56
|
+
Invoke `local design-browser-info` for the actual Chromium executable identity.
|
|
57
|
+
Choose an isolated `http://127.0.0.1:<port>/` preview before approval. Invoke
|
|
58
|
+
`local design-propose` with the real PNGs (each `mockups` entry includes its
|
|
59
|
+
`kind`), actual tool generation/export records and executable
|
|
60
|
+
`rules`. Each generation record is a plain JSON object:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{"schemaVersion":"entrorove.image-generation-record/1.0","tool":"actual-host-tool","requestId":"actual-tool-call-id","promptSha256":"<64 hex>","imageSha256":"<64 hex>","generatedAt":"<actual ISO timestamp>"}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
These references bind bytes and chronology; they are not an independent signature
|
|
67
|
+
from the external image/design tool. Show the actual host tool result and
|
|
68
|
+
mockup/prototype views. Never present an owned test bitmap as a real user design.
|
|
69
|
+
|
|
70
|
+
Rules freeze UI paths, verification paths, browser identity, preview origin,
|
|
71
|
+
approved component imports, native replacements, central style/token sources,
|
|
72
|
+
public field projections and executable interaction/layout/style assertions.
|
|
73
|
+
Every page/state/theme mockup needs its own scenario. Each scenario has an
|
|
74
|
+
`entryPageId`, entry `route`, ordered original flow edges, a final mockup index,
|
|
75
|
+
layout bounds, computed styles and a success-message selector.
|
|
76
|
+
|
|
77
|
+
Show the flow, research links, images and concrete rules to the user. Submit
|
|
78
|
+
`continue` → `workflow-design-review` with the exact `design`. Present the
|
|
79
|
+
returned interaction using the host question UI and submit only the user's
|
|
80
|
+
actual answer to its original callback. No default, timeout, remembered answer,
|
|
81
|
+
model-generated `approved:true` or another account can approve it. Do not
|
|
82
|
+
prepare UI implementation code before this confirmation.
|
|
83
|
+
|
|
84
|
+
The response's `approvedDesign` contains `design` and `designApproval`. Carry
|
|
85
|
+
both into the original `policy.discipline`. This approval grants no execution,
|
|
86
|
+
scope or write authority. Original scope, candidate source hashes, signed plan,
|
|
87
|
+
budget, mutation pass and any separate sensitive-change approval still apply.
|
|
88
|
+
Changing the design needs a new revision and genuine confirmation. Keep every
|
|
89
|
+
old proposal, rejection and approval. An existing planned task may instead use
|
|
90
|
+
the combined `workflow-discipline-review`, with the same original proof checks.
|
|
91
|
+
|
|
92
|
+
## 4. Implement through the original guarded entry
|
|
93
|
+
|
|
94
|
+
UI paths and UI content are detected without an opt-out flag. Guarded writes,
|
|
95
|
+
batches, deletes/moves and approved runners check the approved baseline. Source
|
|
96
|
+
checks reject introduced inline styles, undeclared components/style imports,
|
|
97
|
+
unknown tokens, private fields, whole-object dumping and dynamic UI bypasses.
|
|
98
|
+
Framework component libraries require their approved standard controls.
|
|
99
|
+
Put new appearance rules in the approved central style/token files.
|
|
100
|
+
|
|
101
|
+
The frozen source baseline preserves unchanged legacy violations; it cannot
|
|
102
|
+
justify adding more of them. Do not bulk-rewrite old styles to satisfy a new
|
|
103
|
+
rule. Executable lint counterexamples ship with this capability.
|
|
104
|
+
|
|
105
|
+
Open/edit/fill/validation/navigation/cancel must not commit HTTP or browser
|
|
106
|
+
storage writes. Save/delete must be explicit. Render only approved public
|
|
107
|
+
fields; an allowed field need not appear everywhere. Internal IDs, costs,
|
|
108
|
+
protocol metadata and whole API objects must not spill into the UI.
|
|
109
|
+
|
|
110
|
+
## 5. Functional and visual acceptance
|
|
111
|
+
|
|
112
|
+
Run all original approved functional tests first. Obtain
|
|
113
|
+
`continue` → `workflow-design-context` with `baselineDigest` and a fresh
|
|
114
|
+
`attemptNonce`. Its short-lived original W signature binds the original owner,
|
|
115
|
+
task, plan, physical repository, all actual tested input hashes and tests digest.
|
|
116
|
+
No fresh context is issued from a caller's success flag or incomplete tests.
|
|
117
|
+
|
|
118
|
+
Invoke `local design-verify` with the exact original `acceptedPlan`,
|
|
119
|
+
`validationContext`, `baselineDigest`, `repositoryRoot`, `home`, `flowId`,
|
|
120
|
+
approved `baseUrl` and `browserExecutablePath`. The runner launches actual
|
|
121
|
+
Chromium with clean temporary contexts, approved viewport/theme, no external
|
|
122
|
+
network or user profile, and verifies source bytes before and after testing.
|
|
123
|
+
The context and original plan must stay valid; the entire run is bounded to
|
|
124
|
+
five minutes and the original budget. Oversized/incomplete runs do not pass.
|
|
125
|
+
|
|
126
|
+
Each save/delete step requires `readBack`: same-preview HTTP GET or local
|
|
127
|
+
storage JSON, public `before` and `after` projections, and `afterReload`
|
|
128
|
+
assertions for every changed public field (or record absence after deletion).
|
|
129
|
+
Successful commits must change state; failed commits must preserve it. Real
|
|
130
|
+
HTTP status/count, premature success, browser errors, field leakage, layout
|
|
131
|
+
bounds, computed styles, overflow, persistence and refresh are checked.
|
|
132
|
+
For example, HTTP 200 followed by a toast but unchanged read-back is a failure.
|
|
133
|
+
Keep preview requests restricted to isolated authorized fixture data.
|
|
134
|
+
|
|
135
|
+
The result is `needs-visual-confirmation`, never an aesthetic pass. Show real
|
|
136
|
+
screenshots alongside approved mockups, then submit the exact report through
|
|
137
|
+
`continue` → `workflow-design-acceptance` and obtain the original user's actual
|
|
138
|
+
visual answer. Final verdict rechecks original approval, tests and source
|
|
139
|
+
bindings. Later source/test changes require fresh acceptance.
|
|
140
|
+
|
|
141
|
+
## Failure and enforcement boundaries
|
|
142
|
+
|
|
143
|
+
Keep original request IDs, reports, screenshots and failures. An unfinished
|
|
144
|
+
browser attempt remains unknown and blocks repeating preview actions; inspect
|
|
145
|
+
`local design-status`. Do not erase its journal or infer that no effects occurred.
|
|
146
|
+
An expired authority never renews itself. These gates cannot repair or invent
|
|
147
|
+
historical missing process results.
|
|
148
|
+
|
|
149
|
+
Static rules cover the implemented HTML/JS/TS/JSX/Vue/Svelte and CSS subset.
|
|
150
|
+
Unsupported preprocessors/native UI syntax fail explicitly; official research
|
|
151
|
+
does not prove every framework API or language scenario was verified. Browser
|
|
152
|
+
checks and a user's visual judgment remain necessary.
|
|
153
|
+
|
|
154
|
+
SDK checks govern SDK operations. Optional host hooks can add interception, but
|
|
155
|
+
neither a hook nor a generic process runner is an OS sandbox. A generic runner's
|
|
156
|
+
unexpected UI writes are retained as failed/unknown effects, never successful
|
|
157
|
+
delivery. Do not claim that disabled hooks prevent arbitrary IDE or OS edits,
|
|
158
|
+
or that screenshots alone guarantee aesthetics, accessibility or full business
|
|
159
|
+
correctness. Exiting governance preserves the unfinished business and all
|
|
160
|
+
original evidence; it does not count as acceptance.
|
package/REUSE-DISCIPLINE.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Apply these rules when adding to or changing an existing system. Reuse is assessed inside the current accepted repository; external projects, databases, accounts, sessions and keys are outside scope.
|
|
4
4
|
|
|
5
|
+
UI changes also require [DESIGN-GATES.md](DESIGN-GATES.md): approved flow, researched layout and official framework rules, generated mockups and original user confirmation before UI implementation. New style rules check only introduced violations against the frozen original source; do not rewrite existing styles in bulk to satisfy them. Final UI delivery additionally requires real functional and visual acceptance.
|
|
6
|
+
|
|
5
7
|
Snapshot scans stream whole-repository integrity hashes and bound semantic work to 1 MiB per source, 8 MiB of retained source, 512 files and 200,000 syntax nodes. Original target paths and proposed files receive capacity first. A resource-limited non-target source stays in the integrity comparison and is recorded as unresolved; `scanComplete` is false and absent references cannot prove a symbol unused. A target that exceeds a bound returns `ENTROROVE_DISCIPLINE_ANALYSIS_LIMIT` with its path and the exceeded limit. These are execution memory limits, never size-based review exemptions or approval thresholds. Preserve the original flow, signer, scope and queue during recovery. Upgrade with the standard managed installer, then retry the original `scope-connect` input; do not create a replacement task or bypass the host guard with an arbitrary Node environment wrapper.
|
|
6
8
|
|
|
7
9
|
- D-1: Before another business mapping or fact definition, search actual definitions and reuse them. Snapshot planning records the whole repository comparison and AST usage review in `discipline-review.json`; its digest is covered by the signed original snapshot. Typed source-entity field mappings use the existing import/alias resolver to identify the same entity, then compare field/value sets. A conflicting second mapping is refused with the existing file and line. An unresolved semantic relation remains unknown. No overlap threshold is invented.
|
|
@@ -17,4 +19,16 @@ For an independent new file with no same-semantic definition, documentation/comm
|
|
|
17
19
|
|
|
18
20
|
The write entry accepts complete content and also has separate structural action entries; complete-file replacement still passes actual old/new AST and diff checks. Error disposition `needs-human` exists for original proposal/usage/snapshot approval failures. Usage review is a separate hash-bound artifact reused by write declarations. Equivalence baselines reuse the existing frozen GoldenBaseline; original source hashes and actual baseline bytes are independently checked. Moves use explicit guarded action events, independent of Git rename heuristics. Semantic matching is limited to proven same source-entity mapping contexts; generic field-name overlap is never enough.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
## Package replacement and rollback
|
|
23
|
+
|
|
24
|
+
The managed installer compares actual package versions before changing installed files or hooks. It also records a digest of every packaged runtime/document file. Same-version conflicting documentation or runtime identity is refused. Identical replay and restoration of a missing managed document whose remaining hashes match are allowed; an edited or unowned copy is preserved. Old markers without a runtime digest do not prove their historical runtime identity.
|
|
25
|
+
|
|
26
|
+
Guarded writes and forward batches inspect embedded `name`/SemVer in ordinary ZIP, tar and tgz package roots, with bounded structure/CRC/checksum/expanded-length validation in memory. They never extract or execute archives. A filename, timestamp, download label or `force` flag cannot establish freshness. Older versions, different identities, unknown/corrupt/ambiguous metadata, and same-version archives with different bytes refuse before M consumption or physical writes. Same-version development `package.json` changes remain ordinary source edits; unversioned source configuration is not falsely treated as a release archive. Unsupported installer formats refuse replacement rather than claiming a version comparison. Limits: 16 MiB per entry, 64 MiB expanded/input archive, 10,000 entries, 256 KiB metadata; links, ZIP64 and unsupported tar extensions stay unknown.
|
|
27
|
+
|
|
28
|
+
Guarded entries retain immutable, repository/path-bound observations of actual earlier snapshot digests. A candidate equal to a known earlier image cannot use a fresh forward pass to silently restore it. These metadata records preserve original authority/snapshot digests, claim neither completed writes nor rollback authority, and survive snapshot cleanup. They do not reconstruct unobserved historical versions or infer the outcome of an interrupted process. Direct package-manager installation requires the original frozen manifest and lock file in the actual tested source set, no package operand or destination override, and `npm ci` / frozen-lock / immutable mode. Shell-wrapped package replacement is refused. Recognizable `git reset/restore/revert/checkout/switch` commands are refused in approved process entry points before launch; use read-only `git show/log/diff` for diagnosis.
|
|
29
|
+
|
|
30
|
+
Intentional rollback uses the existing `local rollback` protocol: original successful transaction and journal head, independently signed fresh rollback authorization, exact current and restore images, original owner/task/flow/repository/scope, fresh independent lease and M pass, and real signed result reporting. A generic mutation pass, old snapshot, caller approval flag or expired contract cannot substitute. Missing, stale or foreign evidence refuses; retain failures and unknown effects. Atomic compensation of the same failed in-progress batch may restore only its already frozen exact inverse image; it is not discretionary rollback authority, never erases consumption and never converts the failed operation into success.
|
|
31
|
+
|
|
32
|
+
Approved arbitrary scripts are not an OS sandbox. A script that hides a package replacement/rollback can physically change files; post-execution checks then refuse successful evidence, preserve the actual failed comparison and report unknown effects. Do not automatically reverse those unknown effects. These checks do not claim to prevent edits outside SDK or optional host guards.
|
|
33
|
+
|
|
34
|
+
Claims are limited to observed managed actions: undeclared changes can be detected and retained snapshots can support evidenced rollback. Neither static usage nor equivalence proves the business requirement is correct. Report deployment/publication only with actual evidence. Keep first-party files ≤500 lines and functions ≤100 lines; regenerate pinned assets with their generator. Do not create branches/worktrees. Validate the changed units and reverse dependencies; UI changes require light and dark checks.
|
package/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: entrorove
|
|
3
|
-
description: Execute a task selected for Entrorove through explicit SDK operations, preserving
|
|
3
|
+
description: Execute a task selected for Entrorove through explicit SDK operations, preserving scope, approvals and verifiable results. UI work requires a whole-site flow, current layout and official framework research, mockups or prototypes, original user design approval, and functional plus visual acceptance. Hooks are optional.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.67.
|
|
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
|
|
|
@@ -24,6 +24,8 @@ Entrorove applies only to the task for which it is selected; reading these instr
|
|
|
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
25
|
For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass; it keeps unresolved loader files and project configuration read only. 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 approved command and tests without a validator private key. A server-signed `localAcceptance` advances local dependencies; a failed test does not. 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
|
+
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
|
+
**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.
|
|
27
29
|
8. Read [DELIVERY-BOUNDARIES.md](DELIVERY-BOUNDARIES.md) before claiming a delivered stage or showing consumption. A delivered stage requires a clean boundary and uses `delivered-stage-N`. Blocked or unknown work can wait with recovery data intact; it is not a delivered stage. Display consumption only with a measured baseline.
|
|
28
30
|
9. For independent verified completion, submit actual independent execution evidence and run server verification and verdict. For a signed member-local plan, finish all original local steps and requirement checkpoints, then call the returned `workflow-verdict`; only its signed final `localAcceptance` means local acceptance is complete. Describe it as local execution and tests accepted, with independent validation not performed. Never invoke formal V, merge, Swarm or handoff to relabel it. Display the confirmed evaluation acknowledgement and preserve `receiptId`, `requestId`, execution generation and evaluation digest. “已回传” requires `acknowledgement.stored: true`. Missing or failed feedback remains visible. Active, waiting and unknown tasks retain recovery data.
|
|
29
31
|
|
|
@@ -95,7 +95,7 @@ export async function runBlueprintCriterionAttempt(input) {
|
|
|
95
95
|
attemptNonce: attempt.lease.attemptNonce, executionId: `criterion-run-${attempt.attemptId}` });
|
|
96
96
|
if (canonical(activity.authority) !== canonical(attempt.executionAuthority)) fail('criterion running authority differs from its formal server attempt');
|
|
97
97
|
const signer = await loadTaskHandoffSigner(ctx, input.signerConfigPath, attempt.runnerKeyId);
|
|
98
|
-
const execution = await runApprovedPlanObject({ repositoryRoot: ctx.root, executionPlan: attempt.executionPlan,
|
|
98
|
+
const execution = await runApprovedPlanObject({ repositoryRoot: ctx.root, home: ctx.home, flowId: ctx.flowId, executionPlan: attempt.executionPlan,
|
|
99
99
|
approvalPath: input.approvalPath, signerConfigPath: input.signerConfigPath,
|
|
100
100
|
executionDeadlineAt: attempt.expiresAt });
|
|
101
101
|
// The approved runner checks this signed deadline immediately before its actual spawn, after all control/manifest IO.
|
package/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { compareVersions } from './version.mjs';
|
|
8
|
+
import { installPackageIdentity } from './install-identity.mjs';
|
|
8
9
|
import { credentialPath, readCredential, writeCredential } from './local-credential.mjs';
|
|
9
10
|
import { runLocal } from './local-cli.mjs';
|
|
10
11
|
import { runNative } from './native-client.mjs';
|
|
@@ -25,7 +26,7 @@ import { exitIdeSession } from './ide-exit.mjs';
|
|
|
25
26
|
import { installOptions } from './ide-command.mjs';
|
|
26
27
|
import { readCliInput } from './cli-input.mjs';
|
|
27
28
|
const markerName = '.entrorove-managed.json';
|
|
28
|
-
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json', 'IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md', 'DELIVERY-BOUNDARIES.md'];
|
|
29
|
+
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json', 'IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md', 'DELIVERY-BOUNDARIES.md', 'DESIGN-GATES.md'];
|
|
29
30
|
const markerSchema = 'entrorove.managed-install/1.0';
|
|
30
31
|
const agreementSchema = 'entrorove.agreement-acceptance/1.0';
|
|
31
32
|
const installLockWaitMs = 10_000;
|
|
@@ -63,7 +64,7 @@ async function sourceBundle() {
|
|
|
63
64
|
throw new Error('entrorove skill instructions have a mismatched version');
|
|
64
65
|
}
|
|
65
66
|
const hashes = Object.fromEntries(installFiles.map((name) => [name, sha256(content.get(name))]));
|
|
66
|
-
return { manifest, content, hashes };
|
|
67
|
+
return { manifest, content, hashes, packageDigest: await installPackageIdentity(packageDir, packageMetadata) };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function globalInstallPath(home = os.homedir()) { return path.join(home, '.codex', 'skills', 'entrorove'); }
|
|
@@ -135,6 +136,7 @@ async function managedState(directory) {
|
|
|
135
136
|
? Object.keys(marker.files) : [];
|
|
136
137
|
if (marker.schemaVersion !== markerSchema || marker.name !== 'entrorove'
|
|
137
138
|
|| typeof marker.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(marker.version)
|
|
139
|
+
|| marker.packageDigest !== undefined && !/^[a-f0-9]{64}$/.test(marker.packageDigest)
|
|
138
140
|
|| !markerFiles.includes('SKILL.md') || !markerFiles.includes('manifest.json')
|
|
139
141
|
|| markerFiles.some((name) => !installFiles.includes(name) && !(marker.version === '0.1.39' && name === 'DELIVERY-BOUNDARIES.md'))
|
|
140
142
|
|| entries.slice().sort().join(',') !== [...markerFiles, markerName].sort().join(',')) {
|
|
@@ -155,7 +157,7 @@ async function writeStagedInstall(directory, bundle) {
|
|
|
155
157
|
for (const name of installFiles) {
|
|
156
158
|
await fs.writeFile(path.join(directory, name), bundle.content.get(name), { flag: 'wx', mode: 0o600 });
|
|
157
159
|
}
|
|
158
|
-
const marker = { schemaVersion: markerSchema, name: 'entrorove', version: bundle.manifest.version, files: bundle.hashes };
|
|
160
|
+
const marker = { schemaVersion: markerSchema, name: 'entrorove', version: bundle.manifest.version, files: bundle.hashes, packageDigest: bundle.packageDigest };
|
|
159
161
|
await fs.writeFile(path.join(directory, markerName), JSON.stringify(marker) + '\n', { flag: 'wx', mode: 0o600 });
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -225,7 +227,12 @@ async function updateGlobalInstall(target, bundle) {
|
|
|
225
227
|
const previous = await managedState(target);
|
|
226
228
|
if (previous === false) throw new Error('Global entrorove skill is not pristine and managed; refusing overwrite');
|
|
227
229
|
if (previous && compareVersions(previous.version, bundle.manifest.version) > 0) {
|
|
228
|
-
throw new Error('A newer managed entrorove skill is installed; use the latest package');
|
|
230
|
+
throw Object.assign(new Error('A newer managed entrorove skill is installed; use the latest package'), { code: 'ENTROROVE_PACKAGE_DOWNGRADE' });
|
|
231
|
+
}
|
|
232
|
+
if (previous?.version === bundle.manifest.version
|
|
233
|
+
&& (Object.keys(previous.files).some(name => previous.files[name] !== bundle.hashes[name])
|
|
234
|
+
|| previous.packageDigest && previous.packageDigest !== bundle.packageDigest)) {
|
|
235
|
+
throw Object.assign(new Error('Same-version managed package has different bytes; refusing overwrite'), { code: 'ENTROROVE_PACKAGE_VERSION_CONFLICT' });
|
|
229
236
|
}
|
|
230
237
|
if (previous?.version === bundle.manifest.version
|
|
231
238
|
&& installFiles.every((name) => previous.files[name] === bundle.hashes[name])) return false;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { designFail } from './design-policy.mjs';
|
|
2
|
+
|
|
3
|
+
export async function browserAssertion(page, check) {
|
|
4
|
+
const locator = page.locator(check.selector);
|
|
5
|
+
if (check.kind === 'count') return await locator.count() === check.count;
|
|
6
|
+
if (check.kind === 'hidden') return !await locator.isVisible();
|
|
7
|
+
if (await locator.count() !== 1 || !await locator.isVisible()) return false;
|
|
8
|
+
if (check.kind === 'visible') return true;
|
|
9
|
+
if (check.kind === 'text') return (await locator.innerText()).trim() === check.value;
|
|
10
|
+
if (check.kind === 'value') return await locator.inputValue() === check.value;
|
|
11
|
+
if (check.kind === 'style') return await locator.evaluate((node, property) => getComputedStyle(node).getPropertyValue(property), check.property) === check.value;
|
|
12
|
+
if (check.kind === 'bounds') {
|
|
13
|
+
const box = await locator.boundingBox();
|
|
14
|
+
return Boolean(box && ['x', 'y', 'width', 'height'].every(key => Math.abs(box[key] - check.bounds[key]) <= check.bounds.tolerance));
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
export async function browserChecks(page, checks, failures) {
|
|
19
|
+
for (const check of checks) {
|
|
20
|
+
try { if (!await browserAssertion(page, check)) failures.push(`assert-${check.kind}`); }
|
|
21
|
+
catch { failures.push(`assert-${check.kind}`); }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function publicFieldCheck(page, pageContract, forbidden, failures) {
|
|
25
|
+
const fields = await page.locator('[data-entrorove-field]').evaluateAll(nodes => nodes.filter(node => {
|
|
26
|
+
const style = getComputedStyle(node); return style.display !== 'none' && style.visibility !== 'hidden' && node.getClientRects().length;
|
|
27
|
+
}).map(node => node.getAttribute('data-entrorove-field')));
|
|
28
|
+
if (fields.some(name => !pageContract.publicFields.includes(name))) failures.push('public-field-projection');
|
|
29
|
+
const text = await page.locator('body').innerText();
|
|
30
|
+
if ([...pageContract.internalFields, ...forbidden].some(value => value && text.includes(value))) failures.push('private-field-visible');
|
|
31
|
+
}
|
|
32
|
+
export function networkCheck(actual, expected, failures) {
|
|
33
|
+
if (actual.length !== expected.reduce((sum, item) => sum + item.count, 0)
|
|
34
|
+
|| expected.some(item => actual.filter(event => event.method === item.method && event.path === item.path && event.status === item.status).length !== item.count)) failures.push('unexpected-http-mutation');
|
|
35
|
+
}
|
|
36
|
+
export async function installSuccessMonitor(page, successSelector) {
|
|
37
|
+
await page.addInitScript(selector => {
|
|
38
|
+
const events = [], acknowledgements = [], storage = [], original = window.fetch.bind(window);
|
|
39
|
+
Object.defineProperty(window, '__entroroveDesignMonitor', { value: { events, acknowledgements, storage } });
|
|
40
|
+
for (const method of ['setItem', 'removeItem', 'clear']) {
|
|
41
|
+
const apply = Storage.prototype[method];
|
|
42
|
+
Storage.prototype[method] = function(...args) {
|
|
43
|
+
const result = apply.apply(this, args); storage.push({ method, at: performance.now() });
|
|
44
|
+
acknowledgements.push({ at: performance.now(), ok: true }); return result;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (typeof IDBObjectStore !== 'undefined') for (const method of ['add', 'put', 'delete', 'clear']) {
|
|
48
|
+
const apply = IDBObjectStore.prototype[method];
|
|
49
|
+
IDBObjectStore.prototype[method] = function(...args) { storage.push({ method: 'indexed-db-write', at: performance.now() }); return apply.apply(this, args); };
|
|
50
|
+
}
|
|
51
|
+
window.fetch = async (...args) => {
|
|
52
|
+
const response = await original(...args);
|
|
53
|
+
const method = String(args[1]?.method ?? (args[0] instanceof Request ? args[0].method : 'GET')).toUpperCase();
|
|
54
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) acknowledgements.push({ at: performance.now(), ok: response.ok });
|
|
55
|
+
return response;
|
|
56
|
+
};
|
|
57
|
+
const open = XMLHttpRequest.prototype.open;
|
|
58
|
+
XMLHttpRequest.prototype.open = function(method, ...args) {
|
|
59
|
+
const mutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method).toUpperCase());
|
|
60
|
+
this.addEventListener('readystatechange', () => {
|
|
61
|
+
if (mutation && this.readyState === 4) acknowledgements.push({ at: performance.now(), ok: this.status >= 200 && this.status < 300 });
|
|
62
|
+
});
|
|
63
|
+
return open.call(this, method, ...args);
|
|
64
|
+
};
|
|
65
|
+
let visible = false;
|
|
66
|
+
const observe = () => {
|
|
67
|
+
const node = document.querySelector(selector), style = node && getComputedStyle(node);
|
|
68
|
+
const next = Boolean(node && style.display !== 'none' && style.visibility !== 'hidden' && node.getClientRects().length);
|
|
69
|
+
if (next && !visible) events.push({ at: performance.now(), successfulAcknowledgement: acknowledgements.some(item => item.ok) });
|
|
70
|
+
visible = next;
|
|
71
|
+
};
|
|
72
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
73
|
+
new MutationObserver(observe).observe(document.documentElement, { attributes: true, childList: true, subtree: true, characterData: true }); observe();
|
|
74
|
+
});
|
|
75
|
+
}, successSelector);
|
|
76
|
+
}
|
|
77
|
+
export async function monitorSuccess(page, failures, canSucceed) {
|
|
78
|
+
const observed = await page.evaluate(() => window.__entroroveDesignMonitor?.events ?? []);
|
|
79
|
+
if (!canSucceed && observed.length || observed.some(event => !event.successfulAcknowledgement)) failures.push('premature-success-message');
|
|
80
|
+
}
|
|
81
|
+
export function previewUrl(value) {
|
|
82
|
+
let url; try { url = new URL(value); } catch { designFail('PREVIEW', 'Start the approved local preview before browser acceptance'); }
|
|
83
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password
|
|
84
|
+
|| url.pathname !== '/' || url.search || url.hash) designFail('PREVIEW', 'Use an isolated HTTP preview on 127.0.0.1 with a specific port, without credentials');
|
|
85
|
+
return url;
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { repositoryIntegrityFile } from './local-discipline-repository.mjs';
|
|
4
|
+
import { designFail } from './design-policy.mjs';
|
|
5
|
+
|
|
6
|
+
export async function readDesignBrowserIdentity(input) {
|
|
7
|
+
if (typeof input.browserExecutablePath !== 'string' || !path.isAbsolute(input.browserExecutablePath)) designFail('BROWSER', 'Configure a local Chromium executable before design confirmation');
|
|
8
|
+
const executablePath = await fs.realpath(input.browserExecutablePath), stat = await fs.stat(executablePath);
|
|
9
|
+
if (!stat.isFile() || !(stat.mode & 0o111) || stat.size > 536_870_912
|
|
10
|
+
|| !/^(?:Google Chrome|Chromium|chrome|chromium|chromium-browser|google-chrome|chrome-headless-shell)$/.test(path.basename(executablePath))) designFail('BROWSER', 'Browser checks require an actual configured Chromium executable, without arbitrary command arguments');
|
|
11
|
+
const { sha256 } = await repositoryIntegrityFile(path.dirname(executablePath), path.basename(executablePath));
|
|
12
|
+
return { executablePath, sha256 };
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { canonical } from './local-integrity.mjs';
|
|
2
|
+
|
|
3
|
+
function projected(value, pointer = '') {
|
|
4
|
+
for (const part of pointer.split('/').slice(1).map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'))) {
|
|
5
|
+
if (!value || !Object.hasOwn(value, part)) return undefined;
|
|
6
|
+
value = value[part];
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export async function checkBrowserState(page, proof, phase, fields, baseUrl, failures) {
|
|
11
|
+
const expected = proof[phase]; let actual;
|
|
12
|
+
try {
|
|
13
|
+
if (proof.kind === 'http') {
|
|
14
|
+
const response = await page.request.get(new URL(proof.path, baseUrl).href, { maxRedirects: 0, timeout: 5000 });
|
|
15
|
+
if (response.status() !== proof[phase === 'before' ? 'statusBefore' : 'statusAfter']) failures.push('read-back-http-status');
|
|
16
|
+
if (expected !== null || response.status() >= 200 && response.status() < 300) {
|
|
17
|
+
const bytes = await response.body();
|
|
18
|
+
if (bytes.length > 1_048_576) throw new Error();
|
|
19
|
+
actual = projected(JSON.parse(bytes.toString('utf8')), proof.jsonPointer);
|
|
20
|
+
} else actual = null;
|
|
21
|
+
await response.dispose();
|
|
22
|
+
} else {
|
|
23
|
+
const value = await page.evaluate(key => localStorage.getItem(key), proof.key);
|
|
24
|
+
actual = value === null ? null : projected(JSON.parse(value), proof.jsonPointer);
|
|
25
|
+
}
|
|
26
|
+
if (expected === null) { if (actual !== null) failures.push('read-back-state'); }
|
|
27
|
+
else if (!actual || typeof actual !== 'object' || Array.isArray(actual)
|
|
28
|
+
|| Object.keys(actual).some(key => !fields.includes(key))
|
|
29
|
+
|| Object.keys(expected).some(key => canonical(actual[key] ?? null) !== canonical(expected[key]))) failures.push('read-back-state');
|
|
30
|
+
} catch { failures.push('read-back-unavailable'); }
|
|
31
|
+
}
|
|
32
|
+
export async function beginBrowserAction(page) {
|
|
33
|
+
await page.evaluate(() => {
|
|
34
|
+
const monitor = window.__entroroveDesignMonitor;
|
|
35
|
+
monitor.storage.length = 0; monitor.acknowledgements.length = 0; monitor.events.length = 0;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function checkNonCommitStorage(page, commits, failures) {
|
|
39
|
+
const count = await page.evaluate(() => window.__entroroveDesignMonitor.storage.length);
|
|
40
|
+
if (!commits && count) failures.push('unexpected-browser-storage-write');
|
|
41
|
+
}
|