@deftai/directive-core 0.97.0 → 0.98.0
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 +291 -0
- package/dist/check/cached-orchestrator.d.ts +5 -0
- package/dist/check/cached-orchestrator.js +18 -1
- package/dist/check/gate-lists.d.ts +20 -0
- package/dist/check/gate-lists.js +46 -9
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +4 -0
- package/dist/doctor/checks.d.ts +7 -0
- package/dist/doctor/checks.js +83 -0
- package/dist/hooks/dispatcher.d.ts +5 -0
- package/dist/hooks/dispatcher.js +54 -4
- package/dist/init-deposit/hygiene.d.ts +70 -1
- package/dist/init-deposit/hygiene.js +582 -8
- package/dist/init-deposit/scaffold.js +277 -4
- package/dist/init-deposit/skill-discovery-deposit.js +15 -0
- package/dist/policy/check-resume.d.ts +72 -0
- package/dist/policy/check-resume.js +253 -0
- package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
- package/dist/policy/coverage-check-resume-presets.js +228 -0
- package/dist/policy/coverage-debt.d.ts +76 -0
- package/dist/policy/coverage-debt.js +262 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +50 -21
- package/dist/release/auto-hatch.d.ts +114 -0
- package/dist/release/auto-hatch.js +301 -0
- package/dist/release/coverage-debt-ledger.d.ts +22 -0
- package/dist/release/coverage-debt-ledger.js +157 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/pipeline.js +164 -12
- package/dist/release/suite-stamp.d.ts +44 -0
- package/dist/release/suite-stamp.js +133 -0
- package/dist/release/types.d.ts +19 -0
- package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
- package/dist/session/coverage-check-resume-nudge.js +66 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/session-start.js +21 -0
- package/dist/triage/classify/label-mirror.d.ts +31 -1
- package/dist/triage/classify/label-mirror.js +78 -6
- package/dist/triage/help/registry-data.d.ts +6 -6
- package/dist/triage/help/registry-data.js +12 -3
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +54 -0
- package/package.json +3 -3
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and scope briefs are never installer-managed; if they reappear in
|
|
9
9
|
* `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
|
|
10
10
|
*
|
|
11
|
-
* Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
|
|
11
|
+
* Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117, #3193.
|
|
12
12
|
*/
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
@@ -82,11 +82,12 @@ export function installerManagedMatchers() {
|
|
|
82
82
|
{ exact: CODEQL_CONFIG_REL },
|
|
83
83
|
{ exact: CORE_GUARD_WORKFLOW_REL },
|
|
84
84
|
{ exact: "Taskfile.yml" },
|
|
85
|
-
// Upgrade co-travel unit (#3127):
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// @deftai/directive
|
|
89
|
-
//
|
|
85
|
+
// Upgrade co-travel unit (#3127 / #3193): path-allow package/lock + GENERATION
|
|
86
|
+
// so they are not auto-classified as "app". When mixed with .deft/core/**, the
|
|
87
|
+
// deposited guard + classifyMixedCoreAndAppContentAware additionally require
|
|
88
|
+
// package.json to be @deftai/directive* dependency-key pin-only and lockfiles
|
|
89
|
+
// to be pin follow-through (not unrelated direct product deps). GENERATION
|
|
90
|
+
// stays path-only (#3117). True app/product paths still fail (#1430).
|
|
90
91
|
{ exact: "package.json" },
|
|
91
92
|
{ exact: "package-lock.json" },
|
|
92
93
|
{ exact: "pnpm-lock.yaml" },
|
|
@@ -183,10 +184,541 @@ export function installerManagedGuardEre() {
|
|
|
183
184
|
export function isInstallerManagedPath(path) {
|
|
184
185
|
return matchesInstallerManaged(path, installerManagedMatchers());
|
|
185
186
|
}
|
|
187
|
+
/** Paths that path-allowlist for upgrade co-travel but need content checks with core (#3193). */
|
|
188
|
+
export const UPGRADE_PIN_CONTENT_PATHS = [
|
|
189
|
+
"package.json",
|
|
190
|
+
"package-lock.json",
|
|
191
|
+
"pnpm-lock.yaml",
|
|
192
|
+
"yarn.lock",
|
|
193
|
+
];
|
|
194
|
+
/** package.json dependency map fields that may hold @deftai/directive* pins. */
|
|
195
|
+
export const PACKAGE_JSON_DEP_FIELDS = [
|
|
196
|
+
"dependencies",
|
|
197
|
+
"devDependencies",
|
|
198
|
+
"optionalDependencies",
|
|
199
|
+
"peerDependencies",
|
|
200
|
+
];
|
|
201
|
+
/**
|
|
202
|
+
* True when `name` is an `@deftai/directive*` dependency **key** (#3193).
|
|
203
|
+
* Substring hits in scripts/settings values do not qualify.
|
|
204
|
+
*/
|
|
205
|
+
export function isDirectiveDependencyKey(name) {
|
|
206
|
+
return name.startsWith("@deftai/directive");
|
|
207
|
+
}
|
|
208
|
+
function deepEqualJson(a, b) {
|
|
209
|
+
if (a === b)
|
|
210
|
+
return true;
|
|
211
|
+
if (a === null || b === null)
|
|
212
|
+
return a === b;
|
|
213
|
+
if (typeof a !== typeof b)
|
|
214
|
+
return false;
|
|
215
|
+
if (typeof a !== "object")
|
|
216
|
+
return false;
|
|
217
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
218
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
219
|
+
return false;
|
|
220
|
+
return a.every((v, i) => deepEqualJson(v, b[i]));
|
|
221
|
+
}
|
|
222
|
+
const ao = a;
|
|
223
|
+
const bo = b;
|
|
224
|
+
const ak = Object.keys(ao).sort();
|
|
225
|
+
const bk = Object.keys(bo).sort();
|
|
226
|
+
if (ak.length !== bk.length)
|
|
227
|
+
return false;
|
|
228
|
+
for (let i = 0; i < ak.length; i++) {
|
|
229
|
+
if (ak[i] !== bk[i])
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return ak.every((k) => deepEqualJson(ao[k], bo[k]));
|
|
233
|
+
}
|
|
234
|
+
function stripDirectivePinsFromPackageJson(value) {
|
|
235
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
const obj = value;
|
|
239
|
+
const out = {};
|
|
240
|
+
for (const [key, raw] of Object.entries(obj)) {
|
|
241
|
+
if (PACKAGE_JSON_DEP_FIELDS.includes(key) &&
|
|
242
|
+
raw !== null &&
|
|
243
|
+
typeof raw === "object" &&
|
|
244
|
+
!Array.isArray(raw)) {
|
|
245
|
+
const kept = {};
|
|
246
|
+
for (const [depKey, depVal] of Object.entries(raw)) {
|
|
247
|
+
if (!isDirectiveDependencyKey(depKey))
|
|
248
|
+
kept[depKey] = depVal;
|
|
249
|
+
}
|
|
250
|
+
out[key] = kept;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
out[key] = raw;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* package.json co-travel is allowed only when the sole differences are
|
|
260
|
+
* `@deftai/directive*` dependency-key pins under the standard dep maps (#3193).
|
|
261
|
+
* Scripts/settings/metadata that merely contain the substring still fail.
|
|
262
|
+
*/
|
|
263
|
+
export function isPackageJsonDirectivePinOnlyDiff(baseRaw, headRaw) {
|
|
264
|
+
let base;
|
|
265
|
+
let head;
|
|
266
|
+
try {
|
|
267
|
+
base = JSON.parse(baseRaw);
|
|
268
|
+
head = JSON.parse(headRaw);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return deepEqualJson(stripDirectivePinsFromPackageJson(base), stripDirectivePinsFromPackageJson(head));
|
|
274
|
+
}
|
|
275
|
+
function collectDepIdentities(pkg) {
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const field of PACKAGE_JSON_DEP_FIELDS) {
|
|
278
|
+
const block = pkg[field];
|
|
279
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
280
|
+
for (const [k, v] of Object.entries(block)) {
|
|
281
|
+
if (typeof v === "string")
|
|
282
|
+
out[k] = v;
|
|
283
|
+
else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
284
|
+
const version = v.version;
|
|
285
|
+
if (typeof version === "string")
|
|
286
|
+
out[k] = version;
|
|
287
|
+
else
|
|
288
|
+
out[k] = JSON.stringify(v);
|
|
289
|
+
}
|
|
290
|
+
else if (v !== undefined && v !== null) {
|
|
291
|
+
out[k] = String(v);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
function onlyDirectiveDirectDepsDiffer(baseDeps, headDeps) {
|
|
299
|
+
const keys = new Set([...Object.keys(baseDeps), ...Object.keys(headDeps)]);
|
|
300
|
+
for (const key of keys) {
|
|
301
|
+
if (isDirectiveDependencyKey(key))
|
|
302
|
+
continue;
|
|
303
|
+
if ((baseDeps[key] ?? null) !== (headDeps[key] ?? null))
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
function npmLockRootDirectDeps(lock) {
|
|
309
|
+
const packages = lock.packages;
|
|
310
|
+
if (packages !== null && typeof packages === "object" && !Array.isArray(packages)) {
|
|
311
|
+
const root = packages[""];
|
|
312
|
+
if (root !== null && typeof root === "object" && !Array.isArray(root)) {
|
|
313
|
+
return collectDepIdentities(root);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (lock.dependencies !== null &&
|
|
317
|
+
typeof lock.dependencies === "object" &&
|
|
318
|
+
!Array.isArray(lock.dependencies)) {
|
|
319
|
+
const out = {};
|
|
320
|
+
for (const [k, v] of Object.entries(lock.dependencies)) {
|
|
321
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
322
|
+
const version = v.version;
|
|
323
|
+
out[k] = typeof version === "string" ? version : JSON.stringify(v);
|
|
324
|
+
}
|
|
325
|
+
else if (typeof v === "string") {
|
|
326
|
+
out[k] = v;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
/** Strip @deftai/directive* keys recursively from an npm v1 dependencies tree. */
|
|
334
|
+
function stripDirectiveFromNpmV1Deps(value) {
|
|
335
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
336
|
+
return value;
|
|
337
|
+
const obj = value;
|
|
338
|
+
const out = {};
|
|
339
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
340
|
+
if (isDirectiveDependencyKey(k))
|
|
341
|
+
continue;
|
|
342
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
343
|
+
const entry = v;
|
|
344
|
+
const next = { ...entry };
|
|
345
|
+
if (entry.dependencies !== undefined) {
|
|
346
|
+
next.dependencies = stripDirectiveFromNpmV1Deps(entry.dependencies);
|
|
347
|
+
}
|
|
348
|
+
out[k] = next;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
out[k] = v;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* package-lock.json follow-through: non-@deftai/directive* root direct dependency
|
|
358
|
+
* identities (and their `node_modules/<name>` package entries when present) must
|
|
359
|
+
* be unchanged. Directive pin identity + transitive/resolution churn may change.
|
|
360
|
+
*/
|
|
361
|
+
export function isPackageLockDirectivePinFollowThrough(baseRaw, headRaw) {
|
|
362
|
+
let base;
|
|
363
|
+
let head;
|
|
364
|
+
try {
|
|
365
|
+
base = JSON.parse(baseRaw);
|
|
366
|
+
head = JSON.parse(headRaw);
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
if (base === null ||
|
|
372
|
+
head === null ||
|
|
373
|
+
typeof base !== "object" ||
|
|
374
|
+
typeof head !== "object" ||
|
|
375
|
+
Array.isArray(base) ||
|
|
376
|
+
Array.isArray(head)) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const baseObj = base;
|
|
380
|
+
const headObj = head;
|
|
381
|
+
const baseRoot = npmLockRootDirectDeps(baseObj);
|
|
382
|
+
const headRoot = npmLockRootDirectDeps(headObj);
|
|
383
|
+
if (!onlyDirectiveDirectDepsDiffer(baseRoot, headRoot))
|
|
384
|
+
return false;
|
|
385
|
+
const basePkgs = baseObj.packages !== null &&
|
|
386
|
+
typeof baseObj.packages === "object" &&
|
|
387
|
+
!Array.isArray(baseObj.packages)
|
|
388
|
+
? baseObj.packages
|
|
389
|
+
: {};
|
|
390
|
+
const headPkgs = headObj.packages !== null &&
|
|
391
|
+
typeof headObj.packages === "object" &&
|
|
392
|
+
!Array.isArray(headObj.packages)
|
|
393
|
+
? headObj.packages
|
|
394
|
+
: {};
|
|
395
|
+
// Freeze every non-@deftai/directive package record (including hoisted
|
|
396
|
+
// transitives and nested product trees). Only packages[""] root dep maps
|
|
397
|
+
// (directive keys only) and node_modules/@deftai/directive* trees may change.
|
|
398
|
+
const allPkgKeys = new Set([...Object.keys(basePkgs), ...Object.keys(headPkgs)]);
|
|
399
|
+
for (const key of allPkgKeys) {
|
|
400
|
+
if (key === "")
|
|
401
|
+
continue;
|
|
402
|
+
if (key.includes("node_modules/@deftai/directive") || key.includes("/@deftai/directive/")) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (!deepEqualJson(basePkgs[key], headPkgs[key]))
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
// Legacy npm lockfileVersion 1: no packages map — freeze the full nested
|
|
409
|
+
// dependencies tree after stripping @deftai/directive* keys (#3193 Greptile).
|
|
410
|
+
if (Object.keys(basePkgs).length === 0 && Object.keys(headPkgs).length === 0) {
|
|
411
|
+
if (!deepEqualJson(stripDirectiveFromNpmV1Deps(baseObj.dependencies), stripDirectiveFromNpmV1Deps(headObj.dependencies))) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Minimal pnpm-lock.yaml (v6/v9) root importer (`.`) direct-dep extractor.
|
|
419
|
+
* Avoids a YAML dependency; sufficient for pin follow-through identity checks.
|
|
420
|
+
*/
|
|
421
|
+
export function pnpmLockRootDirectDeps(raw) {
|
|
422
|
+
const lines = raw.split(/\r?\n/);
|
|
423
|
+
const out = {};
|
|
424
|
+
let inImporters = false;
|
|
425
|
+
let inRoot = false;
|
|
426
|
+
let inDepBlock = false;
|
|
427
|
+
let currentPkg = null;
|
|
428
|
+
const unquote = (s) => {
|
|
429
|
+
const t = s.trim();
|
|
430
|
+
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
|
431
|
+
return t.slice(1, -1);
|
|
432
|
+
}
|
|
433
|
+
return t;
|
|
434
|
+
};
|
|
435
|
+
for (const line of lines) {
|
|
436
|
+
if (/^importers:\s*$/.test(line)) {
|
|
437
|
+
inImporters = true;
|
|
438
|
+
inRoot = false;
|
|
439
|
+
inDepBlock = false;
|
|
440
|
+
currentPkg = null;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (!inImporters)
|
|
444
|
+
continue;
|
|
445
|
+
// Left the importers section (top-level key at column 0).
|
|
446
|
+
if (/^[^\s#]/.test(line) && !line.startsWith("importers")) {
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
// Root importer `.` (quoted or bare).
|
|
450
|
+
if (/^ {2}(?:\.|'\.'|"\."):\s*$/.test(line)) {
|
|
451
|
+
inRoot = true;
|
|
452
|
+
inDepBlock = false;
|
|
453
|
+
currentPkg = null;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
// Sibling importer under importers (2-space indent, not a dep field).
|
|
457
|
+
if (inRoot && /^ {2}\S/.test(line) && !/^ {2}\./.test(line)) {
|
|
458
|
+
inRoot = false;
|
|
459
|
+
inDepBlock = false;
|
|
460
|
+
currentPkg = null;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!inRoot)
|
|
464
|
+
continue;
|
|
465
|
+
if (/^ {4}(?:dependencies|devDependencies|optionalDependencies|peerDependencies):\s*$/.test(line)) {
|
|
466
|
+
inDepBlock = true;
|
|
467
|
+
currentPkg = null;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
// Non-dep field under root importer ends a dep block.
|
|
471
|
+
if (inDepBlock && /^ {4}\S/.test(line)) {
|
|
472
|
+
inDepBlock = false;
|
|
473
|
+
currentPkg = null;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (!inDepBlock)
|
|
477
|
+
continue;
|
|
478
|
+
const pkgMatch = line.match(/^ {6}(.+?):\s*$/);
|
|
479
|
+
if (pkgMatch) {
|
|
480
|
+
currentPkg = unquote(pkgMatch[1] ?? "");
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (currentPkg) {
|
|
484
|
+
const verMatch = line.match(/^ {8}version:\s*(.+?)\s*$/);
|
|
485
|
+
if (verMatch) {
|
|
486
|
+
out[currentPkg] = unquote(verMatch[1] ?? "");
|
|
487
|
+
currentPkg = null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Extract pnpm `packages:` section records keyed by the package name (not
|
|
495
|
+
* name@version). Used to freeze product package resolution blocks.
|
|
496
|
+
*/
|
|
497
|
+
export function pnpmPackagesByName(raw) {
|
|
498
|
+
const map = new Map();
|
|
499
|
+
const lines = raw.split(/\r?\n/);
|
|
500
|
+
let inPackages = false;
|
|
501
|
+
let currentName = null;
|
|
502
|
+
let buf = [];
|
|
503
|
+
const flush = () => {
|
|
504
|
+
if (currentName === null)
|
|
505
|
+
return;
|
|
506
|
+
const prev = map.get(currentName) ?? [];
|
|
507
|
+
prev.push(buf.join("\n"));
|
|
508
|
+
map.set(currentName, prev);
|
|
509
|
+
currentName = null;
|
|
510
|
+
buf = [];
|
|
511
|
+
};
|
|
512
|
+
const unquote = (s) => {
|
|
513
|
+
const t = s.trim();
|
|
514
|
+
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
|
515
|
+
return t.slice(1, -1);
|
|
516
|
+
}
|
|
517
|
+
return t;
|
|
518
|
+
};
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
if (/^packages:\s*$/.test(line)) {
|
|
521
|
+
flush();
|
|
522
|
+
inPackages = true;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (!inPackages)
|
|
526
|
+
continue;
|
|
527
|
+
if (/^[^\s#]/.test(line) && !line.startsWith("packages")) {
|
|
528
|
+
flush();
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
// Package key at 2-space indent: ` lodash@4.17.21:` or ` '@scope/pkg@1.0.0':`
|
|
532
|
+
const keyMatch = line.match(/^ {2}(.+?):\s*$/);
|
|
533
|
+
if (keyMatch) {
|
|
534
|
+
flush();
|
|
535
|
+
const key = unquote(keyMatch[1] ?? "");
|
|
536
|
+
// name@version or @scope/name@version — strip version suffix after last @
|
|
537
|
+
// that is not the scope marker.
|
|
538
|
+
const at = key.startsWith("@") ? key.indexOf("@", 1) : key.indexOf("@");
|
|
539
|
+
currentName = at > 0 ? key.slice(0, at) : key;
|
|
540
|
+
buf = [line];
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (currentName !== null)
|
|
544
|
+
buf.push(line);
|
|
545
|
+
}
|
|
546
|
+
flush();
|
|
547
|
+
// Join multi-version records for stable compare.
|
|
548
|
+
const out = new Map();
|
|
549
|
+
for (const [name, blocks] of map) {
|
|
550
|
+
out.set(name, blocks.slice().sort().join("\n---\n"));
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* pnpm-lock.yaml follow-through: root importer (`.`) non-directive direct dep
|
|
556
|
+
* identities must be unchanged, and every non-@deftai/directive packages-section
|
|
557
|
+
* record (including transitive product packages) must be byte-stable (#3193).
|
|
558
|
+
*/
|
|
559
|
+
/**
|
|
560
|
+
* Extract a top-level pnpm section (`packages:` or `snapshots:`) keyed by package name.
|
|
561
|
+
*/
|
|
562
|
+
function pnpmNamedSectionByName(raw, section) {
|
|
563
|
+
const map = new Map();
|
|
564
|
+
const lines = raw.split(/\r?\n/);
|
|
565
|
+
let inSection = false;
|
|
566
|
+
let currentName = null;
|
|
567
|
+
let buf = [];
|
|
568
|
+
const sectionRe = new RegExp(`^${section}:\\s*$`);
|
|
569
|
+
const flush = () => {
|
|
570
|
+
if (currentName === null)
|
|
571
|
+
return;
|
|
572
|
+
const prev = map.get(currentName) ?? [];
|
|
573
|
+
prev.push(buf.join("\n"));
|
|
574
|
+
map.set(currentName, prev);
|
|
575
|
+
currentName = null;
|
|
576
|
+
buf = [];
|
|
577
|
+
};
|
|
578
|
+
const unquote = (s) => {
|
|
579
|
+
const t = s.trim();
|
|
580
|
+
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
|
581
|
+
return t.slice(1, -1);
|
|
582
|
+
}
|
|
583
|
+
return t;
|
|
584
|
+
};
|
|
585
|
+
for (const line of lines) {
|
|
586
|
+
if (sectionRe.test(line)) {
|
|
587
|
+
flush();
|
|
588
|
+
inSection = true;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (!inSection)
|
|
592
|
+
continue;
|
|
593
|
+
if (/^[^\s#]/.test(line) && !line.startsWith(section)) {
|
|
594
|
+
flush();
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
const keyMatch = line.match(/^ {2}(.+?):\s*$/);
|
|
598
|
+
if (keyMatch) {
|
|
599
|
+
flush();
|
|
600
|
+
const key = unquote(keyMatch[1] ?? "");
|
|
601
|
+
const at = key.startsWith("@") ? key.indexOf("@", 1) : key.indexOf("@");
|
|
602
|
+
currentName = at > 0 ? key.slice(0, at) : key;
|
|
603
|
+
buf = [line];
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (currentName !== null)
|
|
607
|
+
buf.push(line);
|
|
608
|
+
}
|
|
609
|
+
flush();
|
|
610
|
+
const out = new Map();
|
|
611
|
+
for (const [name, blocks] of map) {
|
|
612
|
+
out.set(name, blocks.slice().sort().join("\n---\n"));
|
|
613
|
+
}
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
export function isPnpmLockDirectivePinFollowThrough(baseRaw, headRaw) {
|
|
617
|
+
const baseRoot = pnpmLockRootDirectDeps(baseRaw);
|
|
618
|
+
const headRoot = pnpmLockRootDirectDeps(headRaw);
|
|
619
|
+
if (!onlyDirectiveDirectDepsDiffer(baseRoot, headRoot))
|
|
620
|
+
return false;
|
|
621
|
+
for (const section of ["packages", "snapshots"]) {
|
|
622
|
+
const baseSec = pnpmNamedSectionByName(baseRaw, section);
|
|
623
|
+
const headSec = pnpmNamedSectionByName(headRaw, section);
|
|
624
|
+
const names = new Set([...baseSec.keys(), ...headSec.keys()]);
|
|
625
|
+
for (const name of names) {
|
|
626
|
+
if (isDirectiveDependencyKey(name))
|
|
627
|
+
continue;
|
|
628
|
+
if ((baseSec.get(name) ?? null) !== (headSec.get(name) ?? null))
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* yarn.lock (v1) package identity blocks: every non-@deftai/directive* package
|
|
636
|
+
* must keep identical block text (or fail if added/removed). Directive package
|
|
637
|
+
* blocks may change freely as pin follow-through (#3193).
|
|
638
|
+
*/
|
|
639
|
+
export function isYarnLockDirectivePinFollowThrough(baseRaw, headRaw) {
|
|
640
|
+
const baseBlocks = yarnLockPackageBlocks(baseRaw);
|
|
641
|
+
const headBlocks = yarnLockPackageBlocks(headRaw);
|
|
642
|
+
const names = new Set([...baseBlocks.keys(), ...headBlocks.keys()]);
|
|
643
|
+
for (const name of names) {
|
|
644
|
+
if (isDirectiveDependencyKey(name))
|
|
645
|
+
continue;
|
|
646
|
+
if ((baseBlocks.get(name) ?? null) !== (headBlocks.get(name) ?? null))
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
function yarnLockPackageBlocks(raw) {
|
|
652
|
+
const map = new Map();
|
|
653
|
+
const lines = raw.split(/\r?\n/);
|
|
654
|
+
let currentNames = [];
|
|
655
|
+
let buf = [];
|
|
656
|
+
const flush = () => {
|
|
657
|
+
if (currentNames.length === 0)
|
|
658
|
+
return;
|
|
659
|
+
const body = buf.join("\n");
|
|
660
|
+
for (const name of currentNames) {
|
|
661
|
+
map.set(name, body);
|
|
662
|
+
}
|
|
663
|
+
currentNames = [];
|
|
664
|
+
buf = [];
|
|
665
|
+
};
|
|
666
|
+
for (const line of lines) {
|
|
667
|
+
if (line === "" || line.startsWith("#")) {
|
|
668
|
+
if (currentNames.length > 0 && line === "") {
|
|
669
|
+
flush();
|
|
670
|
+
}
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
// Package header: `"lodash@^4.17.21":` or `lodash@^4.17.21:`
|
|
674
|
+
if (!/^\s/.test(line) && line.endsWith(":")) {
|
|
675
|
+
flush();
|
|
676
|
+
const header = line.slice(0, -1);
|
|
677
|
+
currentNames = header.split(",").map((part) => {
|
|
678
|
+
let p = part.trim();
|
|
679
|
+
if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
|
|
680
|
+
p = p.slice(1, -1);
|
|
681
|
+
}
|
|
682
|
+
// name@version or @scope/name@version
|
|
683
|
+
const at = p.startsWith("@") ? p.indexOf("@", 1) : p.indexOf("@");
|
|
684
|
+
return at > 0 ? p.slice(0, at) : p;
|
|
685
|
+
});
|
|
686
|
+
buf = [line];
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (currentNames.length > 0)
|
|
690
|
+
buf.push(line);
|
|
691
|
+
}
|
|
692
|
+
flush();
|
|
693
|
+
return map;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Content-aware check for a single upgrade pin path (#3193).
|
|
697
|
+
* Returns true when the path may co-travel with `.deft/core/**`.
|
|
698
|
+
*/
|
|
699
|
+
export function isUpgradePinPathContentAllowed(path, baseRaw, headRaw) {
|
|
700
|
+
const normalized = path.replace(/\\/g, "/");
|
|
701
|
+
switch (normalized) {
|
|
702
|
+
case "package.json":
|
|
703
|
+
return isPackageJsonDirectivePinOnlyDiff(baseRaw, headRaw);
|
|
704
|
+
case "package-lock.json":
|
|
705
|
+
return isPackageLockDirectivePinFollowThrough(baseRaw, headRaw);
|
|
706
|
+
case "pnpm-lock.yaml":
|
|
707
|
+
return isPnpmLockDirectivePinFollowThrough(baseRaw, headRaw);
|
|
708
|
+
case "yarn.lock":
|
|
709
|
+
return isYarnLockDirectivePinFollowThrough(baseRaw, headRaw);
|
|
710
|
+
default:
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
186
714
|
/**
|
|
187
715
|
* TS twin of Go `classifyChangedPaths` / deposited shell guard (#1430).
|
|
188
716
|
* Core = `.deft/core/**`; installer-managed = allowlist; app = everything else.
|
|
189
717
|
* Guard fails iff both core and app are non-empty.
|
|
718
|
+
*
|
|
719
|
+
* Path-only: does not inspect package/lock contents. Prefer
|
|
720
|
+
* {@link classifyMixedCoreAndAppContentAware} when base/head blobs are available
|
|
721
|
+
* (deposited guard + unit tests for #3193).
|
|
190
722
|
*/
|
|
191
723
|
export function classifyMixedCoreAndApp(changedPaths, matchers = installerManagedMatchers()) {
|
|
192
724
|
const core = [];
|
|
@@ -213,6 +745,47 @@ export function classifyMixedCoreAndApp(changedPaths, matchers = installerManage
|
|
|
213
745
|
wouldFail: core.length > 0 && app.length > 0,
|
|
214
746
|
};
|
|
215
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Content-aware upgrade co-travel classifier (#3193).
|
|
750
|
+
*
|
|
751
|
+
* Starts from path classification (#3127 allowlist), then when `.deft/core/**`
|
|
752
|
+
* is present reclassifies package.json / lockfile paths as **app** unless their
|
|
753
|
+
* base→head content is the Directive pin unit (or lock follow-through).
|
|
754
|
+
* `.deft/GENERATION.json` remains path-allowlisted with no content constraint.
|
|
755
|
+
*
|
|
756
|
+
* Missing content for a pin path co-travelling with core fails closed (treated
|
|
757
|
+
* as app) so partial fixtures cannot silently re-open the path-only hole.
|
|
758
|
+
*/
|
|
759
|
+
export function classifyMixedCoreAndAppContentAware(changedPaths, fileContents, matchers = installerManagedMatchers()) {
|
|
760
|
+
const base = classifyMixedCoreAndApp(changedPaths, matchers);
|
|
761
|
+
if (base.core.length === 0) {
|
|
762
|
+
return { ...base, pinContentRejected: [] };
|
|
763
|
+
}
|
|
764
|
+
const pinContentRejected = [];
|
|
765
|
+
const keptManaged = [];
|
|
766
|
+
const app = [...base.app];
|
|
767
|
+
for (const path of base.installerManaged) {
|
|
768
|
+
if (!UPGRADE_PIN_CONTENT_PATHS.includes(path)) {
|
|
769
|
+
keptManaged.push(path);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const pair = fileContents[path];
|
|
773
|
+
if (!pair || !isUpgradePinPathContentAllowed(path, pair.base, pair.head)) {
|
|
774
|
+
pinContentRejected.push(path);
|
|
775
|
+
app.push(path);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
keptManaged.push(path);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
core: base.core,
|
|
783
|
+
installerManaged: keptManaged,
|
|
784
|
+
app,
|
|
785
|
+
wouldFail: app.length > 0,
|
|
786
|
+
pinContentRejected,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
216
789
|
export function frameworkStagePaths(projectDir, deftDir, options = {}) {
|
|
217
790
|
const paths = [];
|
|
218
791
|
const seen = new Set();
|
|
@@ -486,11 +1059,12 @@ export function printCommitGuidance(io, paths, staged) {
|
|
|
486
1059
|
if (paths.length === 0)
|
|
487
1060
|
return;
|
|
488
1061
|
const addCmd = `git add ${paths.join(" ")}`;
|
|
489
|
-
io.printf("\nCommit hygiene (#1453, #1671, #3127): keep the framework upgrade in its OWN branch/PR.\n");
|
|
1062
|
+
io.printf("\nCommit hygiene (#1453, #1671, #3127, #3193): keep the framework upgrade in its OWN branch/PR.\n");
|
|
490
1063
|
io.printf("Do NOT use `git add -A` -- mixing the payload with product/app files trips the\n");
|
|
491
1064
|
io.printf("deft-core-guard CI check.\n");
|
|
492
1065
|
io.printf("One upgrade PR MAY co-travel: .deft/core/** + installer-managed deposits + package.json\n");
|
|
493
|
-
io.printf("pin/lock + .deft/GENERATION.json
|
|
1066
|
+
io.printf("pin/lock (Directive pin-only + lock follow-through, #3193) + .deft/GENERATION.json.\n");
|
|
1067
|
+
io.printf("True app/product paths still require a separate PR.\n");
|
|
494
1068
|
if (staged) {
|
|
495
1069
|
io.printf("The installer already staged ONLY these framework + installer-managed paths:\n");
|
|
496
1070
|
io.printf(` ${addCmd}\n`);
|