@entrorove/skill 0.1.62 → 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 +30 -2
- package/CHANGELOG.md +25 -0
- package/DELIVERY-BOUNDARIES.md +2 -0
- package/DESIGN-GATES.md +160 -0
- package/IDE-EXECUTION.md +32 -3
- package/REUSE-DISCIPLINE.md +16 -2
- package/SKILL.md +9 -3
- package/blueprint-criterion-runner.mjs +1 -1
- package/cli-input.mjs +22 -0
- package/cli.mjs +13 -12
- package/cloud-task-client.mjs +10 -2
- package/cloud-task-images.mjs +39 -0
- 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/ide-exit.mjs +49 -9
- package/ide-hook.mjs +2 -2
- package/install-identity.mjs +16 -0
- package/local-cli.mjs +3 -1
- package/local-contracts.mjs +6 -1
- 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 +17 -2
- package/local-discipline-repository.mjs +11 -5
- package/local-error.mjs +18 -1
- package/local-execution.mjs +2 -1
- package/local-finder-plist.mjs +87 -0
- package/local-finder-store.mjs +115 -0
- package/local-mutation-authority.mjs +13 -0
- package/local-mutation-completion-evidence.mjs +83 -0
- package/local-mutation-consumption.mjs +33 -10
- package/local-mutation-environment-observation.mjs +85 -0
- package/local-mutation-finder-observation.mjs +58 -0
- package/local-mutation-wire.mjs +26 -8
- package/local-runner-discipline.mjs +17 -3
- package/local-scope-files.mjs +3 -1
- package/local-scope-font.mjs +45 -0
- package/local-scope-repository.mjs +16 -3
- package/local-scope-zip.mjs +114 -0
- package/manifest.json +1 -1
- package/member-producer-repair-authority.mjs +48 -0
- package/model-images.d.mts +9 -0
- package/model-images.mjs +87 -0
- package/mutation-challenge-refresh.mjs +51 -0
- package/mutation-client.mjs +42 -24
- package/mutation-connection-state.mjs +17 -0
- package/package-archive.mjs +54 -0
- package/package-replacement.mjs +48 -0
- package/package.json +50 -6
- package/release-state.json +87 -45
- package/release.mjs +11 -1
- package/releases.json +30 -0
- package/remote-client.mjs +1 -1
- package/remote-transport.mjs +48 -16
- package/reuse-discipline-release.mjs +3 -1
- package/rollback-history.mjs +51 -0
- package/rollback-runner.mjs +77 -0
- package/scope-amendment-client.mjs +1 -1
- package/scope-amendment-consumption.mjs +32 -0
- package/scope-amendment-mutation.mjs +2 -3
- package/scope-amendment-observer.mjs +2 -1
- package/scope-amendment-remote.mjs +12 -2
- package/scope-amendment-retirement.mjs +54 -0
- package/scope-amendment-state.mjs +7 -4
- package/task-entry-client.mjs +1 -0
- package/version.mjs +20 -3
- package/workflow-local-runner.mjs +9 -0
- package/workflow-observation-client.mjs +11 -0
- package/workflow-observation-connection.mjs +2 -1
- package/workflow-observation-remote.mjs +26 -1
- package/workflow-observation-wire.mjs +6 -3
package/design-store.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inflateSync } from 'node:zlib';
|
|
4
|
+
import { mutationRoot14, mutationStateRoot14, mutationTarget14, mutationRead14, privateMutationDirectory14, readMutationJson14,
|
|
5
|
+
writeMutationFile14, atomicMutationJson14, mutationId14 } from './local-mutation-files.mjs';
|
|
6
|
+
import { regular } from './local-task-handoff-storage.mjs';
|
|
7
|
+
import { sha256, canonical } from './local-integrity.mjs';
|
|
8
|
+
import { designFail, validateDesignSite } from './design-policy.mjs';
|
|
9
|
+
|
|
10
|
+
export async function designDirectory(input) {
|
|
11
|
+
const root = await mutationRoot14(input.repositoryRoot), state = await mutationStateRoot14(input);
|
|
12
|
+
mutationId14(input.flowId, 'flowId'); mutationId14(input.designId, 'designId');
|
|
13
|
+
if (!Number.isSafeInteger(input.revision) || input.revision < 1) designFail('INVALID', 'A positive design revision is required');
|
|
14
|
+
const directory = path.join(state, 'design', sha256(root).slice(0, 32), input.flowId, input.designId, `r${input.revision}`);
|
|
15
|
+
await privateMutationDirectory14(directory, { privateFrom: state });
|
|
16
|
+
return directory;
|
|
17
|
+
}
|
|
18
|
+
export async function designSave(directory, name, value) {
|
|
19
|
+
const file = path.join(directory, name);
|
|
20
|
+
await atomicMutationJson14(file, value);
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
export async function designLoad(directory, name) { return readMutationJson14(path.join(directory, name)); }
|
|
24
|
+
export async function designArtifact(file, expected) {
|
|
25
|
+
const { bytes, stat } = await regular(file, 8_388_608);
|
|
26
|
+
if (stat.nlink !== 1 || stat.mode & 0o111 || expected && (sha256(bytes) !== expected.sha256 || bytes.length !== expected.bytes)) designFail('ARTIFACT_CHANGED', 'Design evidence must retain its exact non-executable, single-link bytes');
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
export async function designCopy(directory, name, file) {
|
|
30
|
+
const bytes = await designArtifact(file), target = path.join(directory, name);
|
|
31
|
+
await writeMutationFile14(target, bytes);
|
|
32
|
+
return { path: target, sha256: sha256(bytes), bytes: bytes.length };
|
|
33
|
+
}
|
|
34
|
+
function crc32(bytes) {
|
|
35
|
+
let crc = 0xffffffff;
|
|
36
|
+
for (const byte of bytes) { crc ^= byte; for (let i = 0; i < 8; i++) crc = crc & 1 ? 0xedb88320 ^ crc >>> 1 : crc >>> 1; }
|
|
37
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
export function designPng(bytes) {
|
|
40
|
+
if (!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) designFail('IMAGE', 'Generated design images must be complete PNG files');
|
|
41
|
+
let offset = 8, header, end = false, data = [];
|
|
42
|
+
while (offset + 12 <= bytes.length) {
|
|
43
|
+
const length = bytes.readUInt32BE(offset), type = bytes.toString('ascii', offset + 4, offset + 8), next = offset + length + 12;
|
|
44
|
+
if (next > bytes.length || crc32(bytes.subarray(offset + 4, next - 4)) !== bytes.readUInt32BE(next - 4)) designFail('IMAGE', 'PNG bounds or checksum failed');
|
|
45
|
+
if (offset === 8 && type !== 'IHDR' || type === 'IHDR' && (header || length !== 13)) designFail('IMAGE', 'PNG header is invalid');
|
|
46
|
+
if (type === 'IHDR') header = bytes.subarray(offset + 8, next - 4);
|
|
47
|
+
else if (type === 'IDAT') data.push(bytes.subarray(offset + 8, next - 4));
|
|
48
|
+
else if (type === 'IEND') { end = length === 0 && next === bytes.length; break; }
|
|
49
|
+
else if (/^[A-Z]/.test(type) && type !== 'PLTE') designFail('IMAGE', 'Unsupported critical PNG chunk');
|
|
50
|
+
offset = next;
|
|
51
|
+
}
|
|
52
|
+
const width = header?.readUInt32BE(0), height = header?.readUInt32BE(4), channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[header?.[9]];
|
|
53
|
+
if (!end || !width || !height || width > 4096 || height > 4096 || header[8] !== 8 || !channels
|
|
54
|
+
|| header[10] || header[11] || header[12] || !data.length) designFail('IMAGE', 'Unsupported or incomplete generated PNG; use non-interlaced 8-bit PNG');
|
|
55
|
+
let expanded;
|
|
56
|
+
try { expanded = inflateSync(Buffer.concat(data), { maxOutputLength: 68_000_000 }); }
|
|
57
|
+
catch { designFail('IMAGE', 'Generated PNG data cannot be decoded'); }
|
|
58
|
+
const row = width * channels + 1;
|
|
59
|
+
if (expanded.length !== row * height || Array.from({ length: height }, (_, i) => expanded[i * row]).some(filter => filter > 4)) designFail('IMAGE', 'Generated PNG scanlines are invalid');
|
|
60
|
+
return { width, height };
|
|
61
|
+
}
|
|
62
|
+
export function designMermaid(raw) {
|
|
63
|
+
const site = validateDesignSite(raw), ids = new Map(site.pages.map((page, index) => [page.id, `p${index}`]));
|
|
64
|
+
const label = value => `"${value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/\|/g, '|').replace(/[\r\n]/g, ' ')}"`;
|
|
65
|
+
return ['flowchart TD', ...site.pages.map(page => ` ${ids.get(page.id)}[${label(`${page.title} (${page.id}) · ${page.roles.join('/')} · ${page.states.join('/')}`)}]`),
|
|
66
|
+
...site.edges.map(edge => ` ${ids.get(edge.from)} -->|${label(`${edge.id}: ${edge.label} (${edge.kind})`)}| ${ids.get(edge.to)}`)].join('\n') + '\n';
|
|
67
|
+
}
|
|
68
|
+
export async function assertDesignArtifacts(policy, input) {
|
|
69
|
+
const directory = input ? await designDirectory({ ...input, designId: policy.designId, revision: policy.revision }) : path.dirname(policy.flow.path);
|
|
70
|
+
if (policy.flow.path !== path.join(directory, 'site-flow.mmd') || policy.mockups.some((mockup, index) =>
|
|
71
|
+
mockup.image.path !== path.join(directory, `mockup-${index}.png`)
|
|
72
|
+
|| mockup.generation.receipt.path !== path.join(directory, `generation-${index}.json`))) designFail('ARTIFACT_LOCATION', 'Design evidence must remain in its original repository, flow and revision storage');
|
|
73
|
+
const diagram = await designArtifact(policy.flow.path, policy.flow);
|
|
74
|
+
if (diagram.toString('utf8') !== designMermaid(policy.site)) designFail('FLOW', 'The displayed whole-site diagram differs from its page/action inventory');
|
|
75
|
+
for (const [index, source] of policy.sourceBaseline.entries()) if (source.original) {
|
|
76
|
+
if (source.original.path !== path.join(directory, `source-${index}.original`)) designFail('ARTIFACT_LOCATION', 'Original UI bytes must remain in their approved design revision');
|
|
77
|
+
await designArtifact(source.original.path, source.original);
|
|
78
|
+
}
|
|
79
|
+
for (const mockup of policy.mockups) {
|
|
80
|
+
designPng(await designArtifact(mockup.image.path, mockup.image));
|
|
81
|
+
let receipt;
|
|
82
|
+
try { receipt = JSON.parse((await designArtifact(mockup.generation.receipt.path, mockup.generation.receipt)).toString('utf8')); }
|
|
83
|
+
catch { designFail('GENERATION', 'The original image generation record is unavailable or invalid'); }
|
|
84
|
+
if (Object.keys(receipt).sort().join('|') !== 'generatedAt|imageSha256|promptSha256|requestId|schemaVersion|tool') designFail('GENERATION', 'Image generation records contain only tool identity, prompt/image hashes and the actual generation time');
|
|
85
|
+
if (receipt.schemaVersion !== 'entrorove.image-generation-record/1.0' || receipt.tool !== mockup.generation.tool
|
|
86
|
+
|| receipt.requestId !== mockup.generation.requestId || receipt.promptSha256 !== mockup.generation.promptSha256
|
|
87
|
+
|| receipt.imageSha256 !== mockup.image.sha256 || !Number.isFinite(Date.parse(receipt.generatedAt))
|
|
88
|
+
|| Date.parse(receipt.generatedAt) < Date.parse(policy.researchRecordedAt)
|
|
89
|
+
|| Date.parse(receipt.generatedAt) > Date.parse(policy.mockupsRecordedAt)) designFail('GENERATION', 'Record the actual image/design tool result after research, bound to these image bytes');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function captureDesignSourceBaseline(input, directory, names) {
|
|
93
|
+
const baseline = [];
|
|
94
|
+
for (const [index, name] of names.entries()) {
|
|
95
|
+
const file = await mutationTarget14(input.repositoryRoot, name, true);
|
|
96
|
+
if (!file.exists) { baseline.push({ path: name, original: null }); continue; }
|
|
97
|
+
const { content } = await mutationRead14(input, input.repositoryRoot, name), target = path.join(directory, `source-${index}.original`);
|
|
98
|
+
if (content.length > 2_097_152) designFail('LINT_LIMIT', 'Split approved UI source inspection before capturing a source larger than 2 MiB');
|
|
99
|
+
await writeMutationFile14(target, content);
|
|
100
|
+
baseline.push({ path: name, original: { path: target, sha256: sha256(content), bytes: content.length } });
|
|
101
|
+
}
|
|
102
|
+
return baseline;
|
|
103
|
+
}
|
|
104
|
+
export async function designOriginalSource(policy, name) {
|
|
105
|
+
const source = policy.sourceBaseline.find(file => file.path === name);
|
|
106
|
+
if (!source) designFail('BASELINE', 'UI source has no original approved baseline');
|
|
107
|
+
return source.original ? (await designArtifact(source.original.path, source.original)).toString('utf8') : '';
|
|
108
|
+
}
|
|
109
|
+
export async function designSourceManifest(input, policy) {
|
|
110
|
+
const { mutationRead14 } = await import('./local-mutation-files.mjs');
|
|
111
|
+
const root = await mutationRoot14(input.repositoryRoot), files = [];
|
|
112
|
+
const paths = new Set([...policy.rules.verificationPaths, ...policy.frameworks.flatMap(f => [f.packagePath, f.lockPath])]);
|
|
113
|
+
for (const name of [...paths].sort()) files.push({ path: name, sha256: (await mutationRead14(input, root, name)).sha256 });
|
|
114
|
+
return files;
|
|
115
|
+
}
|
|
116
|
+
export const sameDesignBytes = (a, b) => canonical(a) === canonical(b);
|
package/design-ui.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
const uiExtension = /\.(?:[jt]sx|vue|svelte|html?|xhtml|css|scss|sass|less|styl|wxml|wxss|axml|acss)$/i;
|
|
4
|
+
export const designUiAsset = name => /\.(?:png|jpe?g|webp|avif|gif|ico|woff2?|ttf|otf)$/i.test(name);
|
|
5
|
+
const markup = /<\/?(?:html|head|body|main|div|section|form|button|input|table|dialog|template|script|style|[A-Z][A-Za-z0-9]*)\b[^>]*>/;
|
|
6
|
+
function scriptUi(source, name) {
|
|
7
|
+
const kind = /\.[cm]?ts$/i.test(name) ? ts.ScriptKind.TS : ts.ScriptKind.TSX;
|
|
8
|
+
const tree = ts.createSourceFile(name, source, ts.ScriptTarget.Latest, true, kind);
|
|
9
|
+
let found = false;
|
|
10
|
+
const walk = node => {
|
|
11
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)
|
|
12
|
+
|| ts.isPropertyAccessExpression(node) && ['createElement', 'createRoot', 'innerHTML', 'outerHTML', 'insertAdjacentHTML',
|
|
13
|
+
'querySelector', 'querySelectorAll', 'getElementById', 'style'].includes(node.name.text)
|
|
14
|
+
|| (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node)) && markup.test(node.text)) found = true;
|
|
15
|
+
if (!found) ts.forEachChild(node, walk);
|
|
16
|
+
};
|
|
17
|
+
walk(tree); return found;
|
|
18
|
+
}
|
|
19
|
+
export function designUiCandidate(name, before = '', after = '') {
|
|
20
|
+
if (uiExtension.test(name) || /\.svg$/i.test(name)
|
|
21
|
+
|| /(?:^|\/)(?:public|assets|images|icons)\//i.test(name) && designUiAsset(name)) return true;
|
|
22
|
+
if (/\.(?:[cm]?[jt]s)$/i.test(name)) return Boolean(before || after) && (scriptUi(before, name) || scriptUi(after, name));
|
|
23
|
+
return /\.(?:php|txt)$/i.test(name) && markup.test(`${before}\n${after}`);
|
|
24
|
+
}
|
package/ide-exit.mjs
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
5
|
+
import { ideDirectory, ideDigest, ideError, ideGuardBinding, readIdeJson, writeIdeJson, verifiedIdeGuards } from './ide-session.mjs';
|
|
4
6
|
|
|
5
7
|
const exactExit = /^(?:(?:请)?(?:停止使用|退出)\s*\$?entrorove|(?:stop using|exit)\s+\$?entrorove)[.!。!]?$/iu;
|
|
6
8
|
const naturalExit = /(?:^|[,,。;;!!\n])\s*(?:从现在开始|接下来|今后)?\s*(?:停止使用|不再使用|不使用)\s*(?:entrorove(?:\s*技能)?|(?:这个|该)?技能)\s*(?:(?:来)?(?:执行|处理|完成|继续)[^,,。;;!!\n]{0,40}(?:任务|工作))?[。!!]?\s*$/iu;
|
|
9
|
+
const englishExit = /^(?:please[,:]?\s+)?(?:from\s+now\s+on[,:]?\s+)?(?:stop\s+using|do\s+not\s+use|no\s+longer\s+use|I\s+will\s+no\s+longer\s+use)\s+(?:\$?entrorove(?:\s+skill)?|(?:this|the)\s+skill)(?:\s+(?:for|to\s+(?:execute|perform|handle|complete|continue))\s+(?:the\s+)?(?:remaining\s+(?:tasks|work)|rest\s+of\s+the\s+(?:tasks|work)))?(?:\s+(?:from\s+now\s+on|anymore|any\s+longer))?[.!]?$/iu;
|
|
10
|
+
export const IDE_EXIT_DECISIONS = Object.freeze({
|
|
11
|
+
zh: '用户明确决定:错误问题发送给技能修改人员,从现在开始不使用技能来执行剩余任务。仅结束本轮Entrorove治理;保留源码、原清单顺序、审批和全部失败记录,不代表剩余业务任务取消或完成。',
|
|
12
|
+
en: 'The user has explicitly decided: report the errors to the skill maintainers and stop using the skill for the remaining tasks from now on. End only the current Entrorove governance session; preserve the source code, the original task-list order, approvals, and all failure records. This does not mean that the remaining business tasks are cancelled or completed.',
|
|
13
|
+
});
|
|
14
|
+
const decisionKey = text => text.normalize('NFC').replace(/\s+/gu, '').toLowerCase();
|
|
15
|
+
const decisionKeys = new Set(Object.values(IDE_EXIT_DECISIONS).map(decisionKey));
|
|
16
|
+
const quotedOrConditional = /[`"'“”‘’「」『』<>??]|如果|假如|假设|示例|截图|引用|\b(?:if|unless|whether|suppose|assuming|example|screenshot|quoted?|translate|translation)\b/iu;
|
|
7
17
|
export function explicitIdeExit(prompt) {
|
|
8
|
-
if (
|
|
9
|
-
|
|
18
|
+
if (typeof prompt !== 'string') return false;
|
|
19
|
+
const text = prompt.trim().replace(/\bdon['’]t\b/giu, 'do not');
|
|
20
|
+
if (quotedOrConditional.test(text)) return false;
|
|
21
|
+
return exactExit.test(text) || naturalExit.test(text) || englishExit.test(text) || decisionKeys.has(decisionKey(text));
|
|
22
|
+
}
|
|
23
|
+
export function ideExitNotice(prompt) {
|
|
24
|
+
return /\p{Script=Han}/u.test(prompt)
|
|
25
|
+
? '用户明确退出本轮 Entrorove 管理;源码、原清单顺序、审批和全部失败记录均保留。剩余业务未取消或完成,未声明验收通过或评分为成功。'
|
|
26
|
+
: 'The user explicitly ended this Entrorove governance session. The source code, original task-list order, approvals, and all failure records are preserved. Remaining business work is neither cancelled nor completed, and no acceptance or success score is claimed.';
|
|
10
27
|
}
|
|
11
28
|
export function markIdeExited(state, source) {
|
|
12
29
|
if (state.phase !== 'user-exited') state.exitPhase = state.phase;
|
|
13
30
|
state.active = false; state.phase = 'user-exited';
|
|
14
31
|
state.exitDecision = { source, observedAt: new Date().toISOString() };
|
|
15
32
|
}
|
|
16
|
-
|
|
33
|
+
async function exitState(file, sessionId, cwd) {
|
|
34
|
+
let state;
|
|
35
|
+
try { state = await readIdeJson(file); }
|
|
36
|
+
catch { throw ideError('ENTROROVE_IDE_STORE', 'Current IDE exit state is not a readable private record'); }
|
|
37
|
+
if (state) {
|
|
38
|
+
if (state.schemaVersion !== 'entrorove.ide-session/1.0' || state.sessionId !== sessionId
|
|
39
|
+
|| !path.isAbsolute(state.repositoryRoot ?? '')) throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit cannot replace an invalid existing IDE session');
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await fs.lstat(file);
|
|
44
|
+
throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit cannot replace an invalid existing IDE session');
|
|
45
|
+
} catch (cause) { if (cause.code !== 'ENOENT') throw cause; }
|
|
46
|
+
if (!path.isAbsolute(cwd ?? '')) throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit requires the current physical repository directory');
|
|
47
|
+
let repositoryRoot;
|
|
48
|
+
try { repositoryRoot = await fs.realpath(cwd); if (!(await fs.stat(repositoryRoot)).isDirectory()) throw new Error(); }
|
|
49
|
+
catch { throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit requires the current physical repository directory'); }
|
|
50
|
+
return { schemaVersion: 'entrorove.ide-session/1.0', sessionId, repositoryRoot, active: false,
|
|
51
|
+
phase: 'entry', seen: [], blockStreak: 0 };
|
|
52
|
+
}
|
|
53
|
+
export async function exitIdeSession({ home = os.homedir(), environment = process.env, cwd = process.cwd() } = {}) {
|
|
17
54
|
const { host, sessionId } = ideGuardBinding(environment);
|
|
18
|
-
if (!Object.hasOwn(verifiedIdeGuards, host) || !sessionId
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
if (!Object.hasOwn(verifiedIdeGuards, host) || !sessionId || !/^[A-Za-z0-9_-]{1,200}$/u.test(sessionId)) {
|
|
56
|
+
throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit requires the current verified IDE session identity');
|
|
57
|
+
}
|
|
58
|
+
const directory = await ideDirectory(home, true), file = path.join(directory, `session-${ideDigest(sessionId)}.json`);
|
|
59
|
+
return withStateLock(file, async () => {
|
|
60
|
+
const current = await exitState(file, sessionId, cwd);
|
|
22
61
|
markIdeExited(current, 'cli');
|
|
62
|
+
await writeIdeJson(file, { ...current, observedAt: new Date().toISOString() });
|
|
23
63
|
return { schemaVersion: 'entrorove.ide-exit/1.0', status: 'user-exited', sessionId,
|
|
24
64
|
offline: true, network: false, hook: 'not-required', originalFlowPreserved: true, claimsCompletion: false };
|
|
25
|
-
}
|
|
65
|
+
});
|
|
26
66
|
}
|
package/ide-hook.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { parseIdeResults, consumeIdeResults } from './ide-results.mjs';
|
|
|
13
13
|
import { verifyIdeLocalAcceptance } from './ide-local-acceptance.mjs';
|
|
14
14
|
import { registerIdeRequirement, ideQueueContext, unclassifiedIdeRequirements,
|
|
15
15
|
advanceIdeQueue, checkQueuedStart, unfinishedIdeQueue } from './ide-queue.mjs';
|
|
16
|
-
import { explicitIdeExit, markIdeExited } from './ide-exit.mjs';
|
|
16
|
+
import { explicitIdeExit, ideExitNotice, markIdeExited } from './ide-exit.mjs';
|
|
17
17
|
import { appliesIdeHook, hasIdeHookSelection, selectsIdeHook, ideUserPrompt as userPrompt } from './ide-selection.mjs';
|
|
18
18
|
|
|
19
19
|
const output = (event, extra = {}) => ({ hookSpecificOutput: { hookEventName: event, ...extra } });
|
|
@@ -251,7 +251,7 @@ export async function runIdeHook(event, { home } = {}) {
|
|
|
251
251
|
if (name === 'PreToolUse') state.enforcement = 'enforced';
|
|
252
252
|
if (name === 'UserPromptSubmit' && explicitIdeExit(userPrompt(event))) {
|
|
253
253
|
markIdeExited(state, 'user-prompt');
|
|
254
|
-
return output(name, { additionalContext:
|
|
254
|
+
return output(name, { additionalContext: ideExitNotice(userPrompt(event)) });
|
|
255
255
|
}
|
|
256
256
|
if (selected) {
|
|
257
257
|
state.active = true;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export async function installPackageIdentity(directory, metadata) {
|
|
6
|
+
const files = [...new Set(['package.json', ...metadata.files])].sort(), digest = createHash('sha256');
|
|
7
|
+
for (const name of files) {
|
|
8
|
+
if (typeof name !== 'string' || path.isAbsolute(name) || name.includes('\\')
|
|
9
|
+
|| name.split('/').some(part => !part || part === '.' || part === '..')) throw new Error('Invalid packaged source inventory');
|
|
10
|
+
const file = path.join(directory, name), stat = await fs.lstat(file);
|
|
11
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Packaged source identity requires regular files');
|
|
12
|
+
const bytes = await fs.readFile(file);
|
|
13
|
+
digest.update(`${name}\0${createHash('sha256').update(bytes).digest('hex')}\n`);
|
|
14
|
+
}
|
|
15
|
+
return digest.digest('hex');
|
|
16
|
+
}
|
package/local-cli.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { ARCHGUARD_LOCAL_OPERATIONS } from './local-archguard-operations.mjs';
|
|
|
25
25
|
import { LOCAL_BUDGET_HANDOFF_OPERATIONS } from './local-budget-handoff.mjs';
|
|
26
26
|
import { LOCAL_CHAIN_OPERATIONS } from './local-chain-operations.mjs';
|
|
27
27
|
import { DISCIPLINE_LOCAL_OPERATIONS } from './local-discipline-operations.mjs';
|
|
28
|
+
import { DESIGN_LOCAL_OPERATIONS } from './design-operations.mjs';
|
|
28
29
|
import { assertIdeExecution, settleBreakGlassSession } from './ide-session.mjs';
|
|
29
30
|
import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
|
|
30
31
|
import { localErrorEnvelope } from './local-error.mjs';
|
|
@@ -47,6 +48,7 @@ const operations = {
|
|
|
47
48
|
'mutation-consumption-attest': attestMutationConsumption14,
|
|
48
49
|
'mutation-delegate-prepare': preparePreservedMutationSigner,
|
|
49
50
|
...DISCIPLINE_LOCAL_OPERATIONS,
|
|
51
|
+
...DESIGN_LOCAL_OPERATIONS,
|
|
50
52
|
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
|
|
51
53
|
'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
|
|
52
54
|
'budget-init': budgetInit, 'budget-context': budgetContext, 'budget-read': budgetRead, 'budget-status': budgetStatus,
|
|
@@ -87,7 +89,7 @@ function capabilitiesFor(group) {
|
|
|
87
89
|
async function checkedLocalInput(operation, input) {
|
|
88
90
|
try {
|
|
89
91
|
const validated = validateLocalInput(operation, input);
|
|
90
|
-
if (['mutation-snapshot-attest', 'mutation-consumption-attest', 'workflow-run-step', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write', 'guarded-delete', 'guarded-move', 'discipline-review', 'discipline-check'].includes(operation)) await hydrateCloudMutationAuthority(input.home, new URL(apiUrl).origin);
|
|
92
|
+
if (['mutation-snapshot-attest', 'mutation-consumption-attest', 'workflow-run-step', 'design-verify', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write', 'guarded-delete', 'guarded-move', 'discipline-review', 'discipline-check'].includes(operation)) await hydrateCloudMutationAuthority(input.home, new URL(apiUrl).origin);
|
|
91
93
|
return validated;
|
|
92
94
|
}
|
|
93
95
|
catch (cause) {
|
package/local-contracts.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { WORKFLOW_LOCAL_OPERATION_SCHEMA } from './workflow-local-contracts.mjs'
|
|
|
9
9
|
import { HANDOFF_WRITE_PASS_SCHEMA, HANDOFF_PROVISIONAL_LEASE_SCHEMA, HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA } from './local-task-handoff-contracts.mjs';
|
|
10
10
|
import { LOCAL_CHAIN_OPERATION_SCHEMAS } from './local-chain-contracts.mjs';
|
|
11
11
|
import { DISCIPLINE_DECLARATION_SCHEMA } from './local-discipline-contracts.mjs';
|
|
12
|
+
import { DESIGN_LOCAL_OPERATION_SCHEMAS } from './design-local-contracts.mjs';
|
|
12
13
|
import { array, binding, change, confirmation, enumeration, evidence, evidenceArray, executionContext, executionPlan,
|
|
13
14
|
formula, formulaDefinitions, formulaEntry, decimalValues, identifier, integer, ledgerIdentifier, modes, nullable, object, paths, record, relativePath, sha256, text } from './local-contract-types.mjs';
|
|
14
15
|
|
|
@@ -38,7 +39,10 @@ const write = { ...step, taskId: identifier, generation: integer(1), executionAt
|
|
|
38
39
|
add('gate-verify', write, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']);
|
|
39
40
|
add('discipline-review|discipline-check', write, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']);
|
|
40
41
|
add('guarded-delete|guarded-move', { ...write, declaration: DISCIPLINE_DECLARATION_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath', 'declaration']);
|
|
41
|
-
add('guarded-write', { ...write, content: {},
|
|
42
|
+
add('guarded-write', { ...write, content: {}, contentBase64: { type: 'string', maxLength: 16_000_000,
|
|
43
|
+
pattern: '^[A-Za-z0-9+/]*={0,2}$' }, expectedSha256: nullable(sha256), declaration: DISCIPLINE_DECLARATION_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']);
|
|
44
|
+
schemas['guarded-write'].oneOf = [{ required: ['content'], not: { required: ['contentBase64'] } },
|
|
45
|
+
{ required: ['contentBase64'], not: { required: ['content'] } }];
|
|
42
46
|
schemas['guarded-write'] = { oneOf: [schemas['guarded-write'], object({ ...step, targetPath: relativePath, content: {},
|
|
43
47
|
expectedSha256: nullable(sha256), handoffId: ledgerIdentifier, epoch: integer(1, 2147483647),
|
|
44
48
|
handoffPass: HANDOFF_WRITE_PASS_SCHEMA, provisionalLease: HANDOFF_PROVISIONAL_LEASE_SCHEMA },
|
|
@@ -82,6 +86,7 @@ add('chain-resume', { ...chain, stepId: ledgerIdentifier, status: enumeration(['
|
|
|
82
86
|
add('chain-status|chain-ledger-status', chain, ['repositoryRoot', 'chainId']);
|
|
83
87
|
add('chain-answer', { ...chain, stepId: ledgerIdentifier, answer: { anyOf: [text, object({ id: text }, ['id'])] }, accepted: { type: 'boolean' }, actorId: nullable(text) }, ['repositoryRoot', 'chainId', 'answer']);
|
|
84
88
|
Object.assign(schemas, LOCAL_CHAIN_OPERATION_SCHEMAS);
|
|
89
|
+
Object.assign(schemas, DESIGN_LOCAL_OPERATION_SCHEMAS);
|
|
85
90
|
const renewal = { ...flow, requestId: identifier, scope: object({ goal: text, allowedPaths: paths, forbiddenPaths: paths }, ['goal', 'allowedPaths']), policy: object({ intervalMs: integer(1), maxRenewals: integer(1, 100) }, ['intervalMs', 'maxRenewals']) };
|
|
86
91
|
add('budget-auto-renew-request', renewal, ['repositoryRoot', 'flowId', 'requestId', 'scope', 'policy']);
|
|
87
92
|
add('budget-auto-renew', { ...renewal, confirmation, grant: record }, ['repositoryRoot', 'flowId', 'requestId', 'scope', 'policy'], ['grant', 'confirmation']);
|
|
@@ -9,6 +9,8 @@ import { assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
|
9
9
|
import { saveMutationConsumption14 } from './local-mutation-pass-store.mjs';
|
|
10
10
|
import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs';
|
|
11
11
|
import { mutationFail14, removeMutationProjectFile14, moveMutationProjectFile14 } from './local-mutation-files.mjs';
|
|
12
|
+
import { assertDesignWrite } from './design-gate.mjs';
|
|
13
|
+
import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
|
|
12
14
|
|
|
13
15
|
function actionIntent(input, value, kind, review) {
|
|
14
16
|
if (value.pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation forbids structural file actions');
|
|
@@ -38,6 +40,9 @@ async function perform(input, kind, original, state) {
|
|
|
38
40
|
const review = await readDisciplineSnapshot(snapshot), intent = actionIntent(input, value, kind, review);
|
|
39
41
|
await assertDisciplineRepository(input, binding.root, review, ledger);
|
|
40
42
|
const source = snapshot.manifest.files.find(file => file.path === value.target);
|
|
43
|
+
const sourceBytes = (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, value.target)).content.toString('utf8');
|
|
44
|
+
await assertDesignWrite(input, pass, value.target, sourceBytes, kind === 'move' ? sourceBytes : '');
|
|
45
|
+
if (kind === 'move') await assertDesignWrite(input, pass, intent.destinationPath, '', sourceBytes);
|
|
41
46
|
const at = new Date().toISOString();
|
|
42
47
|
const writes = { ...ledger.writes, [value.target]: { status: 'pending', kind: 'delete', contentDigest: source.sha256, mode: source.mode, at } };
|
|
43
48
|
let destination;
|
|
@@ -3,6 +3,10 @@ import { mutationFail14 } from './local-mutation-files.mjs';
|
|
|
3
3
|
import { disciplineWritePreflight } from './local-discipline-gate.mjs';
|
|
4
4
|
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
5
5
|
import { assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
6
|
+
import { designUiCandidate } from './design-ui.mjs';
|
|
7
|
+
import { archiveFormat } from './package-archive.mjs';
|
|
8
|
+
import { assertRollbackCandidate, rememberMutationBaselines } from './rollback-history.mjs';
|
|
9
|
+
import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
|
|
6
10
|
|
|
7
11
|
export async function disciplineBatchBefore(input, value, options, before, after, ledger) {
|
|
8
12
|
const review = await readDisciplineSnapshot(value.snapshot);
|
|
@@ -11,14 +15,18 @@ export async function disciplineBatchBefore(input, value, options, before, after
|
|
|
11
15
|
// by mutationBatchReceiptBinding14. It restores that known image, rather than
|
|
12
16
|
// proposing a new definition or reusing a forward mutation permission.
|
|
13
17
|
const restoration = options.contract?.schemaVersion === 'entrorove.merge-rollback-authorization/1.0';
|
|
14
|
-
if (restoration) return review;
|
|
18
|
+
if (restoration) { await rememberMutationBaselines(input, value, before); return review; }
|
|
15
19
|
for (const file of after) {
|
|
16
20
|
const original = before.find(item => item.path === file.path);
|
|
17
21
|
if (canonical(original) === canonical(file)) continue;
|
|
18
22
|
if (!file.exists) mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'Forward batch deletion requires its separate guarded deletion declaration');
|
|
23
|
+
await assertRollbackCandidate(input, value.binding.root, file.path, original.sha256, file.sha256);
|
|
19
24
|
const source = options.contents?.find(item => item.path === file.path);
|
|
20
25
|
if (!source) {
|
|
21
|
-
|
|
26
|
+
const bytes = original.exists ? (await readSnapshotCopy14(input, value.snapshot.snapshotRoot, value.snapshot.manifest, file.path)).content : Buffer.alloc(0);
|
|
27
|
+
if (designUiCandidate(file.path) || archiveFormat(file.path, bytes)
|
|
28
|
+
|| /(?:^|\/)(?:package|manifest)\.json$/i.test(file.path)
|
|
29
|
+
|| /\.(?:[cm]?[jt]sx?|php|py|java|rs|vue|svelte|css)$/iu.test(file.path)) mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_REQUIRED', 'Source or package batch needs actual candidate bytes before any write');
|
|
22
30
|
continue;
|
|
23
31
|
}
|
|
24
32
|
const bytes = Buffer.from(source.contentBase64, 'base64');
|
|
@@ -26,7 +34,8 @@ export async function disciplineBatchBefore(input, value, options, before, after
|
|
|
26
34
|
const intent = value.pass.disciplinePolicy?.writes.find(write => write.targetPath === file.path);
|
|
27
35
|
await disciplineWritePreflight({ ...input, targetPath: file.path,
|
|
28
36
|
...(intent ? { declaration: { ...intent, usageReviewDigest: value.snapshot.manifest.disciplineReviewDigest } } : {}) },
|
|
29
|
-
{ ...value, target: file.path, ledger }, bytes
|
|
37
|
+
{ ...value, target: file.path, ledger }, bytes);
|
|
30
38
|
}
|
|
39
|
+
await rememberMutationBaselines(input, value, before);
|
|
31
40
|
return review;
|
|
32
41
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Ajv from 'ajv';
|
|
2
2
|
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
3
|
import { object, array, text, identifier, relativePath, sha256 as shaSchema, integer } from './local-contract-types.mjs';
|
|
4
|
+
import { DESIGN_POLICY_SCHEMA } from './design-contracts.mjs';
|
|
5
|
+
import { assertDesignDeclaration } from './design-declaration.mjs';
|
|
4
6
|
|
|
5
7
|
const refs = { ...array(identifier, 1), uniqueItems: true };
|
|
6
8
|
const defect = object({ path: relativePath, line: integer(1) }, ['path', 'line']);
|
|
@@ -13,7 +15,7 @@ export const DISCIPLINE_WRITE_SCHEMA = object({ targetPath: relativePath,
|
|
|
13
15
|
const approval = object({ interactionId: identifier, interactionDigest: shaSchema, auditId: identifier,
|
|
14
16
|
proposalDigest: shaSchema }, ['interactionId', 'interactionDigest', 'auditId', 'proposalDigest']);
|
|
15
17
|
const mutablePolicy = object({ schemaVersion: { const: 'entrorove.discipline-policy/1.0' },
|
|
16
|
-
requirementIds: refs, writes: array(DISCIPLINE_WRITE_SCHEMA, 1), approval,
|
|
18
|
+
requirementIds: refs, writes: array(DISCIPLINE_WRITE_SCHEMA, 1), approval, designApproval: approval, design: DESIGN_POLICY_SCHEMA,
|
|
17
19
|
dataOperations: array(object({ operation: { enum: ['delete', 'migrate', 'rebuild'] }, target: text,
|
|
18
20
|
reason: text, requirementRefs: refs, snapshotArtifactDigest: shaSchema }, ['operation', 'target', 'reason', 'requirementRefs', 'snapshotArtifactDigest'])),
|
|
19
21
|
}, ['schemaVersion', 'requirementIds', 'writes']);
|
|
@@ -37,10 +39,14 @@ export function disciplinePolicy(value) {
|
|
|
37
39
|
if (value.dataOperations?.some(action => action.requirementRefs.some(id => !requirements.has(id)))) {
|
|
38
40
|
throw Object.assign(new Error('Data operations must reference original requirements'), { code: 'ENTROROVE_DISCIPLINE_POLICY_INVALID' });
|
|
39
41
|
}
|
|
42
|
+
assertDesignDeclaration(value, value.writes.flatMap(write => [write.targetPath, ...(write.destinationPath ? [write.destinationPath] : [])]));
|
|
43
|
+
if (value.design && value.design.site.requirementIds.some(id => !requirements.has(id))) {
|
|
44
|
+
throw Object.assign(new Error('Design must retain its original task requirements'), { code: 'ENTROROVE_DESIGN_BINDING' });
|
|
45
|
+
}
|
|
40
46
|
return structuredClone(value);
|
|
41
47
|
}
|
|
42
48
|
export function disciplineProposalDigest(value) {
|
|
43
|
-
const { approval, ...proposal } = disciplinePolicy(value);
|
|
49
|
+
const { approval, designApproval, ...proposal } = disciplinePolicy(value);
|
|
44
50
|
return sha256(canonical(proposal));
|
|
45
51
|
}
|
|
46
52
|
export function disciplineHumanTriggers(value) {
|
|
@@ -50,11 +56,17 @@ export function disciplineHumanTriggers(value) {
|
|
|
50
56
|
if (['delete', 'move'].includes(write.kind) || write.deletedSymbols?.length) triggers.add(write.kind === 'move' ? 'move' : 'delete');
|
|
51
57
|
}
|
|
52
58
|
if (policy.dataOperations?.length) triggers.add('existing-data');
|
|
59
|
+
if (policy.design) triggers.add('interface-design');
|
|
53
60
|
return [...triggers].sort();
|
|
54
61
|
}
|
|
55
62
|
export function disciplineApprovedPolicy(value) {
|
|
56
63
|
const policy = disciplinePolicy(value);
|
|
57
|
-
|
|
64
|
+
const triggers = disciplineHumanTriggers(policy), fullReview = triggers.some(trigger => trigger !== 'interface-design')
|
|
65
|
+
|| Boolean(policy.design && !policy.designApproval);
|
|
66
|
+
if (policy.designApproval && (!policy.design || policy.designApproval.proposalDigest !== policy.design.baselineDigest)) {
|
|
67
|
+
throw Object.assign(new Error('The exact design revision needs its original human confirmation'), { code: 'ENTROROVE_DESIGN_APPROVAL_REQUIRED' });
|
|
68
|
+
}
|
|
69
|
+
if (fullReview && (!policy.approval || policy.approval.proposalDigest !== disciplineProposalDigest(policy))) {
|
|
58
70
|
throw Object.assign(new Error('This exact change proposal needs the original persisted human approval'), { code: 'ENTROROVE_DISCIPLINE_NEEDS_HUMAN' });
|
|
59
71
|
}
|
|
60
72
|
return policy;
|
|
@@ -6,6 +6,9 @@ import { disciplineRepositoryObservation, assertDisciplineRepository } from './l
|
|
|
6
6
|
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
7
7
|
import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
|
|
8
8
|
import { assertDelegatedSourceModification } from './local-delegated-modify-analysis.mjs';
|
|
9
|
+
import { assertDesignWrite } from './design-gate.mjs';
|
|
10
|
+
import { assertPackageReplacement } from './package-replacement.mjs';
|
|
11
|
+
import { assertRollbackCandidate } from './rollback-history.mjs';
|
|
9
12
|
|
|
10
13
|
const needs = message => mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', message);
|
|
11
14
|
function declaredWrite(input, pass, baseline) {
|
|
@@ -71,11 +74,23 @@ function checkReuse(review, candidate, target, intent, parallelPaths) {
|
|
|
71
74
|
empty: duplicates.length === 0, complete: review.scanComplete };
|
|
72
75
|
}
|
|
73
76
|
export async function disciplineWritePreflight(input, value, content) {
|
|
77
|
+
const candidateBytes = Buffer.from(content), candidateDigest = sha256(candidateBytes);
|
|
78
|
+
// Analyze decoded source bytes through the same original source checks.
|
|
79
|
+
// Binary assets remain in the exact-byte repository and M/S integrity chain.
|
|
80
|
+
if (Buffer.isBuffer(content)) {
|
|
81
|
+
try { content = new TextDecoder('utf-8', { ignoreBOM: true,
|
|
82
|
+
fatal: /\.(?:[cm]?[jt]sx?|php|py|java|rs|vue|svelte|css)$/i.test(value.target) }).decode(content); }
|
|
83
|
+
catch { mutationFail14('ENTROROVE_CONTENT_INVALID', 'Source content must contain valid UTF-8 bytes'); }
|
|
84
|
+
}
|
|
74
85
|
const { pass, target, binding, snapshot, ledger } = value;
|
|
75
86
|
const review = await readDisciplineSnapshot(snapshot), baseline = snapshot.manifest.files.find(file => file.path === target);
|
|
76
87
|
await assertDisciplineRepository(input, binding.root, review, ledger);
|
|
77
88
|
const intent = declaredWrite(input, pass, baseline);
|
|
78
|
-
const
|
|
89
|
+
const originalBytes = baseline.exists ? (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target)).content : Buffer.alloc(0);
|
|
90
|
+
if (baseline.exists) assertPackageReplacement(target, originalBytes, candidateBytes);
|
|
91
|
+
await assertRollbackCandidate(input, binding.root, target, baseline.sha256, candidateDigest);
|
|
92
|
+
const original = originalBytes.toString('utf8');
|
|
93
|
+
await assertDesignWrite(input, pass, target, original, content);
|
|
79
94
|
if (pass.writeDelegation?.purpose === 'planned-file-create') {
|
|
80
95
|
if (baseline.exists || intent?.kind !== 'create' || !pass.newFilePaths.includes(target)) needs('Planned creation cannot overwrite an existing source or replace its create role');
|
|
81
96
|
} else if (pass.writeDelegation?.purpose === 'preserved-existing-modify') assertDelegatedSourceModification({ path: target, before: original, after: content });
|
|
@@ -86,5 +101,5 @@ export async function disciplineWritePreflight(input, value, content) {
|
|
|
86
101
|
const formatting = baseline.exists ? disciplineFormattingLines(original, content, target) : [];
|
|
87
102
|
if (formatting.length) mutationFail14('ENTROROVE_DISCIPLINE_FORMAT_SCOPE', `Formatting touches lines without functional changes: ${target}:${formatting.join(',')}`);
|
|
88
103
|
return { reviewDigest: snapshot.manifest.disciplineReviewDigest, declaredKind: baseline.exists ? 'modify' : 'create', removed, reuse,
|
|
89
|
-
candidateDigest
|
|
104
|
+
candidateDigest, comparisonScope: review.comparison.scope };
|
|
90
105
|
}
|
|
@@ -27,7 +27,7 @@ function sourceLimit(relative, bytes, sources, used, required) {
|
|
|
27
27
|
`Semantic inspection of "${relative}" (${bytes} bytes) exceeds ${reason}; limits are ${disciplineAnalysisLimits.fileBytes} bytes per file, ${disciplineAnalysisLimits.sourceBytes} total bytes and ${disciplineAnalysisLimits.files} files. Preserve the original flow and split its approved inspection before retrying`);
|
|
28
28
|
return reason;
|
|
29
29
|
}
|
|
30
|
-
async function
|
|
30
|
+
export async function repositoryIntegrityFile(root, relative) {
|
|
31
31
|
// This path returns only an integrity digest. It never exposes source bytes or
|
|
32
32
|
// contributes semantic facts outside the signed source-read scope.
|
|
33
33
|
const before = await mutationTarget14(root, relative);
|
|
@@ -39,7 +39,9 @@ async function integrityOnlyFile(root, relative) {
|
|
|
39
39
|
for await (const chunk of handle.createReadStream({ autoClose: false })) digest.update(chunk);
|
|
40
40
|
const last = await handle.stat(), current = await mutationTarget14(root, relative);
|
|
41
41
|
if (!sameFile(first, last) || !sameFile(last, current.stat)) mutationFail14('ENTROROVE_FILE_CHANGED', `${relative} changed during integrity observation`);
|
|
42
|
-
return { sha256: digest.digest('hex'), mode: last.mode & 0o777
|
|
42
|
+
return { sha256: digest.digest('hex'), mode: last.mode & 0o777,
|
|
43
|
+
identity: { dev: last.dev, ino: last.ino, nlink: last.nlink, birthtimeMs: last.birthtimeMs,
|
|
44
|
+
ctimeMs: last.ctimeMs, mtimeMs: last.mtimeMs, size: last.size } };
|
|
43
45
|
} finally { await handle.close(); }
|
|
44
46
|
}
|
|
45
47
|
async function inventory(root, prefix = '') {
|
|
@@ -77,7 +79,7 @@ export async function disciplineRepositoryObservation(input, root, { analyze = f
|
|
|
77
79
|
const allowed = !restricted || withinBudgetScope(relative, restricted);
|
|
78
80
|
const bytes = overrides.has(relative) ? Buffer.byteLength(overrides.get(relative)) : stat.size;
|
|
79
81
|
const limited = semantic && allowed ? sourceLimit(relative, bytes, sources, sourceBytes, required) : null;
|
|
80
|
-
const read = semantic && allowed && !limited ? await mutationRead14(input, root, relative) : await
|
|
82
|
+
const read = semantic && allowed && !limited ? await mutationRead14(input, root, relative) : await repositoryIntegrityFile(root, relative);
|
|
81
83
|
files.push({ path: relative, sha256: read.sha256, mode: read.mode, kind: 'file' });
|
|
82
84
|
if (semantic && allowed && !limited) { sources.push({ path: relative, content: overrides.get(relative) ?? read.content.toString('utf8') }); sourceBytes += bytes; }
|
|
83
85
|
else if (limited) unresolved.push({ path: relative, reason: limited, bytes });
|
|
@@ -124,7 +126,7 @@ export function disciplineRepositoryDifferences(before, after, authorized = new
|
|
|
124
126
|
}
|
|
125
127
|
return changed;
|
|
126
128
|
}
|
|
127
|
-
export async function
|
|
129
|
+
export async function observeDisciplineRepositoryState(input, root, review, ledger) {
|
|
128
130
|
const expected = new Map();
|
|
129
131
|
for (const [relative, write] of Object.entries(ledger.writes ?? {})) {
|
|
130
132
|
if (write.status !== 'written') mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Repository comparison found an unresolved physical action');
|
|
@@ -137,6 +139,10 @@ export async function assertDisciplineRepository(input, root, review, ledger) {
|
|
|
137
139
|
}
|
|
138
140
|
const current = await disciplineRepositoryObservation(input, root);
|
|
139
141
|
const changed = disciplineRepositoryDifferences(review.comparison, current.comparison, expected);
|
|
142
|
+
return { comparison: current.comparison, changed };
|
|
143
|
+
}
|
|
144
|
+
export async function assertDisciplineRepository(input, root, review, ledger) {
|
|
145
|
+
const { comparison, changed } = await observeDisciplineRepositoryState(input, root, review, ledger);
|
|
140
146
|
if (changed.length) mutationFail14('ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT', `Whole-repository comparison differs outside completed declared actions: ${changed.join(', ')}`);
|
|
141
|
-
return
|
|
147
|
+
return comparison;
|
|
142
148
|
}
|
package/local-error.mjs
CHANGED
|
@@ -17,9 +17,24 @@ export const LOCAL_ERROR_CODE_CATALOG = Object.freeze({
|
|
|
17
17
|
ENTROROVE_MUTATION_DELEGATION_INVALID: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
18
18
|
ENTROROVE_IDE_GUARD_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
19
19
|
ENTROROVE_IDE_GUARD_UNSUPPORTED: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
20
|
+
ENTROROVE_DESIGN_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
21
|
+
ENTROROVE_DESIGN_APPROVAL_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
22
|
+
ENTROROVE_DESIGN_LINT: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
23
|
+
ENTROROVE_DESIGN_ACCEPTANCE: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
24
|
+
ENTROROVE_PACKAGE_DOWNGRADE: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
25
|
+
ENTROROVE_PACKAGE_VERSION_CONFLICT: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
26
|
+
ENTROROVE_PACKAGE_VERSION_UNKNOWN: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
27
|
+
ENTROROVE_PACKAGE_IDENTITY_CHANGED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
28
|
+
ENTROROVE_PACKAGE_PROVENANCE_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
29
|
+
ENTROROVE_ROLLBACK_PROOF_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
30
|
+
ENTROROVE_ROLLBACK_HISTORY_INVALID: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
20
31
|
});
|
|
21
32
|
|
|
22
33
|
const BLOCKED_CODES = new Set([
|
|
34
|
+
'ENTROROVE_PACKAGE_DOWNGRADE', 'ENTROROVE_PACKAGE_VERSION_CONFLICT', 'ENTROROVE_PACKAGE_VERSION_UNKNOWN',
|
|
35
|
+
'ENTROROVE_PACKAGE_IDENTITY_CHANGED', 'ENTROROVE_PACKAGE_PROVENANCE_REQUIRED', 'ENTROROVE_ROLLBACK_PROOF_REQUIRED', 'ENTROROVE_ROLLBACK_HISTORY_INVALID',
|
|
36
|
+
'ENTROROVE_DESIGN_REQUIRED', 'ENTROROVE_DESIGN_APPROVAL_REQUIRED', 'ENTROROVE_DESIGN_BASELINE_CHANGED',
|
|
37
|
+
'ENTROROVE_DESIGN_FRAMEWORK_MISSING', 'ENTROROVE_DESIGN_ARTIFACT_CHANGED', 'ENTROROVE_DESIGN_SCOPE',
|
|
23
38
|
'ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'ENTROROVE_DISCIPLINE_USAGE_UNKNOWN', 'ENTROROVE_DISCIPLINE_REVIEW_REQUIRED',
|
|
24
39
|
'ENTROROVE_DISCIPLINE_DATA_SNAPSHOT_REQUIRED', 'ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT', 'ENTROROVE_DISCIPLINE_FORMAT_SCOPE',
|
|
25
40
|
'ENTROROVE_DISCIPLINE_SPECULATIVE', 'ENTROROVE_DISCIPLINE_REVIEW_INVALID',
|
|
@@ -52,5 +67,7 @@ export function localErrorDisposition(code) {
|
|
|
52
67
|
export function localErrorEnvelope(error, fallbackCode = 'ENTROROVE_LOCAL_FAILURE') {
|
|
53
68
|
const code = typeof error?.code === 'string' && error.code ? error.code : fallbackCode;
|
|
54
69
|
const message = typeof error?.message === 'string' && error.message ? error.message : 'Local operation failed';
|
|
55
|
-
|
|
70
|
+
const report = /^ENTROROVE_DESIGN_/.test(code) && typeof error?.reportPath === 'string' && /^\//.test(error.reportPath)
|
|
71
|
+
&& /^[a-f0-9]{64}$/.test(error.reportDigest ?? '') ? { reportPath: error.reportPath, reportDigest: error.reportDigest } : {};
|
|
72
|
+
return { schemaVersion: LOCAL_ERROR_SCHEMA, status: localErrorStatus({ ...error, code }), code, disposition: localErrorDisposition(code), message, ...report };
|
|
56
73
|
}
|
package/local-execution.mjs
CHANGED
|
@@ -276,12 +276,13 @@ export async function finalGate(input) {
|
|
|
276
276
|
guarantee: 'Original engine, frozen role plans and actual three-party checks are required' };
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
const LOCAL_OPERATION_GROUPS = Object.freeze(['core', 'budget', 'scope', 'merge', 'chain', 'handoff', 'archguard', 'calctool', 'swarm', 'validator', 'formula', 'evidence']);
|
|
279
|
+
const LOCAL_OPERATION_GROUPS = Object.freeze(['core', 'budget', 'scope', 'merge', 'chain', 'handoff', 'archguard', 'calctool', 'swarm', 'validator', 'formula', 'evidence', 'design']);
|
|
280
280
|
const PASS_REQUIRED_OPERATIONS = new Set(['mutation-snapshot-attest', 'mutation-consumption-attest', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
|
|
281
281
|
'discipline-review', 'discipline-check', 'guarded-delete', 'guarded-move',
|
|
282
282
|
'merge-verified', 'rollback', 'archguard-contract-create', 'archguard-contract-update', 'archguard-checkpoint', 'archguard-scope-request',
|
|
283
283
|
'budget-handoff', 'budget-transition', 'spawn-bind', 'peer-complete', 'complete']);
|
|
284
284
|
function operationGroup(operation) {
|
|
285
|
+
if (operation.startsWith('design-')) return 'design';
|
|
285
286
|
if (operation.startsWith('archguard-')) return 'archguard';
|
|
286
287
|
if (operation.startsWith('calctool-')) return 'calctool';
|
|
287
288
|
if (operation.includes('handoff')) return 'handoff';
|