duaer-spec 0.27.0 → 0.27.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/.claude/rules/agents-workflow.md +2 -0
- package/.claude/rules/duaer-spec.md +1 -0
- package/.cursor/rules/agents-workflow.mdc +2 -0
- package/.cursor/rules/duaer-spec.mdc +1 -0
- package/.duaer/active-job.json +3 -3
- package/.duaer/specs/270-patch-version/delivery.json +7 -0
- package/.duaer/specs/270-patch-version/spec.md +19 -0
- package/.duaer/specs/270-patch-version/tasks.md +4 -0
- package/.duaer/specs/271-fde04-compat/delivery.json +9 -0
- package/.duaer/specs/271-fde04-compat/spec.md +25 -0
- package/.duaer/specs/271-fde04-compat/tasks.md +5 -0
- package/.duaer/specs/272-fde03-env-check/delivery.json +9 -0
- package/.duaer/specs/272-fde03-env-check/spec.md +34 -0
- package/.duaer/specs/272-fde03-env-check/tasks.md +5 -0
- package/.duaer/specs/273-fde06-data-precheck/delivery.json +9 -0
- package/.duaer/specs/273-fde06-data-precheck/spec.md +37 -0
- package/.duaer/specs/273-fde06-data-precheck/tasks.md +5 -0
- package/.duaer/specs/274-fde05-external-deps/delivery.json +9 -0
- package/.duaer/specs/274-fde05-external-deps/spec.md +37 -0
- package/.duaer/specs/274-fde05-external-deps/tasks.md +5 -0
- package/.duaer/specs/275-release-0271/delivery.json +9 -0
- package/.duaer/specs/275-release-0271/spec.md +13 -0
- package/.duaer/specs/275-release-0271/tasks.md +4 -0
- package/.duaer/specs/276-fde08-perf-budget/delivery.json +9 -0
- package/.duaer/specs/276-fde08-perf-budget/spec.md +38 -0
- package/.duaer/specs/276-fde08-perf-budget/tasks.md +5 -0
- package/.duaer/specs/277-project-dir-reuse/delivery.json +9 -0
- package/.duaer/specs/277-project-dir-reuse/spec.md +31 -0
- package/.duaer/specs/277-project-dir-reuse/tasks.md +5 -0
- package/.duaer/specs/278-project-create-unique/delivery.json +9 -0
- package/.duaer/specs/278-project-create-unique/spec.md +27 -0
- package/.duaer/specs/278-project-create-unique/tasks.md +5 -0
- package/.duaer/specs/279-arch-side-detour/delivery.json +8 -0
- package/.duaer/specs/279-arch-side-detour/spec.md +28 -0
- package/.duaer/specs/279-arch-side-detour/tasks.md +5 -0
- package/.duaer/specs/280-arch-bottom-via-label/delivery.json +8 -0
- package/.duaer/specs/280-arch-bottom-via-label/spec.md +22 -0
- package/.duaer/specs/280-arch-bottom-via-label/tasks.md +5 -0
- package/.duaer/specs/281-release-0272/spec.md +13 -0
- package/.duaer/specs/281-release-0272/tasks.md +4 -0
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +69 -0
- package/bin/duaer-live.mjs +202 -23
- package/bin/live-archify.mjs +382 -26
- package/bin/live-deliverables.mjs +16 -0
- package/bin/live-modules.mjs +81 -1
- package/bin/live-project-chat.mjs +8 -0
- package/bin/live-repo-path.mjs +91 -2
- package/docs/agent/branching-and-release.md +16 -0
- package/docs/agent/e2e-test-plan.md +10 -0
- package/docs/agent/live-desk.md +34 -4
- package/docs/agent/workflow.md +2 -0
- package/package.json +1 -1
- package/web/live-dev/app.js +95 -2
- package/web/live-dev/baseline.mjs +4 -0
- package/web/live-dev/compat-matrix.mjs +38 -0
- package/web/live-dev/confirm-card.mjs +4 -0
- package/web/live-dev/data-precheck.mjs +35 -0
- package/web/live-dev/env-check.mjs +41 -0
- package/web/live-dev/external-deps.mjs +35 -0
- package/web/live-dev/i18n-de.js +25 -10
- package/web/live-dev/i18n-es.js +25 -10
- package/web/live-dev/i18n-fr.js +25 -10
- package/web/live-dev/i18n-ja.js +25 -10
- package/web/live-dev/i18n-ko.js +25 -10
- package/web/live-dev/i18n-pt-BR.js +25 -10
- package/web/live-dev/i18n-ru.js +25 -10
- package/web/live-dev/i18n-vi.js +25 -10
- package/web/live-dev/i18n-zh-TW.js +25 -10
- package/web/live-dev/i18n.js +50 -20
- package/web/live-dev/index.html +33 -9
- package/web/live-dev/perf-budget.mjs +38 -0
package/bin/live-archify.mjs
CHANGED
|
@@ -355,17 +355,19 @@ export function repairArchitectureGeometry(ir) {
|
|
|
355
355
|
e.label = truncateToUnits(String(e.label).trim(), MAX_EDGE_LABEL_UNITS);
|
|
356
356
|
if (!e.label) delete e.label;
|
|
357
357
|
}
|
|
358
|
-
if (e.label && e.labelAt == null) {
|
|
358
|
+
if (e.label && e.labelAt == null && e.labelDx == null && e.labelDy == null) {
|
|
359
359
|
// Pull labels above the edge so they clear horizontally adjacent boxes.
|
|
360
|
-
e.labelDy = -
|
|
360
|
+
e.labelDy = -36;
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
return ir;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
/**
|
|
367
|
-
* Detour
|
|
368
|
-
*
|
|
367
|
+
* Detour edges that would pass through unrelated components.
|
|
368
|
+
* Same-row long edges prefer a bottom/top via under/over the floor.
|
|
369
|
+
* Diagonal / stacked edges pick a corridor whose segments stay clear
|
|
370
|
+
* of unrelated boxes (Archify clean-flow edge-through-node).
|
|
369
371
|
* @param {object} ir
|
|
370
372
|
*/
|
|
371
373
|
export function routeCrossingEdges(ir) {
|
|
@@ -375,11 +377,15 @@ export function routeCrossingEdges(ir) {
|
|
|
375
377
|
if (components.length < 3 || !connections.length) return ir;
|
|
376
378
|
|
|
377
379
|
const byId = new Map();
|
|
380
|
+
let maxRight = 0;
|
|
381
|
+
let minLeft = Infinity;
|
|
382
|
+
let floorY = 0;
|
|
383
|
+
let ceilY = Infinity;
|
|
378
384
|
for (const c of components) {
|
|
379
385
|
if (!c?.id || !hasFinitePos(c)) continue;
|
|
380
386
|
const w = hasFiniteSize(c) ? c.size[0] : 140;
|
|
381
387
|
const h = hasFiniteSize(c) ? c.size[1] : 64;
|
|
382
|
-
|
|
388
|
+
const box = {
|
|
383
389
|
id: c.id,
|
|
384
390
|
x: c.pos[0],
|
|
385
391
|
y: c.pos[1],
|
|
@@ -389,48 +395,396 @@ export function routeCrossingEdges(ir) {
|
|
|
389
395
|
cy: c.pos[1] + h / 2,
|
|
390
396
|
bottom: c.pos[1] + h,
|
|
391
397
|
top: c.pos[1],
|
|
392
|
-
|
|
398
|
+
right: c.pos[0] + w,
|
|
399
|
+
left: c.pos[0],
|
|
400
|
+
};
|
|
401
|
+
byId.set(c.id, box);
|
|
402
|
+
maxRight = Math.max(maxRight, box.right);
|
|
403
|
+
minLeft = Math.min(minLeft, box.left);
|
|
404
|
+
floorY = Math.max(floorY, box.bottom);
|
|
405
|
+
ceilY = Math.min(ceilY, box.top);
|
|
393
406
|
}
|
|
394
407
|
if (byId.size < 3) return ir;
|
|
395
408
|
|
|
396
|
-
|
|
397
|
-
|
|
409
|
+
const CLEAR = 8;
|
|
410
|
+
|
|
411
|
+
/** Axis-aligned segment vs box with clearance (Archify uses ~2px). */
|
|
412
|
+
function segHitsBox(x1, y1, x2, y2, box) {
|
|
413
|
+
const minX = Math.min(x1, x2);
|
|
414
|
+
const maxX = Math.max(x1, x2);
|
|
415
|
+
const minY = Math.min(y1, y2);
|
|
416
|
+
const maxY = Math.max(y1, y2);
|
|
417
|
+
return (
|
|
418
|
+
maxX >= box.left - CLEAR &&
|
|
419
|
+
minX <= box.right + CLEAR &&
|
|
420
|
+
maxY >= box.top - CLEAR &&
|
|
421
|
+
minY <= box.bottom + CLEAR
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function pathClear(pts, skipA, skipB) {
|
|
426
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
427
|
+
const [x1, y1] = pts[i];
|
|
428
|
+
const [x2, y2] = pts[i + 1];
|
|
429
|
+
for (const other of byId.values()) {
|
|
430
|
+
if (other.id === skipA || other.id === skipB) continue;
|
|
431
|
+
if (segHitsBox(x1, y1, x2, y2, other)) return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Port point on a box side. */
|
|
438
|
+
function port(box, side) {
|
|
439
|
+
switch (side) {
|
|
440
|
+
case "left":
|
|
441
|
+
return [box.left, box.cy];
|
|
442
|
+
case "right":
|
|
443
|
+
return [box.right, box.cy];
|
|
444
|
+
case "top":
|
|
445
|
+
return [box.cx, box.top];
|
|
446
|
+
case "bottom":
|
|
447
|
+
return [box.cx, box.bottom];
|
|
448
|
+
default:
|
|
449
|
+
return [box.cx, box.cy];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function tryRoute(a, b, fromSide, toSide, via) {
|
|
454
|
+
const pts = [port(a, fromSide), ...via, port(b, toSide)];
|
|
455
|
+
if (!pathClear(pts, a.id, b.id)) return null;
|
|
456
|
+
return { fromSide, toSide, via };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Vertical gutters between sorted unique column centers. */
|
|
460
|
+
function gutterXs() {
|
|
461
|
+
const cols = [...byId.values()]
|
|
462
|
+
.map((b) => b.cx)
|
|
463
|
+
.sort((x, y) => x - y);
|
|
464
|
+
const uniq = [];
|
|
465
|
+
for (const x of cols) {
|
|
466
|
+
if (!uniq.length || Math.abs(uniq[uniq.length - 1] - x) > 40) uniq.push(x);
|
|
467
|
+
}
|
|
468
|
+
const gutters = [Math.round(minLeft - 56), Math.round(maxRight + 56)];
|
|
469
|
+
for (let i = 0; i < uniq.length - 1; i++) {
|
|
470
|
+
const leftBoxes = [...byId.values()].filter((b) => Math.abs(b.cx - uniq[i]) < 48);
|
|
471
|
+
const rightBoxes = [...byId.values()].filter(
|
|
472
|
+
(b) => Math.abs(b.cx - uniq[i + 1]) < 48,
|
|
473
|
+
);
|
|
474
|
+
const rightEdge = Math.max(...leftBoxes.map((b) => b.right));
|
|
475
|
+
const leftEdge = Math.min(...rightBoxes.map((b) => b.left));
|
|
476
|
+
if (leftEdge - rightEdge > 40) {
|
|
477
|
+
gutters.push(Math.round((rightEdge + leftEdge) / 2));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return gutters;
|
|
481
|
+
}
|
|
482
|
+
|
|
398
483
|
let detourIndex = 0;
|
|
399
484
|
|
|
400
485
|
for (const e of connections) {
|
|
401
486
|
if (!e || typeof e !== "object") continue;
|
|
402
|
-
if (Array.isArray(e.via) && e.via.length) continue;
|
|
403
487
|
const a = byId.get(e.from);
|
|
404
488
|
const b = byId.get(e.to);
|
|
405
489
|
if (!a || !b) continue;
|
|
406
|
-
const span = Math.abs(a.cx - b.cx);
|
|
407
|
-
if (span < 160) continue;
|
|
408
490
|
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
491
|
+
const horizSpan = Math.abs(a.cx - b.cx);
|
|
492
|
+
const vertSpan = Math.abs(a.cy - b.cy);
|
|
493
|
+
if (horizSpan < 40 && vertSpan < 80) continue;
|
|
494
|
+
|
|
495
|
+
const boxMinX = Math.min(a.left, b.left);
|
|
496
|
+
const boxMaxX = Math.max(a.right, b.right);
|
|
497
|
+
const boxMinY = Math.min(a.top, b.top);
|
|
498
|
+
const boxMaxY = Math.max(a.bottom, b.bottom);
|
|
499
|
+
|
|
413
500
|
let blocked = false;
|
|
414
501
|
for (const other of byId.values()) {
|
|
415
502
|
if (other.id === a.id || other.id === b.id) continue;
|
|
416
|
-
|
|
417
|
-
|
|
503
|
+
const inBox =
|
|
504
|
+
other.right > boxMinX + 8 &&
|
|
505
|
+
other.left < boxMaxX - 8 &&
|
|
506
|
+
other.bottom > boxMinY + 8 &&
|
|
507
|
+
other.top < boxMaxY - 8;
|
|
508
|
+
if (inBox) {
|
|
418
509
|
blocked = true;
|
|
419
510
|
break;
|
|
420
511
|
}
|
|
421
512
|
}
|
|
513
|
+
|
|
514
|
+
if (!blocked && Array.isArray(e.via) && e.via.length) {
|
|
515
|
+
const viaY = Number(e.via[0]?.[1]);
|
|
516
|
+
if (
|
|
517
|
+
Number.isFinite(viaY) &&
|
|
518
|
+
(e.fromSide === "bottom" || e.toSide === "bottom")
|
|
519
|
+
) {
|
|
520
|
+
for (const other of byId.values()) {
|
|
521
|
+
if (other.id === a.id || other.id === b.id) continue;
|
|
522
|
+
const underA =
|
|
523
|
+
Math.abs(other.cx - a.cx) < 48 &&
|
|
524
|
+
other.top >= a.bottom - 4 &&
|
|
525
|
+
other.top < viaY;
|
|
526
|
+
const underB =
|
|
527
|
+
Math.abs(other.cx - b.cx) < 48 &&
|
|
528
|
+
other.top >= b.bottom - 4 &&
|
|
529
|
+
other.top < viaY;
|
|
530
|
+
if (underA || underB) {
|
|
531
|
+
blocked = true;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
422
538
|
if (!blocked) continue;
|
|
423
539
|
|
|
424
|
-
const
|
|
540
|
+
const sameRow = vertSpan < 80 && horizSpan >= 160;
|
|
541
|
+
const candidates = [];
|
|
542
|
+
|
|
543
|
+
if (sameRow) {
|
|
544
|
+
const viaYBot = floorY + 48 + detourIndex * 28;
|
|
545
|
+
const viaYTop = ceilY - 48 - detourIndex * 28;
|
|
546
|
+
candidates.push(
|
|
547
|
+
tryRoute(a, b, "bottom", "bottom", [
|
|
548
|
+
[Math.round(a.cx), Math.round(viaYBot)],
|
|
549
|
+
[Math.round(b.cx), Math.round(viaYBot)],
|
|
550
|
+
]),
|
|
551
|
+
);
|
|
552
|
+
candidates.push(
|
|
553
|
+
tryRoute(a, b, "top", "top", [
|
|
554
|
+
[Math.round(a.cx), Math.round(viaYTop)],
|
|
555
|
+
[Math.round(b.cx), Math.round(viaYTop)],
|
|
556
|
+
]),
|
|
557
|
+
);
|
|
558
|
+
// Side corridors for same-row when floor/ceil are blocked
|
|
559
|
+
for (const side of ["right", "left"]) {
|
|
560
|
+
const viaX =
|
|
561
|
+
side === "right"
|
|
562
|
+
? Math.round(maxRight + 56 + detourIndex * 36)
|
|
563
|
+
: Math.round(minLeft - 56 - detourIndex * 36);
|
|
564
|
+
candidates.push(
|
|
565
|
+
tryRoute(a, b, side, side, [
|
|
566
|
+
[viaX, Math.round(a.cy)],
|
|
567
|
+
[viaX, Math.round(b.cy)],
|
|
568
|
+
]),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
const gutters = gutterXs();
|
|
573
|
+
const ceil = ceilY - 48 - detourIndex * 20;
|
|
574
|
+
const floor = floorY + 48 + detourIndex * 20;
|
|
575
|
+
for (const gx of gutters) {
|
|
576
|
+
// Approach destination from the gutter side
|
|
577
|
+
const toSide = b.cx < gx ? "right" : "left";
|
|
578
|
+
const fromSide = a.cx < gx ? "right" : "left";
|
|
579
|
+
// Top wrap into gutter then into target
|
|
580
|
+
candidates.push(
|
|
581
|
+
tryRoute(a, b, "top", toSide, [
|
|
582
|
+
[Math.round(a.cx), Math.round(ceil)],
|
|
583
|
+
[Math.round(gx), Math.round(ceil)],
|
|
584
|
+
[Math.round(gx), Math.round(b.cy)],
|
|
585
|
+
]),
|
|
586
|
+
);
|
|
587
|
+
candidates.push(
|
|
588
|
+
tryRoute(a, b, fromSide, toSide, [
|
|
589
|
+
[Math.round(gx), Math.round(a.cy)],
|
|
590
|
+
[Math.round(gx), Math.round(b.cy)],
|
|
591
|
+
]),
|
|
592
|
+
);
|
|
593
|
+
candidates.push(
|
|
594
|
+
tryRoute(a, b, fromSide, "bottom", [
|
|
595
|
+
[Math.round(gx), Math.round(a.cy)],
|
|
596
|
+
[Math.round(gx), Math.round(floor)],
|
|
597
|
+
[Math.round(b.cx), Math.round(floor)],
|
|
598
|
+
]),
|
|
599
|
+
);
|
|
600
|
+
candidates.push(
|
|
601
|
+
tryRoute(a, b, "bottom", toSide, [
|
|
602
|
+
[Math.round(a.cx), Math.round(floor)],
|
|
603
|
+
[Math.round(gx), Math.round(floor)],
|
|
604
|
+
[Math.round(gx), Math.round(b.cy)],
|
|
605
|
+
]),
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
// Prefer right outer when mid is leftish (matches stacked fixtures)
|
|
609
|
+
const preferRight =
|
|
610
|
+
(a.cx + b.cx) / 2 <= (minLeft + maxRight) / 2 + (maxRight - minLeft) * 0.1;
|
|
611
|
+
const orderedSides = preferRight ? ["right", "left"] : ["left", "right"];
|
|
612
|
+
for (const side of orderedSides) {
|
|
613
|
+
const viaX =
|
|
614
|
+
side === "right"
|
|
615
|
+
? Math.round(maxRight + 56 + detourIndex * 36)
|
|
616
|
+
: Math.round(minLeft - 56 - detourIndex * 36);
|
|
617
|
+
candidates.push(
|
|
618
|
+
tryRoute(a, b, side, side, [
|
|
619
|
+
[viaX, Math.round(a.cy)],
|
|
620
|
+
[viaX, Math.round(b.cy)],
|
|
621
|
+
]),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const chosen = candidates.find(Boolean);
|
|
627
|
+
if (!chosen) continue;
|
|
628
|
+
|
|
425
629
|
detourIndex += 1;
|
|
426
|
-
e.fromSide =
|
|
427
|
-
e.toSide =
|
|
428
|
-
e.via =
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
630
|
+
e.fromSide = chosen.fromSide;
|
|
631
|
+
e.toSide = chosen.toSide;
|
|
632
|
+
e.via = chosen.via;
|
|
633
|
+
if (e.label && e.labelAt == null) {
|
|
634
|
+
// Final placement is repaired by repairEdgeLabelPlacement; leave
|
|
635
|
+
// bottom/top vias unmarked so that helper can pin labelAt on the via.
|
|
636
|
+
if (chosen.fromSide === "bottom" || chosen.toSide === "bottom") {
|
|
637
|
+
delete e.labelDx;
|
|
638
|
+
delete e.labelDy;
|
|
639
|
+
} else if (chosen.fromSide === "top" || chosen.toSide === "top") {
|
|
640
|
+
delete e.labelDx;
|
|
641
|
+
delete e.labelDy;
|
|
642
|
+
} else {
|
|
643
|
+
e.labelDx = chosen.fromSide === "right" ? -36 : 36;
|
|
644
|
+
delete e.labelDy;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return ir;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Keep edge labels off component boxes.
|
|
653
|
+
* Bottom vias must place the label *below* the via (labelDy > 0) — a negative
|
|
654
|
+
* dy pulls the label up into nodes that sit above the detour.
|
|
655
|
+
* Vertical stacked edges get labelAt in the inter-box gap.
|
|
656
|
+
* @param {object} ir
|
|
657
|
+
*/
|
|
658
|
+
export function repairEdgeLabelPlacement(ir) {
|
|
659
|
+
if (!ir || typeof ir !== "object") return ir;
|
|
660
|
+
const components = Array.isArray(ir.components) ? ir.components : [];
|
|
661
|
+
const connections = Array.isArray(ir.connections) ? ir.connections : [];
|
|
662
|
+
const boxes = [];
|
|
663
|
+
const byId = new Map();
|
|
664
|
+
for (const c of components) {
|
|
665
|
+
if (!c?.id || !hasFinitePos(c)) continue;
|
|
666
|
+
const w = hasFiniteSize(c) ? c.size[0] : 140;
|
|
667
|
+
const h = hasFiniteSize(c) ? c.size[1] : 64;
|
|
668
|
+
const box = {
|
|
669
|
+
id: c.id,
|
|
670
|
+
cx: c.pos[0] + w / 2,
|
|
671
|
+
cy: c.pos[1] + h / 2,
|
|
672
|
+
left: c.pos[0],
|
|
673
|
+
right: c.pos[0] + w,
|
|
674
|
+
top: c.pos[1],
|
|
675
|
+
bottom: c.pos[1] + h,
|
|
676
|
+
};
|
|
677
|
+
byId.set(c.id, box);
|
|
678
|
+
boxes.push(box);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const LABEL_W = 36;
|
|
682
|
+
const LABEL_H = 16;
|
|
683
|
+
|
|
684
|
+
function overlapsAny(lx, ly) {
|
|
685
|
+
const left = lx - LABEL_W / 2;
|
|
686
|
+
const right = lx + LABEL_W / 2;
|
|
687
|
+
const top = ly - LABEL_H / 2;
|
|
688
|
+
const bottom = ly + LABEL_H / 2;
|
|
689
|
+
for (const box of boxes) {
|
|
690
|
+
if (
|
|
691
|
+
right > box.left &&
|
|
692
|
+
left < box.right &&
|
|
693
|
+
bottom > box.top &&
|
|
694
|
+
top < box.bottom
|
|
695
|
+
) {
|
|
696
|
+
return box;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function edgeMid(e, a, b) {
|
|
703
|
+
if (Array.isArray(e.via) && e.via.length >= 2) {
|
|
704
|
+
const first = e.via[0];
|
|
705
|
+
const last = e.via[e.via.length - 1];
|
|
706
|
+
return [
|
|
707
|
+
(Number(first[0]) + Number(last[0])) / 2,
|
|
708
|
+
(Number(first[1]) + Number(last[1])) / 2,
|
|
709
|
+
];
|
|
710
|
+
}
|
|
711
|
+
return [(a.cx + b.cx) / 2, (a.cy + b.cy) / 2];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for (const e of connections) {
|
|
715
|
+
if (!e?.label) continue;
|
|
716
|
+
const a = byId.get(e.from);
|
|
717
|
+
const b = byId.get(e.to);
|
|
718
|
+
if (!a || !b) continue;
|
|
719
|
+
|
|
720
|
+
const via = Array.isArray(e.via) ? e.via : null;
|
|
721
|
+
const bottomVia =
|
|
722
|
+
via &&
|
|
723
|
+
via.length >= 2 &&
|
|
724
|
+
(e.fromSide === "bottom" || e.toSide === "bottom");
|
|
725
|
+
const topVia =
|
|
726
|
+
via &&
|
|
727
|
+
via.length >= 2 &&
|
|
728
|
+
(e.fromSide === "top" || e.toSide === "top") &&
|
|
729
|
+
!bottomVia;
|
|
730
|
+
|
|
731
|
+
if (bottomVia) {
|
|
732
|
+
// Stay under the floor detour — never pull up into co-row / mid-row boxes
|
|
733
|
+
const midX = (Number(via[0][0]) + Number(via[via.length - 1][0])) / 2;
|
|
734
|
+
const viaY = Number(via[0][1]);
|
|
735
|
+
e.labelAt = [Math.round(midX), Math.round(viaY + 18)];
|
|
736
|
+
delete e.labelDx;
|
|
737
|
+
delete e.labelDy;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (topVia) {
|
|
741
|
+
const midX = (Number(via[0][0]) + Number(via[via.length - 1][0])) / 2;
|
|
742
|
+
const viaY = Number(via[0][1]);
|
|
743
|
+
e.labelAt = [Math.round(midX), Math.round(viaY - 18)];
|
|
744
|
+
delete e.labelDx;
|
|
745
|
+
delete e.labelDy;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (e.labelAt != null) {
|
|
750
|
+
const hit = overlapsAny(e.labelAt[0], e.labelAt[1]);
|
|
751
|
+
if (!hit) continue;
|
|
752
|
+
// Nudge below the overlapping box
|
|
753
|
+
e.labelAt = [Math.round(hit.cx), Math.round(hit.bottom + 18)];
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const horizSpan = Math.abs(a.cx - b.cx);
|
|
758
|
+
const vertSpan = Math.abs(a.cy - b.cy);
|
|
759
|
+
if (vertSpan >= 60 && horizSpan < 80) {
|
|
760
|
+
const upper = a.cy <= b.cy ? a : b;
|
|
761
|
+
const lower = a.cy <= b.cy ? b : a;
|
|
762
|
+
const gapY = (upper.bottom + lower.top) / 2;
|
|
763
|
+
e.labelAt = [Math.round(a.cx + 56), Math.round(gapY)];
|
|
764
|
+
delete e.labelDx;
|
|
765
|
+
delete e.labelDy;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (e.labelDy == null && e.labelDx == null) {
|
|
770
|
+
e.labelDy = -36;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// If the offset mid still lands on a box, pin labelAt clear of it
|
|
774
|
+
const [mx, my] = edgeMid(e, a, b);
|
|
775
|
+
const lx = mx + (Number(e.labelDx) || 0);
|
|
776
|
+
const ly = my + (Number(e.labelDy) || 0);
|
|
777
|
+
const hit = overlapsAny(lx, ly);
|
|
778
|
+
if (hit) {
|
|
779
|
+
const below = hit.bottom + 18;
|
|
780
|
+
const above = hit.top - 18;
|
|
781
|
+
const preferBelow = Math.abs(below - ly) <= Math.abs(above - ly);
|
|
782
|
+
e.labelAt = [
|
|
783
|
+
Math.round(hit.cx),
|
|
784
|
+
Math.round(preferBelow ? below : above),
|
|
785
|
+
];
|
|
786
|
+
delete e.labelDx;
|
|
787
|
+
delete e.labelDy;
|
|
434
788
|
}
|
|
435
789
|
}
|
|
436
790
|
return ir;
|
|
@@ -477,6 +831,7 @@ export function layoutArchitectureIr(raw) {
|
|
|
477
831
|
}
|
|
478
832
|
repairArchitectureGeometry(ir);
|
|
479
833
|
routeCrossingEdges(ir);
|
|
834
|
+
repairEdgeLabelPlacement(ir);
|
|
480
835
|
recomputeViewBox(ir);
|
|
481
836
|
return ir;
|
|
482
837
|
}
|
|
@@ -546,6 +901,7 @@ export function layoutArchitectureIr(raw) {
|
|
|
546
901
|
}
|
|
547
902
|
repairArchitectureGeometry(ir);
|
|
548
903
|
routeCrossingEdges(ir);
|
|
904
|
+
repairEdgeLabelPlacement(ir);
|
|
549
905
|
const maxX = originX + (layers.length || 1) * colW + 80;
|
|
550
906
|
ir.meta.viewBox = [
|
|
551
907
|
Math.max(320, maxX),
|
|
@@ -218,6 +218,10 @@ function cardBlock(card, labels) {
|
|
|
218
218
|
[labels.paths, c.criticalPaths, "list"],
|
|
219
219
|
[labels.exceptions, c.exceptionCases, "list"],
|
|
220
220
|
[labels.apiContract, c.apiContract, "list"],
|
|
221
|
+
[labels.envChecklist, c.envChecklist, "list"],
|
|
222
|
+
[labels.dataPrecheck, c.dataPrecheck, "list"],
|
|
223
|
+
[labels.externalDeps, c.externalDeps, "list"],
|
|
224
|
+
[labels.perfBudget, c.perfBudget, "list"],
|
|
221
225
|
].filter(([, v]) => String(v || "").trim());
|
|
222
226
|
if (!rows.length) return `<p class="empty">${esc(labels.emptyCard)}</p>`;
|
|
223
227
|
return `<dl class="card-dl">${rows
|
|
@@ -578,6 +582,10 @@ function labelsFor(lang) {
|
|
|
578
582
|
paths: "Critical paths",
|
|
579
583
|
exceptions: "Exception cases",
|
|
580
584
|
apiContract: "API contract",
|
|
585
|
+
envChecklist: "Environment checklist",
|
|
586
|
+
dataPrecheck: "Data precheck",
|
|
587
|
+
externalDeps: "External dependencies",
|
|
588
|
+
perfBudget: "Performance budget",
|
|
581
589
|
baseline: "Baseline sign-off",
|
|
582
590
|
statusDone: "Ready",
|
|
583
591
|
statusPartial: "In progress",
|
|
@@ -614,6 +622,10 @@ function labelsFor(lang) {
|
|
|
614
622
|
paths: "重要パス",
|
|
615
623
|
exceptions: "異常系",
|
|
616
624
|
apiContract: "API契約",
|
|
625
|
+
envChecklist: "環境チェック",
|
|
626
|
+
dataPrecheck: "データ事前検査",
|
|
627
|
+
externalDeps: "外部依存",
|
|
628
|
+
perfBudget: "性能予算",
|
|
617
629
|
baseline: "ベースライン署名",
|
|
618
630
|
statusDone: "完了",
|
|
619
631
|
statusPartial: "進行中",
|
|
@@ -649,6 +661,10 @@ function labelsFor(lang) {
|
|
|
649
661
|
paths: "关键路径",
|
|
650
662
|
exceptions: "异常态",
|
|
651
663
|
apiContract: "接口契约",
|
|
664
|
+
envChecklist: "环境检查",
|
|
665
|
+
dataPrecheck: "数据预检",
|
|
666
|
+
externalDeps: "外部依赖",
|
|
667
|
+
perfBudget: "性能预算",
|
|
652
668
|
baseline: "基线签署",
|
|
653
669
|
statusDone: "已产出",
|
|
654
670
|
statusPartial: "进行中",
|
package/bin/live-modules.mjs
CHANGED
|
@@ -12,6 +12,23 @@ import {
|
|
|
12
12
|
modulesNeedApiContractTasks,
|
|
13
13
|
} from "../web/live-dev/api-contract.mjs";
|
|
14
14
|
import { missingUatCases } from "../web/live-dev/uat-pack.mjs";
|
|
15
|
+
import { missingCompatMatrix } from "../web/live-dev/compat-matrix.mjs";
|
|
16
|
+
import {
|
|
17
|
+
envChecklistNeedsProbe,
|
|
18
|
+
missingEnvChecklist,
|
|
19
|
+
} from "../web/live-dev/env-check.mjs";
|
|
20
|
+
import {
|
|
21
|
+
dataPrecheckNeedsTask,
|
|
22
|
+
missingDataPrecheck,
|
|
23
|
+
} from "../web/live-dev/data-precheck.mjs";
|
|
24
|
+
import {
|
|
25
|
+
externalDepsNeedsTask,
|
|
26
|
+
missingExternalDeps,
|
|
27
|
+
} from "../web/live-dev/external-deps.mjs";
|
|
28
|
+
import {
|
|
29
|
+
missingPerfBudget,
|
|
30
|
+
perfBudgetNeedsTask,
|
|
31
|
+
} from "../web/live-dev/perf-budget.mjs";
|
|
15
32
|
|
|
16
33
|
function clipCard(card) {
|
|
17
34
|
if (!card || typeof card !== "object") {
|
|
@@ -24,6 +41,10 @@ function clipCard(card) {
|
|
|
24
41
|
criticalPaths: "",
|
|
25
42
|
exceptionCases: "",
|
|
26
43
|
apiContract: "",
|
|
44
|
+
envChecklist: "",
|
|
45
|
+
dataPrecheck: "",
|
|
46
|
+
externalDeps: "",
|
|
47
|
+
perfBudget: "",
|
|
27
48
|
};
|
|
28
49
|
}
|
|
29
50
|
return {
|
|
@@ -35,6 +56,10 @@ function clipCard(card) {
|
|
|
35
56
|
criticalPaths: String(card.criticalPaths || "").slice(0, 4000),
|
|
36
57
|
exceptionCases: String(card.exceptionCases || "").slice(0, 4000),
|
|
37
58
|
apiContract: String(card.apiContract || "").slice(0, 4000),
|
|
59
|
+
envChecklist: String(card.envChecklist || "").slice(0, 4000),
|
|
60
|
+
dataPrecheck: String(card.dataPrecheck || "").slice(0, 4000),
|
|
61
|
+
externalDeps: String(card.externalDeps || "").slice(0, 4000),
|
|
62
|
+
perfBudget: String(card.perfBudget || "").slice(0, 4000),
|
|
38
63
|
};
|
|
39
64
|
}
|
|
40
65
|
|
|
@@ -372,10 +397,56 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
|
|
|
372
397
|
|
|
373
398
|
for (let i = 0; i < list.length; i += 1) {
|
|
374
399
|
const m = list[i];
|
|
400
|
+
const envDeps = [];
|
|
401
|
+
if (envChecklistNeedsProbe(m.card?.envChecklist)) {
|
|
402
|
+
const envGap = missingEnvChecklist(m.card?.envChecklist);
|
|
403
|
+
const envNote = envGap.length ? `; still missing ${envGap.join("/")}` : "";
|
|
404
|
+
const env = push({
|
|
405
|
+
moduleId: m.id,
|
|
406
|
+
title: `FDE-03: env probe «${m.title}» — DNS, TLS, CORS, auth, third-party; block joint debug until green${envNote}`,
|
|
407
|
+
dependsOn: [],
|
|
408
|
+
role: EMPLOYEE_ROLES.VERIFY_L3,
|
|
409
|
+
});
|
|
410
|
+
envDeps.push(env.id);
|
|
411
|
+
}
|
|
412
|
+
let beforeImpl = envDeps;
|
|
413
|
+
if (dataPrecheckNeedsTask(m.card?.dataPrecheck)) {
|
|
414
|
+
const dataGap = missingDataPrecheck(m.card?.dataPrecheck);
|
|
415
|
+
const dataNote = dataGap.length ? `; still missing ${dataGap.join("/")}` : "";
|
|
416
|
+
const data = push({
|
|
417
|
+
moduleId: m.id,
|
|
418
|
+
title: `FDE-06: data precheck «${m.title}» — field mapping, import failure list, export for cleanup${dataNote}`,
|
|
419
|
+
dependsOn: envDeps,
|
|
420
|
+
role: EMPLOYEE_ROLES.VERIFY_L3,
|
|
421
|
+
});
|
|
422
|
+
beforeImpl = [data.id];
|
|
423
|
+
}
|
|
424
|
+
if (externalDepsNeedsTask(m.card?.externalDeps)) {
|
|
425
|
+
const extGap = missingExternalDeps(m.card?.externalDeps);
|
|
426
|
+
const extNote = extGap.length ? `; still missing ${extGap.join("/")}` : "";
|
|
427
|
+
const ext = push({
|
|
428
|
+
moduleId: m.id,
|
|
429
|
+
title: `FDE-05: external deps «${m.title}» — blocker, SLA, mock, parallel path${extNote}`,
|
|
430
|
+
dependsOn: beforeImpl,
|
|
431
|
+
role: EMPLOYEE_ROLES.VERIFY_L3,
|
|
432
|
+
});
|
|
433
|
+
beforeImpl = [ext.id];
|
|
434
|
+
}
|
|
435
|
+
if (perfBudgetNeedsTask(m.card?.perfBudget)) {
|
|
436
|
+
const perfGap = missingPerfBudget(m.card?.perfBudget);
|
|
437
|
+
const perfNote = perfGap.length ? `; still missing ${perfGap.join("/")}` : "";
|
|
438
|
+
const perf = push({
|
|
439
|
+
moduleId: m.id,
|
|
440
|
+
title: `FDE-08: perf budget «${m.title}» — LCP, INP, bundle, virtual list, weak net, large data${perfNote}`,
|
|
441
|
+
dependsOn: beforeImpl,
|
|
442
|
+
role: EMPLOYEE_ROLES.VERIFY_L3,
|
|
443
|
+
});
|
|
444
|
+
beforeImpl = [perf.id];
|
|
445
|
+
}
|
|
375
446
|
const impl = push({
|
|
376
447
|
moduleId: m.id,
|
|
377
448
|
title: `Implement module «${m.title}»: ${truncate(m.card.goal, 100)}`,
|
|
378
|
-
dependsOn:
|
|
449
|
+
dependsOn: beforeImpl,
|
|
379
450
|
role: EMPLOYEE_ROLES.IMPLEMENT,
|
|
380
451
|
});
|
|
381
452
|
const acceptLines = splitAcceptanceLines(m.card.acceptance);
|
|
@@ -434,6 +505,15 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
|
|
|
434
505
|
role: EMPLOYEE_ROLES.VERIFY_L3,
|
|
435
506
|
});
|
|
436
507
|
uatIds.push(uat.id);
|
|
508
|
+
const compatGap = missingCompatMatrix(m.card?.deviceMatrix);
|
|
509
|
+
const compatNote = compatGap.length ? `; still missing ${compatGap.join("/")}` : "";
|
|
510
|
+
const compat = push({
|
|
511
|
+
moduleId: m.id,
|
|
512
|
+
title: `FDE-04: compat evidence «${m.title}» — named browsers, screenshot/cloud run, polyfill/fallback${compatNote}`,
|
|
513
|
+
dependsOn: [uat.id],
|
|
514
|
+
role: EMPLOYEE_ROLES.VERIFY_L3,
|
|
515
|
+
});
|
|
516
|
+
uatIds.push(compat.id);
|
|
437
517
|
}
|
|
438
518
|
let gate = uatIds.length ? uatIds : verifyDeps;
|
|
439
519
|
if (modulesNeedApiContractTasks(list)) {
|
|
@@ -37,6 +37,10 @@ function clipCard(card) {
|
|
|
37
37
|
criticalPaths: "",
|
|
38
38
|
exceptionCases: "",
|
|
39
39
|
apiContract: "",
|
|
40
|
+
envChecklist: "",
|
|
41
|
+
dataPrecheck: "",
|
|
42
|
+
externalDeps: "",
|
|
43
|
+
perfBudget: "",
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
return {
|
|
@@ -48,6 +52,10 @@ function clipCard(card) {
|
|
|
48
52
|
criticalPaths: String(card.criticalPaths || "").slice(0, 4000),
|
|
49
53
|
exceptionCases: String(card.exceptionCases || "").slice(0, 4000),
|
|
50
54
|
apiContract: String(card.apiContract || "").slice(0, 4000),
|
|
55
|
+
envChecklist: String(card.envChecklist || "").slice(0, 4000),
|
|
56
|
+
dataPrecheck: String(card.dataPrecheck || "").slice(0, 4000),
|
|
57
|
+
externalDeps: String(card.externalDeps || "").slice(0, 4000),
|
|
58
|
+
perfBudget: String(card.perfBudget || "").slice(0, 4000),
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
61
|
|