youmd 0.10.0 → 0.10.2
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/dist/commands/brain.d.ts +25 -0
- package/dist/commands/brain.d.ts.map +1 -0
- package/dist/commands/brain.js +683 -0
- package/dist/commands/brain.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +4 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/machine.d.ts +3 -0
- package/dist/commands/machine.d.ts.map +1 -1
- package/dist/commands/machine.js +73 -4
- package/dist/commands/machine.js.map +1 -1
- package/dist/commands/orchestrate.d.ts.map +1 -1
- package/dist/commands/orchestrate.js +4 -3
- package/dist/commands/orchestrate.js.map +1 -1
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +24 -0
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/remote.d.ts.map +1 -1
- package/dist/commands/remote.js +42 -13
- package/dist/commands/remote.js.map +1 -1
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +2 -0
- package/dist/commands/skill.js.map +1 -1
- package/dist/commands/tasks.d.ts +23 -3
- package/dist/commands/tasks.d.ts.map +1 -1
- package/dist/commands/tasks.js +179 -49
- package/dist/commands/tasks.js.map +1 -1
- package/dist/generated/capability-registry.d.ts +1096 -0
- package/dist/generated/capability-registry.d.ts.map +1 -0
- package/dist/generated/capability-registry.js +1204 -0
- package/dist/generated/capability-registry.js.map +1 -0
- package/dist/index.js +42 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +84 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +26 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/brain/adapters.d.ts +34 -0
- package/dist/lib/brain/adapters.d.ts.map +1 -0
- package/dist/lib/brain/adapters.js +362 -0
- package/dist/lib/brain/adapters.js.map +1 -0
- package/dist/lib/brain/apply-control-plane.d.ts +17 -0
- package/dist/lib/brain/apply-control-plane.d.ts.map +1 -0
- package/dist/lib/brain/apply-control-plane.js +117 -0
- package/dist/lib/brain/apply-control-plane.js.map +1 -0
- package/dist/lib/brain/cache.d.ts +25 -0
- package/dist/lib/brain/cache.d.ts.map +1 -0
- package/dist/lib/brain/cache.js +99 -0
- package/dist/lib/brain/cache.js.map +1 -0
- package/dist/lib/brain/canonical.d.ts +5 -0
- package/dist/lib/brain/canonical.d.ts.map +1 -0
- package/dist/lib/brain/canonical.js +38 -0
- package/dist/lib/brain/canonical.js.map +1 -0
- package/dist/lib/brain/discovery.d.ts +9 -0
- package/dist/lib/brain/discovery.d.ts.map +1 -0
- package/dist/lib/brain/discovery.js +560 -0
- package/dist/lib/brain/discovery.js.map +1 -0
- package/dist/lib/brain/index.d.ts +55 -0
- package/dist/lib/brain/index.d.ts.map +1 -0
- package/dist/lib/brain/index.js +384 -0
- package/dist/lib/brain/index.js.map +1 -0
- package/dist/lib/brain/proposal.d.ts +179 -0
- package/dist/lib/brain/proposal.d.ts.map +1 -0
- package/dist/lib/brain/proposal.js +694 -0
- package/dist/lib/brain/proposal.js.map +1 -0
- package/dist/lib/brain/render.d.ts +33 -0
- package/dist/lib/brain/render.d.ts.map +1 -0
- package/dist/lib/brain/render.js +515 -0
- package/dist/lib/brain/render.js.map +1 -0
- package/dist/lib/brain/safe-file.d.ts +110 -0
- package/dist/lib/brain/safe-file.d.ts.map +1 -0
- package/dist/lib/brain/safe-file.js +564 -0
- package/dist/lib/brain/safe-file.js.map +1 -0
- package/dist/lib/brain/safety.d.ts +8 -0
- package/dist/lib/brain/safety.d.ts.map +1 -0
- package/dist/lib/brain/safety.js +42 -0
- package/dist/lib/brain/safety.js.map +1 -0
- package/dist/lib/brain/staged-apply.d.ts +57 -0
- package/dist/lib/brain/staged-apply.d.ts.map +1 -0
- package/dist/lib/brain/staged-apply.js +336 -0
- package/dist/lib/brain/staged-apply.js.map +1 -0
- package/dist/lib/brain/store.d.ts +26 -0
- package/dist/lib/brain/store.d.ts.map +1 -0
- package/dist/lib/brain/store.js +504 -0
- package/dist/lib/brain/store.js.map +1 -0
- package/dist/lib/brain/types.d.ts +160 -0
- package/dist/lib/brain/types.d.ts.map +1 -0
- package/dist/lib/brain/types.js +24 -0
- package/dist/lib/brain/types.js.map +1 -0
- package/dist/lib/config.d.ts +22 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +70 -14
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/daemon.d.ts +27 -0
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/daemon.js +57 -0
- package/dist/lib/daemon.js.map +1 -1
- package/dist/lib/machine-identity.d.ts +16 -0
- package/dist/lib/machine-identity.d.ts.map +1 -0
- package/dist/lib/machine-identity.js +101 -0
- package/dist/lib/machine-identity.js.map +1 -0
- package/dist/lib/orchestrator/loop.d.ts +1 -0
- package/dist/lib/orchestrator/loop.d.ts.map +1 -1
- package/dist/lib/orchestrator/loop.js +16 -0
- package/dist/lib/orchestrator/loop.js.map +1 -1
- package/dist/lib/orchestrator/tools.d.ts +11 -0
- package/dist/lib/orchestrator/tools.d.ts.map +1 -1
- package/dist/lib/orchestrator/tools.js +143 -0
- package/dist/lib/orchestrator/tools.js.map +1 -1
- package/dist/lib/project-strategy.d.ts +38 -0
- package/dist/lib/project-strategy.d.ts.map +1 -1
- package/dist/lib/project-strategy.js +91 -0
- package/dist/lib/project-strategy.js.map +1 -1
- package/dist/lib/projectContext.d.ts +6 -4
- package/dist/lib/projectContext.d.ts.map +1 -1
- package/dist/lib/projectContext.js +19 -17
- package/dist/lib/projectContext.js.map +1 -1
- package/dist/lib/tasksFile.d.ts +27 -32
- package/dist/lib/tasksFile.d.ts.map +1 -1
- package/dist/lib/tasksFile.js +359 -163
- package/dist/lib/tasksFile.js.map +1 -1
- package/dist/mcp/registry.d.ts +8 -2
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +189 -2
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +3 -2
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROJECT_BRAIN_PROPOSAL_VERSION = exports.PROJECT_BRAIN_PROPOSAL_SCHEMA = exports.PROJECT_BRAIN_OVERRIDE_VERSION = void 0;
|
|
4
|
+
exports.validateProjectBrainPatch = validateProjectBrainPatch;
|
|
5
|
+
exports.validateProjectBrainUnifiedDiffPatch = validateProjectBrainUnifiedDiffPatch;
|
|
6
|
+
exports.validateProjectBrainProposal = validateProjectBrainProposal;
|
|
7
|
+
exports.proposeProjectBrainAdoption = proposeProjectBrainAdoption;
|
|
8
|
+
exports.importProjectBrainOverrides = importProjectBrainOverrides;
|
|
9
|
+
const redact_1 = require("../redact");
|
|
10
|
+
const canonical_1 = require("./canonical");
|
|
11
|
+
exports.PROJECT_BRAIN_OVERRIDE_VERSION = "you-md/project-brain-override/v1";
|
|
12
|
+
exports.PROJECT_BRAIN_PROPOSAL_SCHEMA = "https://you.md/schemas/project-brain-proposal/v1";
|
|
13
|
+
exports.PROJECT_BRAIN_PROPOSAL_VERSION = "you-md/project-brain-proposal/v1";
|
|
14
|
+
const PROJECT_REF_PATTERN = /^project:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
15
|
+
const REVISION_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
16
|
+
const OVERRIDE_SOURCE_PATTERN = /^(?:overrides|notes)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\.json$/;
|
|
17
|
+
const ARTIFACT_REF_PATTERN = /^artifact:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/[a-z0-9][a-z0-9._-]{0,95}$/;
|
|
18
|
+
const FORBIDDEN_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
19
|
+
const MAX_PATCH_OPERATIONS = 100;
|
|
20
|
+
const MAX_PATCH_BYTES = 256 * 1024;
|
|
21
|
+
const APPROVAL_BINDINGS = [
|
|
22
|
+
"proposalRef",
|
|
23
|
+
"projectRef",
|
|
24
|
+
"machineRef",
|
|
25
|
+
"workspaceRef",
|
|
26
|
+
"baseRevision",
|
|
27
|
+
"target.baseContentHash",
|
|
28
|
+
"source.sourceRevision",
|
|
29
|
+
"patchFormat",
|
|
30
|
+
"patchDigest",
|
|
31
|
+
];
|
|
32
|
+
function proposalError(sourceRef, code, message, operationIndex) {
|
|
33
|
+
return operationIndex === undefined ? { sourceRef, code, message } : { sourceRef, code, message, operationIndex };
|
|
34
|
+
}
|
|
35
|
+
function normalizeJson(value, seen = new Set()) {
|
|
36
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
37
|
+
return value;
|
|
38
|
+
if (typeof value === "number") {
|
|
39
|
+
if (!Number.isFinite(value))
|
|
40
|
+
throw new TypeError("project brain patches reject non-finite numbers");
|
|
41
|
+
return Object.is(value, -0) ? 0 : value;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
if (seen.has(value))
|
|
45
|
+
throw new TypeError("project brain patches reject cyclic values");
|
|
46
|
+
seen.add(value);
|
|
47
|
+
const normalized = value.map((item) => normalizeJson(item, seen));
|
|
48
|
+
seen.delete(value);
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
if (seen.has(value))
|
|
53
|
+
throw new TypeError("project brain patches reject cyclic values");
|
|
54
|
+
seen.add(value);
|
|
55
|
+
const normalized = Object.create(null);
|
|
56
|
+
for (const key of Object.keys(value).sort()) {
|
|
57
|
+
if (FORBIDDEN_SEGMENTS.has(key))
|
|
58
|
+
throw new TypeError("project brain patches reject unsafe object keys");
|
|
59
|
+
const child = value[key];
|
|
60
|
+
if (child === undefined)
|
|
61
|
+
throw new TypeError("project brain patches reject undefined values");
|
|
62
|
+
normalized[key] = normalizeJson(child, seen);
|
|
63
|
+
}
|
|
64
|
+
seen.delete(value);
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
throw new TypeError(`project brain patches reject ${typeof value} values`);
|
|
68
|
+
}
|
|
69
|
+
function cloneJson(value) {
|
|
70
|
+
return normalizeJson(value);
|
|
71
|
+
}
|
|
72
|
+
function digest(value) {
|
|
73
|
+
return `sha256:${(0, canonical_1.sha256)((0, canonical_1.canonicalize)(value))}`;
|
|
74
|
+
}
|
|
75
|
+
function safeSecretScan(value) {
|
|
76
|
+
const serialized = JSON.stringify(value).replace(/(?:(?:machine|repository|worktree|branch):sha256:|(?:scan|workspace|sha256|heuristic):|proposal:brain\/)[a-f0-9]{64}/g, "[OPAQUE_HASH]");
|
|
77
|
+
return (0, redact_1.redactText)(serialized).hits === 0;
|
|
78
|
+
}
|
|
79
|
+
function parsePointer(pointer) {
|
|
80
|
+
if (typeof pointer !== "string" || pointer.length > 500)
|
|
81
|
+
return null;
|
|
82
|
+
if (pointer === "")
|
|
83
|
+
return [];
|
|
84
|
+
if (!pointer.startsWith("/"))
|
|
85
|
+
return null;
|
|
86
|
+
const encoded = pointer.slice(1).split("/");
|
|
87
|
+
if (encoded.length > 100)
|
|
88
|
+
return null;
|
|
89
|
+
const decoded = [];
|
|
90
|
+
for (const segment of encoded) {
|
|
91
|
+
if (/~(?:[^01]|$)/.test(segment))
|
|
92
|
+
return null;
|
|
93
|
+
const value = segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
94
|
+
if (FORBIDDEN_SEGMENTS.has(value))
|
|
95
|
+
return null;
|
|
96
|
+
decoded.push(value);
|
|
97
|
+
}
|
|
98
|
+
return decoded;
|
|
99
|
+
}
|
|
100
|
+
function own(record, key) {
|
|
101
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
102
|
+
}
|
|
103
|
+
function arrayIndex(segment, length, allowEnd) {
|
|
104
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(segment))
|
|
105
|
+
return null;
|
|
106
|
+
const index = Number(segment);
|
|
107
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > length || (!allowEnd && index === length))
|
|
108
|
+
return null;
|
|
109
|
+
return index;
|
|
110
|
+
}
|
|
111
|
+
function state(present, value) {
|
|
112
|
+
return present ? { present: true, value: cloneJson(value) } : { present: false };
|
|
113
|
+
}
|
|
114
|
+
function normalizePatch(raw, sourceRef) {
|
|
115
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > MAX_PATCH_OPERATIONS) {
|
|
116
|
+
return { errors: [proposalError(sourceRef, "invalid_patch", "patch must contain between 1 and 100 operations")] };
|
|
117
|
+
}
|
|
118
|
+
let rawBytes;
|
|
119
|
+
try {
|
|
120
|
+
rawBytes = Buffer.byteLength(JSON.stringify(raw), "utf8");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
rawBytes = MAX_PATCH_BYTES + 1;
|
|
124
|
+
}
|
|
125
|
+
if (rawBytes > MAX_PATCH_BYTES)
|
|
126
|
+
return { errors: [proposalError(sourceRef, "invalid_patch", "patch exceeds the safe size budget")] };
|
|
127
|
+
const patch = [];
|
|
128
|
+
const errors = [];
|
|
129
|
+
for (const [index, candidate] of raw.entries()) {
|
|
130
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
131
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "patch operation must be an object", index));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const operation = candidate;
|
|
135
|
+
const op = operation.op;
|
|
136
|
+
const pointer = parsePointer(operation.path);
|
|
137
|
+
const allowedKeys = op === "remove" ? new Set(["op", "path"]) : new Set(["op", "path", "value"]);
|
|
138
|
+
if (!new Set(["add", "remove", "replace", "test"]).has(String(op)) || !pointer
|
|
139
|
+
|| Object.keys(operation).some((key) => !allowedKeys.has(key))) {
|
|
140
|
+
errors.push(proposalError(sourceRef, "unsafe_patch", "patch operation or JSON pointer is unsupported", index));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (op === "remove") {
|
|
144
|
+
if ("value" in operation)
|
|
145
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "remove operations cannot contain a value", index));
|
|
146
|
+
else
|
|
147
|
+
patch.push({ op: "remove", path: String(operation.path) });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!("value" in operation)) {
|
|
151
|
+
errors.push(proposalError(sourceRef, "invalid_patch", `${String(op)} operations require an exact value`, index));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
patch.push({ op: op, path: String(operation.path), value: normalizeJson(operation.value) });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "patch value is not safe canonical JSON", index));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!errors.length && !safeSecretScan(patch))
|
|
162
|
+
errors.push(proposalError(sourceRef, "secret_detected", "patch contains a secret or private-data pattern"));
|
|
163
|
+
return errors.length ? { errors } : { patch, errors: [] };
|
|
164
|
+
}
|
|
165
|
+
function resolveParent(document, segments) {
|
|
166
|
+
if (segments.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
let current = document;
|
|
169
|
+
for (const segment of segments.slice(0, -1)) {
|
|
170
|
+
if (Array.isArray(current)) {
|
|
171
|
+
const index = arrayIndex(segment, current.length, false);
|
|
172
|
+
if (index === null)
|
|
173
|
+
return null;
|
|
174
|
+
current = current[index];
|
|
175
|
+
}
|
|
176
|
+
else if (current && typeof current === "object") {
|
|
177
|
+
if (!own(current, segment))
|
|
178
|
+
return null;
|
|
179
|
+
current = current[segment];
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return Array.isArray(current) || (current && typeof current === "object")
|
|
185
|
+
? { parent: current, key: segments.at(-1) }
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
188
|
+
/** Pure RFC6902-style validation and evaluation. This function has no filesystem capability. */
|
|
189
|
+
function validateProjectBrainPatch(base, rawPatch, sourceRef = "proposal:input") {
|
|
190
|
+
let document;
|
|
191
|
+
try {
|
|
192
|
+
document = normalizeJson(base);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "base document is not canonical JSON")] };
|
|
196
|
+
}
|
|
197
|
+
const before = cloneJson(document);
|
|
198
|
+
const normalized = normalizePatch(rawPatch, sourceRef);
|
|
199
|
+
if (!normalized.patch)
|
|
200
|
+
return { valid: false, errors: normalized.errors };
|
|
201
|
+
const operations = [];
|
|
202
|
+
const errors = [];
|
|
203
|
+
for (const [index, operation] of normalized.patch.entries()) {
|
|
204
|
+
const segments = parsePointer(operation.path);
|
|
205
|
+
if (segments.length === 0) {
|
|
206
|
+
const beforeState = state(true, document);
|
|
207
|
+
if (operation.op === "remove") {
|
|
208
|
+
errors.push(proposalError(sourceRef, "unsafe_patch", "removing the root document is not supported", index));
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
if (operation.op === "test") {
|
|
212
|
+
if ((0, canonical_1.canonicalize)(document) !== (0, canonical_1.canonicalize)(operation.value)) {
|
|
213
|
+
errors.push(proposalError(sourceRef, "patch_test_failed", "patch test does not match the exact base document", index));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
document = cloneJson(operation.value);
|
|
219
|
+
operations.push({ index, op: operation.op, path: operation.path, before: beforeState, after: state(true, document) });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const resolved = resolveParent(document, segments);
|
|
223
|
+
if (!resolved) {
|
|
224
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "patch parent does not exist in the exact base document", index));
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const { parent, key } = resolved;
|
|
228
|
+
if (Array.isArray(parent)) {
|
|
229
|
+
if (operation.op === "add") {
|
|
230
|
+
const insertion = key === "-" ? parent.length : arrayIndex(key, parent.length, true);
|
|
231
|
+
if (insertion === null) {
|
|
232
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "array add index is invalid", index));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
parent.splice(insertion, 0, cloneJson(operation.value));
|
|
236
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(false), after: state(true, operation.value) });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const target = arrayIndex(key, parent.length, false);
|
|
240
|
+
if (target === null) {
|
|
241
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "array patch target does not exist", index));
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
const oldValue = cloneJson(parent[target]);
|
|
245
|
+
if (operation.op === "test") {
|
|
246
|
+
if ((0, canonical_1.canonicalize)(oldValue) !== (0, canonical_1.canonicalize)(operation.value)) {
|
|
247
|
+
errors.push(proposalError(sourceRef, "patch_test_failed", "patch test does not match the exact base value", index));
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(true, oldValue), after: state(true, oldValue) });
|
|
251
|
+
}
|
|
252
|
+
else if (operation.op === "remove") {
|
|
253
|
+
parent.splice(target, 1);
|
|
254
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(true, oldValue), after: state(false) });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
parent[target] = cloneJson(operation.value);
|
|
258
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(true, oldValue), after: state(true, operation.value) });
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const exists = own(parent, key);
|
|
263
|
+
const oldValue = exists ? cloneJson(parent[key]) : undefined;
|
|
264
|
+
if (operation.op === "add") {
|
|
265
|
+
parent[key] = cloneJson(operation.value);
|
|
266
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(exists, oldValue), after: state(true, operation.value) });
|
|
267
|
+
}
|
|
268
|
+
else if (!exists) {
|
|
269
|
+
errors.push(proposalError(sourceRef, "invalid_patch", "object patch target does not exist", index));
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
else if (operation.op === "test") {
|
|
273
|
+
if ((0, canonical_1.canonicalize)(oldValue) !== (0, canonical_1.canonicalize)(operation.value)) {
|
|
274
|
+
errors.push(proposalError(sourceRef, "patch_test_failed", "patch test does not match the exact base value", index));
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(true, oldValue), after: state(true, oldValue) });
|
|
278
|
+
}
|
|
279
|
+
else if (operation.op === "remove") {
|
|
280
|
+
delete parent[key];
|
|
281
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(true, oldValue), after: state(false) });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
parent[key] = cloneJson(operation.value);
|
|
285
|
+
operations.push({ index, op: operation.op, path: operation.path, before: state(true, oldValue), after: state(true, operation.value) });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (errors.length)
|
|
289
|
+
return { valid: false, errors };
|
|
290
|
+
const diff = {
|
|
291
|
+
format: "rfc6902",
|
|
292
|
+
beforeDigest: digest(before),
|
|
293
|
+
afterDigest: digest(document),
|
|
294
|
+
operations,
|
|
295
|
+
};
|
|
296
|
+
return { valid: true, errors: [], patch: normalized.patch, patchDigest: digest(normalized.patch), result: document, diff };
|
|
297
|
+
}
|
|
298
|
+
function safeRelativeTextPath(value) {
|
|
299
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 500 || value.startsWith("/")
|
|
300
|
+
|| /^[A-Za-z]:/.test(value) || value.includes("\\") || value.includes("//") || value.endsWith("/"))
|
|
301
|
+
return false;
|
|
302
|
+
return value.split("/").every((segment) => segment && segment !== "." && segment !== "..");
|
|
303
|
+
}
|
|
304
|
+
function renderUnifiedDiff(oldPath, newPath, hunks) {
|
|
305
|
+
const lines = [`--- a/${oldPath}`, `+++ b/${newPath}`];
|
|
306
|
+
for (const hunk of hunks) {
|
|
307
|
+
lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`, ...hunk.lines);
|
|
308
|
+
}
|
|
309
|
+
return `${lines.join("\n")}\n`;
|
|
310
|
+
}
|
|
311
|
+
/** Strictly normalize the hosted text variant without converting it to RFC6902. */
|
|
312
|
+
function validateProjectBrainUnifiedDiffPatch(raw, sourceRef = "proposal:input") {
|
|
313
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
314
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "unified diff patch must be an object")] };
|
|
315
|
+
}
|
|
316
|
+
const input = raw;
|
|
317
|
+
if (Object.keys(input).some((key) => !new Set(["text", "oldPath", "newPath", "hunks"]).has(key))
|
|
318
|
+
|| typeof input.text !== "string" || Buffer.byteLength(input.text, "utf8") > 1024 * 1024
|
|
319
|
+
|| !safeRelativeTextPath(input.oldPath) || !safeRelativeTextPath(input.newPath)
|
|
320
|
+
|| !Array.isArray(input.hunks) || input.hunks.length < 1 || input.hunks.length > 100) {
|
|
321
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "unified diff shape or path is invalid")] };
|
|
322
|
+
}
|
|
323
|
+
const hunks = [];
|
|
324
|
+
for (const [index, rawHunk] of input.hunks.entries()) {
|
|
325
|
+
if (!rawHunk || typeof rawHunk !== "object" || Array.isArray(rawHunk)) {
|
|
326
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "unified diff hunk must be an object", index)] };
|
|
327
|
+
}
|
|
328
|
+
const hunk = rawHunk;
|
|
329
|
+
if (Object.keys(hunk).some((key) => !new Set(["oldStart", "oldLines", "newStart", "newLines", "lines"]).has(key))
|
|
330
|
+
|| !Number.isInteger(hunk.oldStart) || Number(hunk.oldStart) < 0
|
|
331
|
+
|| !Number.isInteger(hunk.oldLines) || Number(hunk.oldLines) < 0
|
|
332
|
+
|| !Number.isInteger(hunk.newStart) || Number(hunk.newStart) < 0
|
|
333
|
+
|| !Number.isInteger(hunk.newLines) || Number(hunk.newLines) < 0
|
|
334
|
+
|| !Array.isArray(hunk.lines) || hunk.lines.some((line) => typeof line !== "string" || !/^(?: |\+|-|\\)/.test(line))) {
|
|
335
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "unified diff hunk is invalid", index)] };
|
|
336
|
+
}
|
|
337
|
+
const oldCount = hunk.lines.filter((line) => typeof line === "string" && /^[ -]/.test(line)).length;
|
|
338
|
+
const newCount = hunk.lines.filter((line) => typeof line === "string" && /^[ +]/.test(line)).length;
|
|
339
|
+
if (oldCount !== hunk.oldLines || newCount !== hunk.newLines) {
|
|
340
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "unified diff hunk line counts do not match", index)] };
|
|
341
|
+
}
|
|
342
|
+
hunks.push({
|
|
343
|
+
oldStart: Number(hunk.oldStart), oldLines: Number(hunk.oldLines),
|
|
344
|
+
newStart: Number(hunk.newStart), newLines: Number(hunk.newLines),
|
|
345
|
+
lines: [...hunk.lines],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const patch = { text: input.text, oldPath: input.oldPath, newPath: input.newPath, hunks };
|
|
349
|
+
if (patch.text !== renderUnifiedDiff(patch.oldPath, patch.newPath, patch.hunks)) {
|
|
350
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_patch", "unified diff text does not match its normalized hunks")] };
|
|
351
|
+
}
|
|
352
|
+
if (!safeSecretScan(patch))
|
|
353
|
+
return { valid: false, errors: [proposalError(sourceRef, "secret_detected", "unified diff contains a secret or private-data pattern")] };
|
|
354
|
+
return { valid: true, errors: [], patch, patchDigest: digest(normalizeJson(patch)) };
|
|
355
|
+
}
|
|
356
|
+
/** Validate a transport proposal envelope without approving or applying it. */
|
|
357
|
+
function validateProjectBrainProposal(value) {
|
|
358
|
+
const sourceRef = value && typeof value === "object" && typeof value.proposalRef === "string"
|
|
359
|
+
? String(value.proposalRef)
|
|
360
|
+
: "proposal:input";
|
|
361
|
+
let normalized;
|
|
362
|
+
try {
|
|
363
|
+
normalized = normalizeJson(value);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_proposal", "proposal is not safe canonical JSON")] };
|
|
367
|
+
}
|
|
368
|
+
if (!normalized || typeof normalized !== "object" || Array.isArray(normalized)) {
|
|
369
|
+
return { valid: false, errors: [proposalError(sourceRef, "invalid_proposal", "proposal must be an object")] };
|
|
370
|
+
}
|
|
371
|
+
const proposal = normalized;
|
|
372
|
+
const record = normalized;
|
|
373
|
+
const errors = [];
|
|
374
|
+
const requiredKeys = new Set([
|
|
375
|
+
"$schema", "schemaVersion", "kind", "proposalRef", "proposalType", "projectRef", "machineRef", "workspaceRef",
|
|
376
|
+
"baseRevision", "requestedCapability", "rationale", "status", "target", "patchFormat", "patchDigest", "patch", "diff",
|
|
377
|
+
"approval", "source",
|
|
378
|
+
]);
|
|
379
|
+
if (Object.keys(record).length !== requiredKeys.size || Object.keys(record).some((key) => !requiredKeys.has(key))
|
|
380
|
+
|| proposal.$schema !== exports.PROJECT_BRAIN_PROPOSAL_SCHEMA || proposal.schemaVersion !== exports.PROJECT_BRAIN_PROPOSAL_VERSION
|
|
381
|
+
|| proposal.kind !== "project-brain-proposal" || proposal.status !== "proposed"
|
|
382
|
+
|| !PROJECT_REF_PATTERN.test(proposal.projectRef) || !/^machine:sha256:[a-f0-9]{64}$/.test(proposal.machineRef)
|
|
383
|
+
|| !/^workspace:[a-f0-9]{64}$/.test(proposal.workspaceRef) || !REVISION_PATTERN.test(proposal.baseRevision)
|
|
384
|
+
|| typeof proposal.rationale !== "string" || proposal.rationale.trim().length < 3 || proposal.rationale.length > 1000
|
|
385
|
+
|| !proposal.target || proposal.target.humanOwned !== true || !ARTIFACT_REF_PATTERN.test(proposal.target.artifactRef)
|
|
386
|
+
|| !REVISION_PATTERN.test(proposal.target.baseContentHash) || !proposal.source
|
|
387
|
+
|| !/^(?:sha256|heuristic|git):[A-Za-z0-9._-]{7,128}$/.test(proposal.source.sourceRevision)) {
|
|
388
|
+
errors.push(proposalError(sourceRef, "invalid_proposal", "proposal envelope or identity is invalid"));
|
|
389
|
+
}
|
|
390
|
+
if (!safeSecretScan(proposal))
|
|
391
|
+
errors.push(proposalError(sourceRef, "secret_detected", "proposal contains a secret or private-data pattern"));
|
|
392
|
+
if (!proposal.diff || proposal.target?.baseContentHash !== proposal.diff.beforeDigest || proposal.patchFormat !== proposal.diff.format) {
|
|
393
|
+
errors.push(proposalError(sourceRef, "invalid_proposal", "proposal target or patch format is not bound to the exact diff"));
|
|
394
|
+
}
|
|
395
|
+
if (proposal.patchFormat === "rfc6902") {
|
|
396
|
+
const patch = normalizePatch(proposal.patch, sourceRef);
|
|
397
|
+
if (!patch.patch)
|
|
398
|
+
errors.push(...patch.errors);
|
|
399
|
+
else if (proposal.patchDigest !== digest(patch.patch))
|
|
400
|
+
errors.push(proposalError(sourceRef, "patch_digest_mismatch", "proposal patch digest does not match RFC6902 operations"));
|
|
401
|
+
if (!proposal.diff || !Array.isArray(proposal.diff.operations) || !Array.isArray(proposal.patch)
|
|
402
|
+
|| proposal.diff.operations.length !== proposal.patch.length
|
|
403
|
+
|| proposal.diff.operations.some((operation, index) => operation.index !== index
|
|
404
|
+
|| operation.op !== proposal.patch[index]?.op || operation.path !== proposal.patch[index]?.path)) {
|
|
405
|
+
errors.push(proposalError(sourceRef, "invalid_proposal", "RFC6902 diff operations do not match the exact patch"));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else if (proposal.patchFormat === "unified_diff") {
|
|
409
|
+
const patch = validateProjectBrainUnifiedDiffPatch(proposal.patch, sourceRef);
|
|
410
|
+
if (!patch.valid)
|
|
411
|
+
errors.push(...patch.errors);
|
|
412
|
+
else if (proposal.patchDigest !== patch.patchDigest)
|
|
413
|
+
errors.push(proposalError(sourceRef, "patch_digest_mismatch", "proposal patch digest does not match the normalized unified diff"));
|
|
414
|
+
if (!proposal.diff || JSON.stringify(proposal.diff.hunks) !== JSON.stringify(proposal.patch.hunks)) {
|
|
415
|
+
errors.push(proposalError(sourceRef, "invalid_proposal", "unified diff evidence does not match the normalized patch"));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else
|
|
419
|
+
errors.push(proposalError(sourceRef, "invalid_proposal", "proposal patchFormat is unsupported"));
|
|
420
|
+
if (!proposal.approval || proposal.approval.required !== true || proposal.approval.policy !== "fresh_exact_patch"
|
|
421
|
+
|| proposal.approval.reusable !== false || proposal.approval.maxAgeSeconds !== 900
|
|
422
|
+
|| JSON.stringify(proposal.approval.bindingFields) !== JSON.stringify(APPROVAL_BINDINGS)) {
|
|
423
|
+
errors.push(proposalError(sourceRef, "approval_binding_mismatch", "proposal requires a fresh approval bound to every exact identity and patch field"));
|
|
424
|
+
}
|
|
425
|
+
const { proposalRef: _proposalRef, ...preimage } = proposal;
|
|
426
|
+
if (proposal.proposalRef !== `proposal:brain/${(0, canonical_1.sha256)((0, canonical_1.canonicalize)(preimage))}`) {
|
|
427
|
+
errors.push(proposalError(sourceRef, "proposal_ref_mismatch", "proposalRef does not match the canonical proposal envelope"));
|
|
428
|
+
}
|
|
429
|
+
const typeValid = proposal.proposalType === "descriptor_adoption"
|
|
430
|
+
? proposal.requestedCapability === "brain.descriptor.adopt" && proposal.patchFormat === "rfc6902"
|
|
431
|
+
&& proposal.target.kind === "repository_file" && proposal.target.path === ".you-project" && proposal.target.exists === false
|
|
432
|
+
: proposal.proposalType === "personal_override"
|
|
433
|
+
? proposal.requestedCapability === "brain.override.propose" && proposal.patchFormat === "rfc6902"
|
|
434
|
+
&& proposal.target.kind === "project_snapshot" && proposal.target.path === undefined && proposal.target.exists === true
|
|
435
|
+
: proposal.proposalType === "artifact_update"
|
|
436
|
+
&& proposal.requestedCapability === "brain.artifact.update" && proposal.patchFormat === "unified_diff"
|
|
437
|
+
&& proposal.target.kind === "repository_file" && typeof proposal.target.path === "string" && proposal.target.exists === true;
|
|
438
|
+
if (!typeValid)
|
|
439
|
+
errors.push(proposalError(sourceRef, "invalid_proposal", "proposal type, capability, target, and patch format are inconsistent"));
|
|
440
|
+
return errors.length ? { valid: false, errors } : { valid: true, errors: [], proposal };
|
|
441
|
+
}
|
|
442
|
+
function buildProposal(seed) {
|
|
443
|
+
if (!PROJECT_REF_PATTERN.test(seed.projectRef) || !/^machine:sha256:[a-f0-9]{64}$/.test(seed.machineRef)
|
|
444
|
+
|| !/^workspace:[a-f0-9]{64}$/.test(seed.workspaceRef)
|
|
445
|
+
|| !REVISION_PATTERN.test(seed.baseRevision) || !ARTIFACT_REF_PATTERN.test(seed.target.artifactRef)) {
|
|
446
|
+
throw new TypeError("project brain proposal identity is invalid");
|
|
447
|
+
}
|
|
448
|
+
const preimage = {
|
|
449
|
+
$schema: exports.PROJECT_BRAIN_PROPOSAL_SCHEMA,
|
|
450
|
+
schemaVersion: exports.PROJECT_BRAIN_PROPOSAL_VERSION,
|
|
451
|
+
kind: "project-brain-proposal",
|
|
452
|
+
...seed,
|
|
453
|
+
status: "proposed",
|
|
454
|
+
approval: {
|
|
455
|
+
required: true,
|
|
456
|
+
policy: "fresh_exact_patch",
|
|
457
|
+
reusable: false,
|
|
458
|
+
maxAgeSeconds: 900,
|
|
459
|
+
bindingFields: APPROVAL_BINDINGS,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
return { ...preimage, proposalRef: `proposal:brain/${(0, canonical_1.sha256)((0, canonical_1.canonicalize)(preimage))}` };
|
|
463
|
+
}
|
|
464
|
+
function stripFactProvenance(project) {
|
|
465
|
+
return project.artifacts.map((artifact) => ({
|
|
466
|
+
artifactRef: artifact.artifactRef,
|
|
467
|
+
subjectRef: artifact.subjectRef,
|
|
468
|
+
kind: artifact.kind,
|
|
469
|
+
path: artifact.path,
|
|
470
|
+
owner: artifact.owner,
|
|
471
|
+
authority: artifact.authority,
|
|
472
|
+
writePolicy: "read_only",
|
|
473
|
+
provenance: {
|
|
474
|
+
source: artifact.provenance.source,
|
|
475
|
+
sourceRef: artifact.provenance.sourceRef,
|
|
476
|
+
sourceRevision: artifact.provenance.sourceRevision,
|
|
477
|
+
indexedAt: artifact.provenance.indexedAt,
|
|
478
|
+
freshness: artifact.provenance.freshness,
|
|
479
|
+
},
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
const PROJECT_REVISION_FIELDS = [
|
|
483
|
+
"$schema", "schemaVersion", "kind", "projectRef", "slug", "name", "contextRoot",
|
|
484
|
+
"agentRoots", "artifacts", "relationships", "writePolicy", "generatedExcludes", "provenance",
|
|
485
|
+
];
|
|
486
|
+
function withoutVolatile(value) {
|
|
487
|
+
if (Array.isArray(value))
|
|
488
|
+
return value.map(withoutVolatile);
|
|
489
|
+
if (!value || typeof value !== "object")
|
|
490
|
+
return value;
|
|
491
|
+
const result = Object.create(null);
|
|
492
|
+
for (const [key, child] of Object.entries(value))
|
|
493
|
+
if (key !== "indexedAt" && key !== "checkedAt")
|
|
494
|
+
result[key] = withoutVolatile(child);
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
function adoptionDescriptor(scan, project, name) {
|
|
498
|
+
const slug = project.projectRef.slice("project:".length);
|
|
499
|
+
const descriptor = {
|
|
500
|
+
$schema: "https://you.md/schemas/project-brain/v1",
|
|
501
|
+
schemaVersion: "you-md/project-brain/v1",
|
|
502
|
+
kind: "project",
|
|
503
|
+
projectRef: project.projectRef,
|
|
504
|
+
slug,
|
|
505
|
+
name: name?.trim() || slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "),
|
|
506
|
+
contextRoot: project.contextRoot,
|
|
507
|
+
agentRoots: project.artifacts.filter((artifact) => artifact.kind === "agent").map((artifact) => artifact.path),
|
|
508
|
+
artifacts: stripFactProvenance(project),
|
|
509
|
+
relationships: [],
|
|
510
|
+
writePolicy: { default: "read_only", managedPaths: [], taskWrite: { enabled: false } },
|
|
511
|
+
generatedExcludes: [...scan.generatedExcludes],
|
|
512
|
+
provenance: {
|
|
513
|
+
source: project.management === "legacy-adopted" ? "legacy-adoption" : "declared",
|
|
514
|
+
descriptorPath: ".you-project",
|
|
515
|
+
sourceRevision: scan.revision.digest,
|
|
516
|
+
indexedAt: project.provenance.indexedAt,
|
|
517
|
+
freshness: project.provenance.freshness,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
const preimage = Object.fromEntries(PROJECT_REVISION_FIELDS.map((field) => [field, withoutVolatile(descriptor[field])]));
|
|
521
|
+
descriptor.revision = {
|
|
522
|
+
canonicalization: "RFC8785",
|
|
523
|
+
hashAlgorithm: "SHA-256",
|
|
524
|
+
preimageFields: [...PROJECT_REVISION_FIELDS],
|
|
525
|
+
volatileExclusions: [
|
|
526
|
+
"provenance.indexedAt",
|
|
527
|
+
"provenance.freshness.checkedAt",
|
|
528
|
+
"artifacts[*].provenance.indexedAt",
|
|
529
|
+
"artifacts[*].provenance.freshness.checkedAt",
|
|
530
|
+
],
|
|
531
|
+
digest: `sha256:${(0, canonical_1.sha256)((0, canonical_1.canonicalize)(preimage))}`,
|
|
532
|
+
};
|
|
533
|
+
return normalizeJson(descriptor);
|
|
534
|
+
}
|
|
535
|
+
/** Generate review-only `.you-project` adoption proposals. No target file is created or changed. */
|
|
536
|
+
function proposeProjectBrainAdoption(scan, options = {}) {
|
|
537
|
+
const proposals = [];
|
|
538
|
+
const errors = [];
|
|
539
|
+
const projects = scan.projects.filter((project) => !options.projectRef || project.projectRef === options.projectRef);
|
|
540
|
+
if (options.projectRef && projects.length === 0)
|
|
541
|
+
errors.push(proposalError(options.projectRef, "unknown_project", "project is not present in the exact scan"));
|
|
542
|
+
for (const project of projects) {
|
|
543
|
+
if (project.management === "declared" || project.descriptorPath === ".you-project") {
|
|
544
|
+
errors.push(proposalError(project.projectRef, "not_adoptable", "project already has a declared .you-project descriptor"));
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (project.artifacts.length === 0) {
|
|
548
|
+
errors.push(proposalError(project.projectRef, "not_adoptable", "project has no attributable artifacts for descriptor adoption"));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const descriptor = adoptionDescriptor(scan, project, options.name);
|
|
552
|
+
const patchResult = validateProjectBrainPatch(null, [{ op: "add", path: "", value: descriptor }], project.projectRef);
|
|
553
|
+
if (!patchResult.valid || !patchResult.patch || !patchResult.patchDigest || !patchResult.diff) {
|
|
554
|
+
errors.push(...patchResult.errors);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const slug = project.projectRef.slice("project:".length);
|
|
558
|
+
proposals.push(buildProposal({
|
|
559
|
+
proposalType: "descriptor_adoption",
|
|
560
|
+
projectRef: project.projectRef,
|
|
561
|
+
machineRef: scan.workspaceIdentity.machineRef,
|
|
562
|
+
workspaceRef: scan.workspaceRef,
|
|
563
|
+
baseRevision: scan.revision.digest,
|
|
564
|
+
requestedCapability: "brain.descriptor.adopt",
|
|
565
|
+
patchFormat: "rfc6902",
|
|
566
|
+
patchDigest: patchResult.patchDigest,
|
|
567
|
+
rationale: options.rationale?.trim() || `Adopt ${project.projectRef} with a reviewable .you-project descriptor.`,
|
|
568
|
+
target: {
|
|
569
|
+
kind: "repository_file",
|
|
570
|
+
artifactRef: `artifact:${slug}/project-descriptor`,
|
|
571
|
+
path: ".you-project",
|
|
572
|
+
owner: "repository",
|
|
573
|
+
authority: "canonical",
|
|
574
|
+
humanOwned: true,
|
|
575
|
+
exists: false,
|
|
576
|
+
baseContentHash: digest(null),
|
|
577
|
+
},
|
|
578
|
+
patch: patchResult.patch,
|
|
579
|
+
diff: patchResult.diff,
|
|
580
|
+
source: {
|
|
581
|
+
kind: project.management === "legacy-adopted" ? "legacy_marker" : "heuristic_scan",
|
|
582
|
+
sourceRef: project.provenance.sourceRef,
|
|
583
|
+
sourceRevision: project.provenance.sourceRevision,
|
|
584
|
+
...(project.legacyMarkerPath ? { path: project.legacyMarkerPath } : {}),
|
|
585
|
+
},
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
proposals.sort((a, b) => a.proposalRef.localeCompare(b.proposalRef));
|
|
589
|
+
errors.sort((a, b) => a.sourceRef.localeCompare(b.sourceRef) || a.code.localeCompare(b.code));
|
|
590
|
+
return { proposals, errors };
|
|
591
|
+
}
|
|
592
|
+
function parseOverride(value) {
|
|
593
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
594
|
+
return null;
|
|
595
|
+
const input = value;
|
|
596
|
+
const keys = new Set(["$schema", "schemaVersion", "kind", "projectRef", "baseRevision", "rationale", "patch"]);
|
|
597
|
+
if (Object.keys(input).some((key) => !keys.has(key))
|
|
598
|
+
|| input.$schema !== "https://you.md/schemas/project-brain-override/v1"
|
|
599
|
+
|| input.schemaVersion !== exports.PROJECT_BRAIN_OVERRIDE_VERSION
|
|
600
|
+
|| input.kind !== "project-brain-override"
|
|
601
|
+
|| typeof input.projectRef !== "string" || !PROJECT_REF_PATTERN.test(input.projectRef)
|
|
602
|
+
|| typeof input.baseRevision !== "string" || !REVISION_PATTERN.test(input.baseRevision)
|
|
603
|
+
|| typeof input.rationale !== "string" || input.rationale.trim().length < 3 || input.rationale.length > 1000
|
|
604
|
+
|| !Array.isArray(input.patch))
|
|
605
|
+
return null;
|
|
606
|
+
return {
|
|
607
|
+
$schema: input.$schema,
|
|
608
|
+
schemaVersion: input.schemaVersion,
|
|
609
|
+
kind: input.kind,
|
|
610
|
+
projectRef: input.projectRef,
|
|
611
|
+
baseRevision: input.baseRevision,
|
|
612
|
+
rationale: input.rationale.trim(),
|
|
613
|
+
patch: input.patch,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/** Import explicit personal overrides as exact proposals only. No patch is applied here. */
|
|
617
|
+
function importProjectBrainOverrides(scan, sources) {
|
|
618
|
+
const proposals = [];
|
|
619
|
+
const errors = [];
|
|
620
|
+
const addError = (sourcePath, error) => {
|
|
621
|
+
errors.push({ ...error, sourcePath });
|
|
622
|
+
};
|
|
623
|
+
for (const source of [...sources].sort((a, b) => a.sourcePath.localeCompare(b.sourcePath))) {
|
|
624
|
+
if (!OVERRIDE_SOURCE_PATTERN.test(source.sourcePath) || source.sourcePath.includes("..") || source.sourcePath.includes("\\")) {
|
|
625
|
+
addError(source.sourcePath, proposalError(source.sourcePath, "unsafe_source_path", "override imports must be explicit JSON under overrides/ or notes/"));
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (!safeSecretScan(source.content)) {
|
|
629
|
+
addError(source.sourcePath, proposalError(source.sourcePath, "secret_detected", "override content contains a secret or private-data pattern"));
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
let raw;
|
|
633
|
+
try {
|
|
634
|
+
raw = JSON.parse(source.content);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
addError(source.sourcePath, proposalError(source.sourcePath, "invalid_json", "override content is not valid JSON"));
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const override = parseOverride(raw);
|
|
641
|
+
if (!override) {
|
|
642
|
+
addError(source.sourcePath, proposalError(source.sourcePath, "invalid_override", "override does not match the proposal-only v1 contract"));
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const project = scan.projects.find((candidate) => candidate.projectRef === override.projectRef);
|
|
646
|
+
if (!project) {
|
|
647
|
+
addError(source.sourcePath, proposalError(source.sourcePath, "unknown_project", "override projectRef is not present in this scan"));
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (override.baseRevision !== scan.revision.digest) {
|
|
651
|
+
addError(source.sourcePath, proposalError(source.sourcePath, "stale_base", "override baseRevision does not match the exact scan revision"));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const patchResult = validateProjectBrainPatch(project, override.patch, source.sourcePath);
|
|
655
|
+
if (!patchResult.valid || !patchResult.patch || !patchResult.patchDigest || !patchResult.diff) {
|
|
656
|
+
for (const error of patchResult.errors)
|
|
657
|
+
addError(source.sourcePath, error);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const slug = project.projectRef.slice("project:".length);
|
|
661
|
+
proposals.push(buildProposal({
|
|
662
|
+
proposalType: "personal_override",
|
|
663
|
+
projectRef: project.projectRef,
|
|
664
|
+
machineRef: scan.workspaceIdentity.machineRef,
|
|
665
|
+
workspaceRef: scan.workspaceRef,
|
|
666
|
+
baseRevision: scan.revision.digest,
|
|
667
|
+
requestedCapability: "brain.override.propose",
|
|
668
|
+
patchFormat: "rfc6902",
|
|
669
|
+
patchDigest: patchResult.patchDigest,
|
|
670
|
+
rationale: override.rationale,
|
|
671
|
+
target: {
|
|
672
|
+
kind: "project_snapshot",
|
|
673
|
+
artifactRef: `artifact:${slug}/project-snapshot`,
|
|
674
|
+
owner: project.provenance.owner,
|
|
675
|
+
authority: project.provenance.authority,
|
|
676
|
+
humanOwned: true,
|
|
677
|
+
exists: true,
|
|
678
|
+
baseContentHash: digest(normalizeJson(project)),
|
|
679
|
+
},
|
|
680
|
+
patch: patchResult.patch,
|
|
681
|
+
diff: patchResult.diff,
|
|
682
|
+
source: {
|
|
683
|
+
kind: "override",
|
|
684
|
+
sourceRef: `subject:youmd/${slug}/overrides`,
|
|
685
|
+
sourceRevision: `sha256:${(0, canonical_1.sha256)(source.content)}`,
|
|
686
|
+
path: source.sourcePath,
|
|
687
|
+
},
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
proposals.sort((a, b) => a.proposalRef.localeCompare(b.proposalRef));
|
|
691
|
+
errors.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath) || a.code.localeCompare(b.code));
|
|
692
|
+
return { proposals, errors };
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=proposal.js.map
|