@deftai/directive-core 0.98.0 → 0.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/authz/classify.js
CHANGED
|
@@ -162,6 +162,146 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
|
|
|
162
162
|
]);
|
|
163
163
|
/** Kill-switch / permanent opt-out basenames agents must not plant under UAT (#3186 / #3039). */
|
|
164
164
|
const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
|
|
165
|
+
/**
|
|
166
|
+
* Downloaders / decoders that can plant files without shell redirects (#3206).
|
|
167
|
+
* Not in INDIRECT_WRITE_BINS: those feed hasWriteShape and would classify bare
|
|
168
|
+
* `curl $URL` as a store write via opaque-expansion heuristics.
|
|
169
|
+
*/
|
|
170
|
+
const DOWNLOADER_DECODER_BINS = new Set(["curl", "wget", "xxd", "openssl"]);
|
|
171
|
+
/**
|
|
172
|
+
* File destination flags for downloaders/decoders (#3206).
|
|
173
|
+
* normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
|
|
174
|
+
*/
|
|
175
|
+
const DOWNLOADER_FILE_DEST_FLAGS = new Set([
|
|
176
|
+
"-o",
|
|
177
|
+
"--output",
|
|
178
|
+
"--output-document",
|
|
179
|
+
"-out",
|
|
180
|
+
"--out",
|
|
181
|
+
]);
|
|
182
|
+
/** Directory destination flags (curl --output-dir / wget -P); bin-scoped below. */
|
|
183
|
+
const CURL_DIR_DEST_FLAGS = new Set(["--output-dir"]);
|
|
184
|
+
const WGET_DIR_DEST_FLAGS = new Set(["-p", "--directory-prefix"]);
|
|
185
|
+
/** Final path segment of a token (path-qualified bins / .exe). */
|
|
186
|
+
function binBareName(token) {
|
|
187
|
+
const pathish = token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
188
|
+
const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : pathish;
|
|
189
|
+
return base.endsWith(".exe") ? base.slice(0, -4) : base;
|
|
190
|
+
}
|
|
191
|
+
/** True when token names a downloader/decoder bin (bare or path-qualified). */
|
|
192
|
+
function isDownloaderDecoderBin(token) {
|
|
193
|
+
const n = normalizeToken(token);
|
|
194
|
+
if (n.startsWith("-"))
|
|
195
|
+
return false;
|
|
196
|
+
return DOWNLOADER_DECODER_BINS.has(binBareName(token));
|
|
197
|
+
}
|
|
198
|
+
function isDownloaderDestFlag(flag, bin) {
|
|
199
|
+
if (DOWNLOADER_FILE_DEST_FLAGS.has(flag))
|
|
200
|
+
return true;
|
|
201
|
+
if (bin === "curl" && CURL_DIR_DEST_FLAGS.has(flag))
|
|
202
|
+
return true;
|
|
203
|
+
if (bin === "wget" && WGET_DIR_DEST_FLAGS.has(flag))
|
|
204
|
+
return true;
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Destinations from curl/wget/xxd/openssl via -o/--output/-O/-out/--output-dir/-P
|
|
209
|
+
* (separate, =value, or attached short form) and xxd -r path-like write positionals (#3206).
|
|
210
|
+
* openssl uses flags only (no positional dests — avoids treating -in paths as writes).
|
|
211
|
+
* O(n) token walk — no nested-quantifier regex on untrusted input.
|
|
212
|
+
*/
|
|
213
|
+
function downloaderDecoderDestinations(tokens) {
|
|
214
|
+
const dests = [];
|
|
215
|
+
let i = 0;
|
|
216
|
+
while (i < tokens.length) {
|
|
217
|
+
if (!isDownloaderDecoderBin(tokens[i])) {
|
|
218
|
+
i++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const bin = binBareName(tokens[i]);
|
|
222
|
+
i++;
|
|
223
|
+
// xxd reverse mode writes; without -r, path positionals are dump inputs (read).
|
|
224
|
+
let xxdReverse = false;
|
|
225
|
+
while (i < tokens.length) {
|
|
226
|
+
const raw = tokens[i];
|
|
227
|
+
const n = normalizeToken(raw);
|
|
228
|
+
// New bare bin starts another command segment (not pathish ./wget operands).
|
|
229
|
+
if (!n.startsWith("-") &&
|
|
230
|
+
!raw.includes("/") &&
|
|
231
|
+
!raw.includes("\\") &&
|
|
232
|
+
isDownloaderDecoderBin(raw)) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (bin === "xxd" && (n === "-r" || n === "--reverse")) {
|
|
236
|
+
xxdReverse = true;
|
|
237
|
+
i++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
// Skip openssl/xxd input flags + value so -in PATH is not a write dest.
|
|
241
|
+
if (n === "-in" || n === "--in" || n === "-inform" || n === "--inform") {
|
|
242
|
+
const next = tokens[i + 1];
|
|
243
|
+
if (next !== undefined && !String(next).startsWith("-")) {
|
|
244
|
+
i += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
i++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// --flag=value (and rare -out=value)
|
|
251
|
+
if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
|
|
252
|
+
const eq = raw.indexOf("=");
|
|
253
|
+
const flag = normalizeToken(raw.slice(0, eq));
|
|
254
|
+
if (isDownloaderDestFlag(flag, bin)) {
|
|
255
|
+
dests.push(pathishToken(raw.slice(eq + 1)));
|
|
256
|
+
}
|
|
257
|
+
i++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
|
|
261
|
+
if (n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
|
|
262
|
+
if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output")) {
|
|
263
|
+
dests.push(pathishToken(raw.slice(4)));
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (n.startsWith("-o") && !n.startsWith("-out") && n.length > 2) {
|
|
268
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// wget attached -Pdir
|
|
273
|
+
if (bin === "wget" && n.startsWith("-p") && n.length > 2 && !n.startsWith("-proxy")) {
|
|
274
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
275
|
+
i++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P
|
|
280
|
+
if (isDownloaderDestFlag(n, bin)) {
|
|
281
|
+
const next = tokens[i + 1];
|
|
282
|
+
if (next !== undefined && !String(next).startsWith("-")) {
|
|
283
|
+
dests.push(pathishToken(next));
|
|
284
|
+
i += 2;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
i++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// xxd -r only: path-like write positionals (not http(s) URLs). openssl: flags only.
|
|
291
|
+
if (bin === "xxd" && xxdReverse && !n.startsWith("-")) {
|
|
292
|
+
const p = pathishToken(raw);
|
|
293
|
+
if ((p.includes("/") || p.startsWith(".") || p.includes("\\")) &&
|
|
294
|
+
!p.startsWith("http:") &&
|
|
295
|
+
!p.startsWith("https:") &&
|
|
296
|
+
!p.startsWith("ftp:")) {
|
|
297
|
+
dests.push(p);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
i++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return dests;
|
|
304
|
+
}
|
|
165
305
|
function authzSubcommandFromToken(token) {
|
|
166
306
|
const t = normalizeToken(token);
|
|
167
307
|
if (t.startsWith("authz:")) {
|
|
@@ -273,6 +413,13 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
273
413
|
return true;
|
|
274
414
|
}
|
|
275
415
|
}
|
|
416
|
+
// Downloader/decoder destinations: curl -o .deft-directive-disable, wget -O, … (#3206).
|
|
417
|
+
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
418
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
419
|
+
if (dest.includes(name))
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
276
423
|
// Write/destructive bins with a kill-switch path argument (touch, New-Item, …).
|
|
277
424
|
const killWriteBins = new Set([
|
|
278
425
|
...INDIRECT_WRITE_BINS,
|
|
@@ -512,6 +659,11 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
512
659
|
return true;
|
|
513
660
|
}
|
|
514
661
|
}
|
|
662
|
+
// Downloader/decoder destinations under .deft/authz (#3206 residual after #3186).
|
|
663
|
+
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
664
|
+
if (dest.includes(".deft/authz"))
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
515
667
|
return false;
|
|
516
668
|
}
|
|
517
669
|
/** Write/destructive shell bins (token match after normalizeToken). */
|
|
@@ -61,6 +61,27 @@ export declare function normalizeRepoRelPath(p: string): string;
|
|
|
61
61
|
* Git C-quoting / slash folding may produce (Greptile conf=4 residual).
|
|
62
62
|
*/
|
|
63
63
|
export declare function changedSetHasPath(changedSet: ReadonlySet<string>, rel: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
|
|
66
|
+
* Returns null when schema fields required for authorization are missing/malformed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseApprovedScopeRecordRaw(raw: string): ApprovedScopeRecord | null;
|
|
69
|
+
/**
|
|
70
|
+
* True when the merge-base approved-scope record authorizes the current scope and
|
|
71
|
+
* the current disk record is semantically unchanged from that base authority (#3205).
|
|
72
|
+
*
|
|
73
|
+
* Authority comes from the approval record on the base, not from whether the active
|
|
74
|
+
* xBRIEF path existed on the base (pending→active is the normal first activation).
|
|
75
|
+
*/
|
|
76
|
+
export declare function baseApprovalAuthorizesCurrent(input: {
|
|
77
|
+
readonly projectRoot: string;
|
|
78
|
+
readonly baseRef: string | null;
|
|
79
|
+
readonly approvalRecordRel: string;
|
|
80
|
+
readonly planId: string;
|
|
81
|
+
readonly xbriefRelPath: string;
|
|
82
|
+
readonly currentDigest: string;
|
|
83
|
+
readonly currentApproved: ApprovedScopeRecord;
|
|
84
|
+
}): boolean;
|
|
64
85
|
/**
|
|
65
86
|
* Pure evaluation of one active xBRIEF against its approved baseline.
|
|
66
87
|
* Exported for unit tests without git.
|
|
@@ -203,9 +203,83 @@ function listActiveXbriefPaths(projectRoot) {
|
|
|
203
203
|
}
|
|
204
204
|
function remediationForExpansion() {
|
|
205
205
|
return ("Renew human approval: re-record the approved-scope digest after operator review " +
|
|
206
|
-
"(`task scope:record-approved-scope
|
|
207
|
-
"with humanApproval stamp).
|
|
208
|
-
"
|
|
206
|
+
"(`task scope:record-approved-scope -- <xbrief-path> --actor <you>` writes " +
|
|
207
|
+
"`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
|
|
208
|
+
"approval on the merge base (or a prior PR) before expanding or activating the " +
|
|
209
|
+
"scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
|
|
210
|
+
"does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md.");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
|
|
214
|
+
* Returns null when schema fields required for authorization are missing/malformed.
|
|
215
|
+
*/
|
|
216
|
+
export function parseApprovedScopeRecordRaw(raw) {
|
|
217
|
+
try {
|
|
218
|
+
const data = JSON.parse(raw);
|
|
219
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
220
|
+
return null;
|
|
221
|
+
const rec = data;
|
|
222
|
+
if (rec.schemaVersion !== undefined && rec.schemaVersion !== 1)
|
|
223
|
+
return null;
|
|
224
|
+
if (typeof rec.planId !== "string" || rec.planId.trim().length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
if (typeof rec.xbriefRelPath !== "string" || rec.xbriefRelPath.trim().length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (typeof rec.fileScopeDigest !== "string" || rec.fileScopeDigest.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(rec.fileScope))
|
|
233
|
+
return null;
|
|
234
|
+
// Digest must match the recorded path list — never trust a forged digest alone (#3205 Greptile).
|
|
235
|
+
const scopePaths = rec.fileScope.filter((x) => typeof x === "string");
|
|
236
|
+
const expected = computeFileScopeDigest(scopePaths);
|
|
237
|
+
if (rec.fileScopeDigest !== expected)
|
|
238
|
+
return null;
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* True when the merge-base approved-scope record authorizes the current scope and
|
|
247
|
+
* the current disk record is semantically unchanged from that base authority (#3205).
|
|
248
|
+
*
|
|
249
|
+
* Authority comes from the approval record on the base, not from whether the active
|
|
250
|
+
* xBRIEF path existed on the base (pending→active is the normal first activation).
|
|
251
|
+
*/
|
|
252
|
+
export function baseApprovalAuthorizesCurrent(input) {
|
|
253
|
+
if (input.baseRef === null || input.baseRef === "")
|
|
254
|
+
return false;
|
|
255
|
+
const baseRaw = readRepoFileAtRef(input.projectRoot, input.baseRef, input.approvalRecordRel);
|
|
256
|
+
if (baseRaw === null)
|
|
257
|
+
return false;
|
|
258
|
+
const baseRec = parseApprovedScopeRecordRaw(baseRaw);
|
|
259
|
+
if (baseRec === null)
|
|
260
|
+
return false;
|
|
261
|
+
if (!isHumanApprovalStamp(baseRec.humanApproval))
|
|
262
|
+
return false;
|
|
263
|
+
if (baseRec.planId !== input.planId)
|
|
264
|
+
return false;
|
|
265
|
+
if (normalizeRepoRelPath(baseRec.xbriefRelPath) !== normalizeRepoRelPath(input.xbriefRelPath)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
// Base record must authorize the *current* file_scope (digest match).
|
|
269
|
+
if (baseRec.fileScopeDigest !== input.currentDigest)
|
|
270
|
+
return false;
|
|
271
|
+
// Current on-disk/injected record must not diverge from base authority fields.
|
|
272
|
+
if (input.currentApproved.fileScopeDigest !== baseRec.fileScopeDigest)
|
|
273
|
+
return false;
|
|
274
|
+
if (input.currentApproved.planId !== baseRec.planId)
|
|
275
|
+
return false;
|
|
276
|
+
if (normalizeRepoRelPath(input.currentApproved.xbriefRelPath) !==
|
|
277
|
+
normalizeRepoRelPath(baseRec.xbriefRelPath)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (!isHumanApprovalStamp(input.currentApproved.humanApproval))
|
|
281
|
+
return false;
|
|
282
|
+
return true;
|
|
209
283
|
}
|
|
210
284
|
function configError(message) {
|
|
211
285
|
return { exitCode: 2, findings: [], message };
|
|
@@ -250,13 +324,42 @@ export function evaluateOneScopeProvenance(input) {
|
|
|
250
324
|
isHumanApprovalStamp(input.approved.humanApproval)) {
|
|
251
325
|
return null;
|
|
252
326
|
}
|
|
327
|
+
// Matching digest without human origin: empty-scope body edits may soft-warn
|
|
328
|
+
// via the missing-digest path only when no usable approval; agent/malformed
|
|
329
|
+
// stamps must not authorize non-empty scopes (#3205).
|
|
330
|
+
if (input.approved.fileScopeDigest === currentDigest) {
|
|
331
|
+
if (currentScope.length === 0) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
if (!isHumanApprovalStamp(input.approved.humanApproval)) {
|
|
335
|
+
return {
|
|
336
|
+
xbriefRelPath: input.xbriefRelPath,
|
|
337
|
+
planId,
|
|
338
|
+
kind: "active-xbrief-modified-without-digest",
|
|
339
|
+
expandedPaths: currentScope,
|
|
340
|
+
detail: "active xBRIEF modified with a non-human (agent/missing) approved-scope stamp; " +
|
|
341
|
+
"only humanApproval stamps authorize non-empty file_scope",
|
|
342
|
+
remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
|
|
343
|
+
"<xbrief-path> --actor <you>` (#3145 / #3205).",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
253
347
|
const expanded = scopeExpansion(input.approved.fileScope, currentScope);
|
|
254
348
|
if (expanded.length === 0) {
|
|
255
|
-
// Scope shrink or
|
|
256
|
-
|
|
257
|
-
|
|
349
|
+
// Scope shrink or digest noise without path expansion — OK for v1 when human-stamped.
|
|
350
|
+
// Non-empty current scope still requires human origin (agent shrink must not bypass #3205).
|
|
351
|
+
if (currentScope.length > 0 && !isHumanApprovalStamp(input.approved.humanApproval)) {
|
|
352
|
+
return {
|
|
353
|
+
xbriefRelPath: input.xbriefRelPath,
|
|
354
|
+
planId,
|
|
355
|
+
kind: "active-xbrief-modified-without-digest",
|
|
356
|
+
expandedPaths: currentScope,
|
|
357
|
+
detail: "active xBRIEF modified with a non-human approved-scope stamp (scope shrink/noise path); " +
|
|
358
|
+
"only humanApproval stamps authorize non-empty file_scope",
|
|
359
|
+
remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
|
|
360
|
+
"<xbrief-path> --actor <you>` (#3145 / #3205).",
|
|
361
|
+
};
|
|
258
362
|
}
|
|
259
|
-
// Digest mismatch without path expansion (reorder/noise) — still OK for v1
|
|
260
363
|
return null;
|
|
261
364
|
}
|
|
262
365
|
// Expansion without renewed human approval = self-authorization
|
|
@@ -303,10 +406,18 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
|
|
|
303
406
|
if (baseRef === undefined || baseRef === "" || baseRef === "HEAD") {
|
|
304
407
|
const resolved = resolveDefaultBaseRef(root);
|
|
305
408
|
if (resolved === null) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
409
|
+
// Greenfield / single-commit consumer trees often have no origin/* and no
|
|
410
|
+
// default-branch ref yet. Fail closed only when the caller demanded an
|
|
411
|
+
// explicit --base-ref; otherwise soft-skip (same posture as non-git trees)
|
|
412
|
+
// so verify:scope-provenance does not brick `task check` on init (#3205 smoke).
|
|
413
|
+
return {
|
|
414
|
+
exitCode: 0,
|
|
415
|
+
findings: [],
|
|
416
|
+
message: "verify_scope_provenance: skipped -- no merge-base ref found " +
|
|
417
|
+
"(origin/master|main, DEFT_BASE_REF, or GITHUB_BASE_REF). " +
|
|
418
|
+
"Fetch the default branch or pass --base-ref <ref> before enforcing " +
|
|
419
|
+
"PR scope expansion (#3145 / #3205).",
|
|
420
|
+
};
|
|
310
421
|
}
|
|
311
422
|
baseRef = resolved;
|
|
312
423
|
}
|
|
@@ -412,38 +523,36 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
|
|
|
412
523
|
return (n.includes("/approved-scope/") &&
|
|
413
524
|
(n.endsWith(`/${safe}.json`) || n.endsWith(`${safe}.json`)));
|
|
414
525
|
});
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
526
|
+
// Disk-only / concurrent-rewrite inference (#3205):
|
|
527
|
+
// Authority is the *approval record on the merge base*, not whether the
|
|
528
|
+
// active xBRIEF path existed there. pending→active leaves the active path
|
|
529
|
+
// absent on base; treating that as an approval rewrite is a false positive.
|
|
530
|
+
// Fail closed when base approval is missing, malformed, agent-stamped,
|
|
531
|
+
// path/plan/digest mismatched, or the current record diverged from base.
|
|
532
|
+
// Same-PR git changes still hard-fail via approvalInGitChange.
|
|
419
533
|
let approvalDiskOnly = false;
|
|
420
534
|
if (modified &&
|
|
421
535
|
approved !== null &&
|
|
422
536
|
renewed === null &&
|
|
423
537
|
approvalRecordRel !== null &&
|
|
538
|
+
planId !== null &&
|
|
424
539
|
!approvalInGitChange &&
|
|
425
540
|
existsSync(join(root, approvalRecordRel)) &&
|
|
426
541
|
isHumanApprovalStamp(approved.humanApproval)) {
|
|
427
542
|
const currentDigest = computeFileScopeDigest(normalizeFileScope(extractFileScope(payload)));
|
|
428
543
|
if (approved.fileScopeDigest === currentDigest) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
544
|
+
const baseAuthorizes = baseApprovalAuthorizesCurrent({
|
|
545
|
+
projectRoot: root,
|
|
546
|
+
baseRef: discoveryBaseRef,
|
|
547
|
+
approvalRecordRel,
|
|
548
|
+
planId,
|
|
549
|
+
xbriefRelPath: rel,
|
|
550
|
+
currentDigest,
|
|
551
|
+
currentApproved: approved,
|
|
552
|
+
});
|
|
553
|
+
if (!baseAuthorizes) {
|
|
432
554
|
approvalDiskOnly = true;
|
|
433
555
|
}
|
|
434
|
-
else {
|
|
435
|
-
try {
|
|
436
|
-
const basePayload = JSON.parse(baseRaw);
|
|
437
|
-
const baseDigest = computeFileScopeDigest(normalizeFileScope(extractFileScope(basePayload)));
|
|
438
|
-
// Only concurrent-rewrite when file-scope actually grew/changed.
|
|
439
|
-
if (baseDigest !== currentDigest) {
|
|
440
|
-
approvalDiskOnly = true;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
approvalDiskOnly = true;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
556
|
}
|
|
448
557
|
}
|
|
449
558
|
const approvalRecordRewritten = approvalInGitChange || approvalDiskOnly;
|
|
@@ -463,8 +572,9 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
|
|
|
463
572
|
expandedPaths: currentScope,
|
|
464
573
|
detail: "approved-scope record rewritten in the same change set as the active xBRIEF; " +
|
|
465
574
|
"cannot self-authorize via concurrent approval rewrite",
|
|
466
|
-
remediation: "
|
|
467
|
-
"
|
|
575
|
+
remediation: "Commit human approval via `task scope:record-approved-scope` on the merge base " +
|
|
576
|
+
"(or a prior PR), then activate/expand without rewriting the approval in this " +
|
|
577
|
+
"change set. Same-PR approval rewrites do not authorize expansion (#3145 / #3205).",
|
|
468
578
|
});
|
|
469
579
|
continue;
|
|
470
580
|
}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* scope-provenance package surface (#3145).
|
|
3
3
|
*/
|
|
4
4
|
export { APPROVED_SCOPE_DIR, type ApprovedScopeRecord, approvedScopeDir, approvedScopeRecordPath, buildApprovedScopeRecord, computeFileScopeDigest, computeTextDigest, extractFileScope, extractPlanId, isHumanApprovalStamp, listApprovedScopeRecords, normalizeFileScope, readApprovedScopeRecord, scopeExpansion, writeApprovedScopeRecord, } from "./digest.js";
|
|
5
|
-
export { evaluateOneScopeProvenance, evaluateScopeProvenance, resolveDefaultBaseRef, type ScopeProvenanceFinding, type ScopeProvenanceOptions, type ScopeProvenanceResult, type ScopeProvenanceViolationKind, } from "./evaluate.js";
|
|
5
|
+
export { baseApprovalAuthorizesCurrent, evaluateOneScopeProvenance, evaluateScopeProvenance, parseApprovedScopeRecordRaw, resolveDefaultBaseRef, type ScopeProvenanceFinding, type ScopeProvenanceOptions, type ScopeProvenanceResult, type ScopeProvenanceViolationKind, } from "./evaluate.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* scope-provenance package surface (#3145).
|
|
3
3
|
*/
|
|
4
4
|
export { APPROVED_SCOPE_DIR, approvedScopeDir, approvedScopeRecordPath, buildApprovedScopeRecord, computeFileScopeDigest, computeTextDigest, extractFileScope, extractPlanId, isHumanApprovalStamp, listApprovedScopeRecords, normalizeFileScope, readApprovedScopeRecord, scopeExpansion, writeApprovedScopeRecord, } from "./digest.js";
|
|
5
|
-
export { evaluateOneScopeProvenance, evaluateScopeProvenance, resolveDefaultBaseRef, } from "./evaluate.js";
|
|
5
|
+
export { baseApprovalAuthorizesCurrent, evaluateOneScopeProvenance, evaluateScopeProvenance, parseApprovedScopeRecordRaw, resolveDefaultBaseRef, } from "./evaluate.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.1",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -354,8 +354,8 @@
|
|
|
354
354
|
"provenance": true
|
|
355
355
|
},
|
|
356
356
|
"dependencies": {
|
|
357
|
-
"@deftai/directive-content": "^0.98.
|
|
358
|
-
"@deftai/directive-types": "^0.98.
|
|
357
|
+
"@deftai/directive-content": "^0.98.1",
|
|
358
|
+
"@deftai/directive-types": "^0.98.1",
|
|
359
359
|
"archiver": "^8.0.0"
|
|
360
360
|
},
|
|
361
361
|
"scripts": {
|