@xera-ai/core 0.9.8 → 0.10.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/bin/internal.js +1415 -534
- package/dist/bin/templates/coverage-panel.html.fragment +20 -0
- package/dist/bin/templates/graph.css +82 -0
- package/dist/bin/templates/graph.html.template +17 -9
- package/dist/bin/templates/graph.js +185 -0
- package/dist/src/index.js +6 -0
- package/package.json +3 -3
- package/src/bin-internal/ac-coverage-backfill-finalize.ts +90 -0
- package/src/bin-internal/ac-coverage-backfill-prepare.ts +72 -0
- package/src/bin-internal/coverage-prepare.ts +123 -0
- package/src/bin-internal/fill-gap-finalize.ts +115 -0
- package/src/bin-internal/fill-gap-prepare.ts +150 -0
- package/src/bin-internal/graph-render.ts +32 -4
- package/src/bin-internal/index.ts +10 -0
- package/src/bin-internal/verify-prompts.ts +2 -0
- package/src/config/schema.ts +9 -0
- package/src/coverage/index.ts +29 -0
- package/src/coverage/report.ts +206 -0
- package/src/coverage/risk.ts +69 -0
- package/src/coverage/status.ts +76 -0
- package/src/coverage/types.ts +11 -0
- package/src/coverage/why.ts +122 -0
- package/src/graph/render.ts +16 -2
- package/src/graph/schema.ts +54 -1
- package/src/graph/store.ts +96 -6
- package/src/graph/templates/coverage-panel.html.fragment +20 -0
- package/src/graph/templates/graph.css +82 -0
- package/src/graph/templates/graph.html.template +17 -9
- package/src/graph/templates/graph.js +185 -0
- package/src/graph/types.ts +56 -1
package/dist/bin/internal.js
CHANGED
|
@@ -404,15 +404,15 @@ var require_main = __commonJS((exports, module) => {
|
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
// src/graph/paths.ts
|
|
407
|
-
import { join
|
|
407
|
+
import { join } from "path";
|
|
408
408
|
function graphPaths(repoRoot) {
|
|
409
|
-
const eventsDir =
|
|
409
|
+
const eventsDir = join(repoRoot, ".xera/graph/events");
|
|
410
410
|
return {
|
|
411
411
|
eventsDir,
|
|
412
|
-
snapshotFile:
|
|
413
|
-
costLog:
|
|
414
|
-
eventsMonthDir: (yyyyMm) =>
|
|
415
|
-
eventFile: (ulid, skill, ticketId, yyyyMm) =>
|
|
412
|
+
snapshotFile: join(repoRoot, ".xera/graph/snapshot.json"),
|
|
413
|
+
costLog: join(repoRoot, ".xera/cost-log.jsonl"),
|
|
414
|
+
eventsMonthDir: (yyyyMm) => join(eventsDir, yyyyMm),
|
|
415
|
+
eventFile: (ulid, skill, ticketId, yyyyMm) => join(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`)
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
418
|
function currentYyyyMm(now = new Date) {
|
|
@@ -426,64 +426,65 @@ var init_paths = () => {};
|
|
|
426
426
|
var SCHEMA_VERSION = 1;
|
|
427
427
|
|
|
428
428
|
// src/graph/schema.ts
|
|
429
|
-
import { z
|
|
429
|
+
import { z } from "zod";
|
|
430
430
|
function safeParseEvent(value) {
|
|
431
431
|
const r = EventSchema.safeParse(value);
|
|
432
432
|
if (r.success)
|
|
433
433
|
return { success: true, data: r.data };
|
|
434
434
|
return { success: false, error: r.error };
|
|
435
435
|
}
|
|
436
|
-
var schemaV, iso, ticketFetched, ticketEnriched, scenarioGenerated, pomGenerated, pomPromoted, runCompleted, classification, runClassified, classificationDisputed, edgeDiscovered, base, EventSchema;
|
|
436
|
+
var schemaV, iso, ticketFetched, ticketEnriched, scenarioGenerated, pomGenerated, pomPromoted, runCompleted, classification, runClassified, classificationDisputed, edgeDiscovered, coverageSnapshot, acCoverageBackfilled, base, EventSchema;
|
|
437
437
|
var init_schema = __esm(() => {
|
|
438
|
-
schemaV =
|
|
439
|
-
iso =
|
|
440
|
-
ticketFetched =
|
|
441
|
-
ticketId:
|
|
442
|
-
summary:
|
|
443
|
-
ac:
|
|
444
|
-
jiraLinks:
|
|
445
|
-
ticketId:
|
|
446
|
-
relation:
|
|
438
|
+
schemaV = z.literal(SCHEMA_VERSION);
|
|
439
|
+
iso = z.string().datetime({ offset: false });
|
|
440
|
+
ticketFetched = z.object({
|
|
441
|
+
ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
442
|
+
summary: z.string(),
|
|
443
|
+
ac: z.array(z.string()),
|
|
444
|
+
jiraLinks: z.array(z.object({
|
|
445
|
+
ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
446
|
+
relation: z.enum(["blocks", "duplicates", "relates", "supersedes"])
|
|
447
447
|
})),
|
|
448
|
-
storyHash:
|
|
449
|
-
modifiesAreas:
|
|
448
|
+
storyHash: z.string(),
|
|
449
|
+
modifiesAreas: z.array(z.string().regex(/^[a-z0-9-]+$/))
|
|
450
450
|
}).passthrough();
|
|
451
|
-
ticketEnriched =
|
|
452
|
-
ticketId:
|
|
451
|
+
ticketEnriched = z.object({
|
|
452
|
+
ticketId: z.string(),
|
|
453
453
|
enrichedAt: iso,
|
|
454
|
-
similarCount:
|
|
454
|
+
similarCount: z.number().int().nonnegative()
|
|
455
455
|
}).passthrough();
|
|
456
|
-
scenarioGenerated =
|
|
457
|
-
scenarioId:
|
|
458
|
-
ticketId:
|
|
459
|
-
name:
|
|
460
|
-
gherkin:
|
|
461
|
-
priority:
|
|
462
|
-
featureHash:
|
|
463
|
-
generatedAt: iso
|
|
456
|
+
scenarioGenerated = z.object({
|
|
457
|
+
scenarioId: z.string(),
|
|
458
|
+
ticketId: z.string(),
|
|
459
|
+
name: z.string(),
|
|
460
|
+
gherkin: z.string(),
|
|
461
|
+
priority: z.enum(["p0", "p1", "p2"]),
|
|
462
|
+
featureHash: z.string(),
|
|
463
|
+
generatedAt: iso,
|
|
464
|
+
satisfiesAcs: z.array(z.number().int().nonnegative()).optional()
|
|
464
465
|
}).passthrough();
|
|
465
|
-
pomGenerated =
|
|
466
|
-
pomId:
|
|
467
|
-
ticketId:
|
|
468
|
-
filePath:
|
|
469
|
-
route:
|
|
470
|
-
locators:
|
|
471
|
-
scope:
|
|
466
|
+
pomGenerated = z.object({
|
|
467
|
+
pomId: z.string(),
|
|
468
|
+
ticketId: z.string(),
|
|
469
|
+
filePath: z.string(),
|
|
470
|
+
route: z.string(),
|
|
471
|
+
locators: z.array(z.string()),
|
|
472
|
+
scope: z.enum(["local", "shared"])
|
|
472
473
|
}).passthrough();
|
|
473
|
-
pomPromoted =
|
|
474
|
-
pomId:
|
|
475
|
-
fromPath:
|
|
476
|
-
toPath:
|
|
474
|
+
pomPromoted = z.object({
|
|
475
|
+
pomId: z.string(),
|
|
476
|
+
fromPath: z.string(),
|
|
477
|
+
toPath: z.string()
|
|
477
478
|
}).passthrough();
|
|
478
|
-
runCompleted =
|
|
479
|
-
scenarioId:
|
|
480
|
-
ticketId:
|
|
481
|
-
runId:
|
|
482
|
-
status:
|
|
483
|
-
traceId:
|
|
484
|
-
runtime:
|
|
479
|
+
runCompleted = z.object({
|
|
480
|
+
scenarioId: z.string(),
|
|
481
|
+
ticketId: z.string(),
|
|
482
|
+
runId: z.string(),
|
|
483
|
+
status: z.enum(["pass", "fail"]),
|
|
484
|
+
traceId: z.string().optional(),
|
|
485
|
+
runtime: z.number().nonnegative()
|
|
485
486
|
}).passthrough();
|
|
486
|
-
classification =
|
|
487
|
+
classification = z.enum([
|
|
487
488
|
"REAL_BUG",
|
|
488
489
|
"TEST_BUG",
|
|
489
490
|
"SELECTOR_DRIFT",
|
|
@@ -494,54 +495,94 @@ var init_schema = __esm(() => {
|
|
|
494
495
|
"RATE_LIMITED",
|
|
495
496
|
"AUTH_EXPIRED"
|
|
496
497
|
]);
|
|
497
|
-
runClassified =
|
|
498
|
-
scenarioId:
|
|
499
|
-
runId:
|
|
498
|
+
runClassified = z.object({
|
|
499
|
+
scenarioId: z.string(),
|
|
500
|
+
runId: z.string(),
|
|
500
501
|
classification,
|
|
501
|
-
confidence:
|
|
502
|
+
confidence: z.enum(["low", "medium", "high"])
|
|
502
503
|
}).passthrough();
|
|
503
|
-
classificationDisputed =
|
|
504
|
-
runId:
|
|
505
|
-
scenarioId:
|
|
504
|
+
classificationDisputed = z.object({
|
|
505
|
+
runId: z.string(),
|
|
506
|
+
scenarioId: z.string(),
|
|
506
507
|
originalClassification: classification,
|
|
507
508
|
disputedTo: classification,
|
|
508
|
-
qaActor:
|
|
509
|
-
qaReason:
|
|
509
|
+
qaActor: z.string(),
|
|
510
|
+
qaReason: z.string().optional()
|
|
511
|
+
}).passthrough();
|
|
512
|
+
edgeDiscovered = z.object({
|
|
513
|
+
kind: z.enum([
|
|
514
|
+
"tests",
|
|
515
|
+
"uses",
|
|
516
|
+
"covers",
|
|
517
|
+
"modifies",
|
|
518
|
+
"jira-linked",
|
|
519
|
+
"similar",
|
|
520
|
+
"ran",
|
|
521
|
+
"satisfies"
|
|
522
|
+
]),
|
|
523
|
+
from: z.string(),
|
|
524
|
+
to: z.string(),
|
|
525
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
526
|
+
source: z.string()
|
|
510
527
|
}).passthrough();
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
528
|
+
coverageSnapshot = z.object({
|
|
529
|
+
ts: iso,
|
|
530
|
+
windowDays: z.number().int().positive(),
|
|
531
|
+
areas: z.array(z.object({
|
|
532
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
533
|
+
status: z.enum(["UNCOVERED", "STALE", "COVERED"]),
|
|
534
|
+
risk: z.number().nonnegative(),
|
|
535
|
+
breakdown: z.object({
|
|
536
|
+
recentTickets: z.number().int().nonnegative(),
|
|
537
|
+
recentBugs: z.number().int().nonnegative(),
|
|
538
|
+
criticalBoost: z.union([z.literal(1), z.literal(2)])
|
|
539
|
+
})
|
|
540
|
+
})),
|
|
541
|
+
tickets: z.array(z.object({
|
|
542
|
+
id: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
543
|
+
acCount: z.number().int().nonnegative(),
|
|
544
|
+
satisfiedCount: z.number().int().nonnegative(),
|
|
545
|
+
gapScore: z.number().nonnegative()
|
|
546
|
+
}))
|
|
547
|
+
}).passthrough();
|
|
548
|
+
acCoverageBackfilled = z.object({
|
|
549
|
+
ts: iso,
|
|
550
|
+
ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
551
|
+
mappings: z.array(z.object({
|
|
552
|
+
scenarioId: z.string().min(1),
|
|
553
|
+
satisfiesAcs: z.array(z.number().int().nonnegative()),
|
|
554
|
+
confidence: z.number().min(0).max(1)
|
|
555
|
+
}))
|
|
517
556
|
}).passthrough();
|
|
518
557
|
base = {
|
|
519
|
-
event_id:
|
|
558
|
+
event_id: z.string().min(20),
|
|
520
559
|
schema_version: schemaV,
|
|
521
560
|
ts: iso,
|
|
522
|
-
actor:
|
|
561
|
+
actor: z.string()
|
|
523
562
|
};
|
|
524
|
-
EventSchema =
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
563
|
+
EventSchema = z.discriminatedUnion("type", [
|
|
564
|
+
z.object({ ...base, type: z.literal("ticket.fetched"), payload: ticketFetched }),
|
|
565
|
+
z.object({ ...base, type: z.literal("ticket.enriched"), payload: ticketEnriched }),
|
|
566
|
+
z.object({ ...base, type: z.literal("scenario.generated"), payload: scenarioGenerated }),
|
|
567
|
+
z.object({ ...base, type: z.literal("pom.generated"), payload: pomGenerated }),
|
|
568
|
+
z.object({ ...base, type: z.literal("pom.promoted"), payload: pomPromoted }),
|
|
569
|
+
z.object({ ...base, type: z.literal("run.completed"), payload: runCompleted }),
|
|
570
|
+
z.object({ ...base, type: z.literal("run.classified"), payload: runClassified }),
|
|
571
|
+
z.object({
|
|
533
572
|
...base,
|
|
534
|
-
type:
|
|
573
|
+
type: z.literal("classification.disputed"),
|
|
535
574
|
payload: classificationDisputed
|
|
536
575
|
}),
|
|
537
|
-
|
|
576
|
+
z.object({ ...base, type: z.literal("edge.discovered"), payload: edgeDiscovered }),
|
|
577
|
+
z.object({ ...base, type: z.literal("coverage.snapshot"), payload: coverageSnapshot }),
|
|
578
|
+
z.object({ ...base, type: z.literal("ac-coverage.backfilled"), payload: acCoverageBackfilled })
|
|
538
579
|
]);
|
|
539
580
|
});
|
|
540
581
|
|
|
541
582
|
// src/graph/store.ts
|
|
542
583
|
import { createHash } from "crypto";
|
|
543
584
|
import {
|
|
544
|
-
existsSync
|
|
585
|
+
existsSync,
|
|
545
586
|
mkdirSync,
|
|
546
587
|
readdirSync,
|
|
547
588
|
readFileSync,
|
|
@@ -568,7 +609,7 @@ function appendEvents(repoRoot, events, opts) {
|
|
|
568
609
|
}
|
|
569
610
|
function loadAllEvents(repoRoot) {
|
|
570
611
|
const paths = graphPaths(repoRoot);
|
|
571
|
-
if (!
|
|
612
|
+
if (!existsSync(paths.eventsDir))
|
|
572
613
|
return [];
|
|
573
614
|
const files = [];
|
|
574
615
|
for (const monthDir of readdirSync(paths.eventsDir, { withFileTypes: true })) {
|
|
@@ -625,11 +666,14 @@ function deriveSnapshot(events) {
|
|
|
625
666
|
const areas = {};
|
|
626
667
|
const edges = [];
|
|
627
668
|
const latestFailures = {};
|
|
669
|
+
const acNodes = {};
|
|
670
|
+
const classifications = [];
|
|
628
671
|
for (const e of events) {
|
|
629
672
|
switch (e.type) {
|
|
630
|
-
case "ticket.fetched":
|
|
631
|
-
|
|
632
|
-
|
|
673
|
+
case "ticket.fetched": {
|
|
674
|
+
const tid = e.payload.ticketId;
|
|
675
|
+
tickets[tid] = {
|
|
676
|
+
id: tid,
|
|
633
677
|
summary: e.payload.summary,
|
|
634
678
|
ac: e.payload.ac,
|
|
635
679
|
storyHash: e.payload.storyHash,
|
|
@@ -641,18 +685,34 @@ function deriveSnapshot(events) {
|
|
|
641
685
|
for (const link of e.payload.jiraLinks) {
|
|
642
686
|
edges.push({
|
|
643
687
|
kind: "jira-linked",
|
|
644
|
-
from:
|
|
688
|
+
from: tid,
|
|
645
689
|
to: link.ticketId,
|
|
646
690
|
source: `jira:${link.relation}`,
|
|
647
691
|
discoveredAt: e.ts
|
|
648
692
|
});
|
|
649
693
|
}
|
|
694
|
+
for (const acId of Object.keys(acNodes)) {
|
|
695
|
+
if (acNodes[acId]?.ticketId === tid)
|
|
696
|
+
delete acNodes[acId];
|
|
697
|
+
}
|
|
698
|
+
e.payload.ac.forEach((text, index) => {
|
|
699
|
+
const acId = `${tid}#ac-${index}`;
|
|
700
|
+
acNodes[acId] = { id: acId, ticketId: tid, index, text };
|
|
701
|
+
});
|
|
702
|
+
for (let i = edges.length - 1;i >= 0; i--) {
|
|
703
|
+
const ed = edges[i];
|
|
704
|
+
if (ed.kind !== "satisfies")
|
|
705
|
+
continue;
|
|
706
|
+
if (acNodes[ed.to] === undefined)
|
|
707
|
+
edges.splice(i, 1);
|
|
708
|
+
}
|
|
650
709
|
break;
|
|
710
|
+
}
|
|
651
711
|
case "ticket.enriched":
|
|
652
712
|
if (tickets[e.payload.ticketId])
|
|
653
713
|
tickets[e.payload.ticketId].enrichedAt = e.payload.enrichedAt;
|
|
654
714
|
break;
|
|
655
|
-
case "scenario.generated":
|
|
715
|
+
case "scenario.generated": {
|
|
656
716
|
scenarios[e.payload.scenarioId] = {
|
|
657
717
|
id: e.payload.scenarioId,
|
|
658
718
|
ticketId: e.payload.ticketId,
|
|
@@ -669,7 +729,29 @@ function deriveSnapshot(events) {
|
|
|
669
729
|
source: "xera-script",
|
|
670
730
|
discoveredAt: e.ts
|
|
671
731
|
});
|
|
732
|
+
if (e.payload.satisfiesAcs && e.payload.satisfiesAcs.length > 0) {
|
|
733
|
+
for (let i = edges.length - 1;i >= 0; i--) {
|
|
734
|
+
const ed = edges[i];
|
|
735
|
+
if (ed.kind === "satisfies" && ed.from === e.payload.scenarioId && ed.source === "xera-script") {
|
|
736
|
+
edges.splice(i, 1);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
for (const acIdx of e.payload.satisfiesAcs) {
|
|
740
|
+
const acId = `${e.payload.ticketId}#ac-${acIdx}`;
|
|
741
|
+
if (acNodes[acId] === undefined)
|
|
742
|
+
continue;
|
|
743
|
+
edges.push({
|
|
744
|
+
kind: "satisfies",
|
|
745
|
+
from: e.payload.scenarioId,
|
|
746
|
+
to: acId,
|
|
747
|
+
confidence: 1,
|
|
748
|
+
source: "xera-script",
|
|
749
|
+
discoveredAt: e.ts
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
672
753
|
break;
|
|
754
|
+
}
|
|
673
755
|
case "pom.generated":
|
|
674
756
|
poms[e.payload.pomId] = {
|
|
675
757
|
id: e.payload.pomId,
|
|
@@ -721,6 +803,40 @@ function deriveSnapshot(events) {
|
|
|
721
803
|
}
|
|
722
804
|
break;
|
|
723
805
|
}
|
|
806
|
+
case "run.classified":
|
|
807
|
+
classifications.push({
|
|
808
|
+
scenarioId: e.payload.scenarioId,
|
|
809
|
+
classification: e.payload.classification,
|
|
810
|
+
ts: e.ts
|
|
811
|
+
});
|
|
812
|
+
break;
|
|
813
|
+
case "ac-coverage.backfilled": {
|
|
814
|
+
const { ts, ticketId, mappings } = e.payload;
|
|
815
|
+
for (let i = edges.length - 1;i >= 0; i--) {
|
|
816
|
+
const ed = edges[i];
|
|
817
|
+
if (ed.kind === "satisfies" && ed.source === "ac-coverage" && ed.to.startsWith(`${ticketId}#ac-`)) {
|
|
818
|
+
edges.splice(i, 1);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const m of mappings) {
|
|
822
|
+
for (const acIdx of m.satisfiesAcs) {
|
|
823
|
+
const acId = `${ticketId}#ac-${acIdx}`;
|
|
824
|
+
if (acNodes[acId] === undefined)
|
|
825
|
+
continue;
|
|
826
|
+
edges.push({
|
|
827
|
+
kind: "satisfies",
|
|
828
|
+
from: m.scenarioId,
|
|
829
|
+
to: acId,
|
|
830
|
+
confidence: m.confidence,
|
|
831
|
+
source: "ac-coverage",
|
|
832
|
+
discoveredAt: ts
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case "coverage.snapshot":
|
|
839
|
+
break;
|
|
724
840
|
default:
|
|
725
841
|
break;
|
|
726
842
|
}
|
|
@@ -735,7 +851,9 @@ function deriveSnapshot(events) {
|
|
|
735
851
|
poms,
|
|
736
852
|
areas,
|
|
737
853
|
edges,
|
|
738
|
-
latest_failures: latestFailures
|
|
854
|
+
latest_failures: latestFailures,
|
|
855
|
+
acNodes,
|
|
856
|
+
classifications
|
|
739
857
|
};
|
|
740
858
|
}
|
|
741
859
|
function writeSnapshot(repoRoot, snap) {
|
|
@@ -747,7 +865,7 @@ function writeSnapshot(repoRoot, snap) {
|
|
|
747
865
|
}
|
|
748
866
|
function loadSnapshot(repoRoot) {
|
|
749
867
|
const paths = graphPaths(repoRoot);
|
|
750
|
-
if (!
|
|
868
|
+
if (!existsSync(paths.snapshotFile))
|
|
751
869
|
return null;
|
|
752
870
|
try {
|
|
753
871
|
return JSON.parse(readFileSync(paths.snapshotFile, "utf8"));
|
|
@@ -825,8 +943,8 @@ __export(exports_graph_record_script, {
|
|
|
825
943
|
recordScriptImpl: () => recordScriptImpl
|
|
826
944
|
});
|
|
827
945
|
import { createHash as createHash2 } from "crypto";
|
|
828
|
-
import { existsSync as
|
|
829
|
-
import { basename, join as
|
|
946
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
947
|
+
import { basename, join as join8 } from "path";
|
|
830
948
|
function inferPriority(name, gherkin) {
|
|
831
949
|
const haystack = `${name} ${gherkin}`.toLowerCase();
|
|
832
950
|
for (const kw of P0_KEYWORDS) {
|
|
@@ -868,9 +986,9 @@ function parseFeature(text) {
|
|
|
868
986
|
return scenarios;
|
|
869
987
|
}
|
|
870
988
|
function listPomFiles(dir) {
|
|
871
|
-
if (!
|
|
989
|
+
if (!existsSync7(dir))
|
|
872
990
|
return [];
|
|
873
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) =>
|
|
991
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join8(dir, f));
|
|
874
992
|
}
|
|
875
993
|
function extractRoute(pomContent) {
|
|
876
994
|
const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
|
|
@@ -897,15 +1015,15 @@ function extractPomUsage(specContent) {
|
|
|
897
1015
|
return [...names];
|
|
898
1016
|
}
|
|
899
1017
|
async function recordScriptImpl(repoRoot, ticket) {
|
|
900
|
-
const ticketDir =
|
|
901
|
-
const featurePath =
|
|
902
|
-
const specPath =
|
|
903
|
-
const pomDir =
|
|
904
|
-
if (!
|
|
1018
|
+
const ticketDir = join8(repoRoot, ".xera", ticket);
|
|
1019
|
+
const featurePath = existsSync7(join8(ticketDir, "test.feature")) ? join8(ticketDir, "test.feature") : join8(ticketDir, "feature", `${ticket}.feature`);
|
|
1020
|
+
const specPath = existsSync7(join8(ticketDir, "spec.ts")) ? join8(ticketDir, "spec.ts") : join8(ticketDir, "tests", `${ticket}.spec.ts`);
|
|
1021
|
+
const pomDir = existsSync7(join8(ticketDir, "page-objects")) ? join8(ticketDir, "page-objects") : join8(ticketDir, "poms");
|
|
1022
|
+
if (!existsSync7(featurePath)) {
|
|
905
1023
|
console.error(`[graph-record script] feature missing`);
|
|
906
1024
|
return 1;
|
|
907
1025
|
}
|
|
908
|
-
const featureText =
|
|
1026
|
+
const featureText = readFileSync6(featurePath, "utf8");
|
|
909
1027
|
const featureHash = sha1(featureText);
|
|
910
1028
|
const scenarios = parseFeature(featureText);
|
|
911
1029
|
const events = [];
|
|
@@ -925,7 +1043,7 @@ async function recordScriptImpl(repoRoot, ticket) {
|
|
|
925
1043
|
const pomFiles = listPomFiles(pomDir);
|
|
926
1044
|
const pomNameToId = new Map;
|
|
927
1045
|
for (const pomFile of pomFiles) {
|
|
928
|
-
const content =
|
|
1046
|
+
const content = readFileSync6(pomFile, "utf8");
|
|
929
1047
|
const id = pId(pomFile);
|
|
930
1048
|
const className = content.match(/export\s+class\s+([A-Z][A-Za-z0-9]*Page)/)?.[1] ?? "";
|
|
931
1049
|
pomNameToId.set(className, id);
|
|
@@ -939,8 +1057,8 @@ async function recordScriptImpl(repoRoot, ticket) {
|
|
|
939
1057
|
};
|
|
940
1058
|
events.push(mk("xera-script", "pom.generated", pg));
|
|
941
1059
|
}
|
|
942
|
-
if (
|
|
943
|
-
const specContent =
|
|
1060
|
+
if (existsSync7(specPath)) {
|
|
1061
|
+
const specContent = readFileSync6(specPath, "utf8");
|
|
944
1062
|
const usedPoms = extractPomUsage(specContent);
|
|
945
1063
|
for (const scenario of scenarios) {
|
|
946
1064
|
const scId = sId(ticket, scenario.name);
|
|
@@ -7998,33 +8116,33 @@ var init_dist = __esm(() => {
|
|
|
7998
8116
|
});
|
|
7999
8117
|
|
|
8000
8118
|
// src/artifact/paths.ts
|
|
8001
|
-
import { join as
|
|
8119
|
+
import { join as join9 } from "path";
|
|
8002
8120
|
function resolveArtifactPaths(repoRoot, ticket) {
|
|
8003
|
-
if (!
|
|
8121
|
+
if (!TICKET_RE2.test(ticket)) {
|
|
8004
8122
|
throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
|
|
8005
8123
|
}
|
|
8006
|
-
const ticketDir =
|
|
8124
|
+
const ticketDir = join9(repoRoot, ".xera", ticket);
|
|
8007
8125
|
return {
|
|
8008
8126
|
ticketDir,
|
|
8009
|
-
storyPath:
|
|
8010
|
-
featurePath:
|
|
8011
|
-
specPath:
|
|
8012
|
-
pageObjectsDir:
|
|
8013
|
-
runsDir:
|
|
8014
|
-
metaPath:
|
|
8015
|
-
statusPath:
|
|
8016
|
-
logPath:
|
|
8017
|
-
lockPath:
|
|
8018
|
-
authDir:
|
|
8127
|
+
storyPath: join9(ticketDir, "story.md"),
|
|
8128
|
+
featurePath: join9(ticketDir, "test.feature"),
|
|
8129
|
+
specPath: join9(ticketDir, "spec.ts"),
|
|
8130
|
+
pageObjectsDir: join9(ticketDir, "page-objects"),
|
|
8131
|
+
runsDir: join9(ticketDir, "runs"),
|
|
8132
|
+
metaPath: join9(ticketDir, "meta.json"),
|
|
8133
|
+
statusPath: join9(ticketDir, "status.json"),
|
|
8134
|
+
logPath: join9(ticketDir, "xera.log"),
|
|
8135
|
+
lockPath: join9(ticketDir, ".lock"),
|
|
8136
|
+
authDir: join9(repoRoot, ".xera", ".auth"),
|
|
8019
8137
|
runPath: (runId) => {
|
|
8020
|
-
const runDir =
|
|
8138
|
+
const runDir = join9(ticketDir, "runs", runId);
|
|
8021
8139
|
return {
|
|
8022
8140
|
runDir,
|
|
8023
|
-
reportJsonPath:
|
|
8024
|
-
tracePath:
|
|
8025
|
-
normalizedPath:
|
|
8026
|
-
screenshotsDir:
|
|
8027
|
-
videoDir:
|
|
8141
|
+
reportJsonPath: join9(runDir, "report.json"),
|
|
8142
|
+
tracePath: join9(runDir, "trace.zip"),
|
|
8143
|
+
normalizedPath: join9(runDir, "normalized.json"),
|
|
8144
|
+
screenshotsDir: join9(runDir, "screenshots"),
|
|
8145
|
+
videoDir: join9(runDir, "videos")
|
|
8028
8146
|
};
|
|
8029
8147
|
}
|
|
8030
8148
|
};
|
|
@@ -8032,9 +8150,9 @@ function resolveArtifactPaths(repoRoot, ticket) {
|
|
|
8032
8150
|
function generateRunId(now = new Date) {
|
|
8033
8151
|
return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
|
|
8034
8152
|
}
|
|
8035
|
-
var
|
|
8153
|
+
var TICKET_RE2;
|
|
8036
8154
|
var init_paths2 = __esm(() => {
|
|
8037
|
-
|
|
8155
|
+
TICKET_RE2 = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
|
|
8038
8156
|
});
|
|
8039
8157
|
|
|
8040
8158
|
// src/bin-internal/graph-record.ts
|
|
@@ -8044,8 +8162,8 @@ __export(exports_graph_record, {
|
|
|
8044
8162
|
graphRecordCmd: () => graphRecordCmd
|
|
8045
8163
|
});
|
|
8046
8164
|
import { createHash as createHash3 } from "crypto";
|
|
8047
|
-
import { existsSync as
|
|
8048
|
-
import { basename as basename2, join as
|
|
8165
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
8166
|
+
import { basename as basename2, join as join10 } from "path";
|
|
8049
8167
|
function nowIso2() {
|
|
8050
8168
|
return new Date().toISOString();
|
|
8051
8169
|
}
|
|
@@ -8069,21 +8187,21 @@ function makeEvent(actor, type, payload) {
|
|
|
8069
8187
|
};
|
|
8070
8188
|
}
|
|
8071
8189
|
function readStoryFrontmatter(repoRoot, ticket) {
|
|
8072
|
-
const path =
|
|
8073
|
-
if (!
|
|
8190
|
+
const path = join10(repoRoot, ".xera", ticket, "story.md");
|
|
8191
|
+
if (!existsSync8(path))
|
|
8074
8192
|
return null;
|
|
8075
|
-
const raw =
|
|
8193
|
+
const raw = readFileSync7(path, "utf8");
|
|
8076
8194
|
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
8077
8195
|
if (!m)
|
|
8078
8196
|
return null;
|
|
8079
8197
|
return $parse(m[1]);
|
|
8080
8198
|
}
|
|
8081
8199
|
function readGraphInput(repoRoot, ticket) {
|
|
8082
|
-
const path =
|
|
8083
|
-
if (!
|
|
8200
|
+
const path = join10(repoRoot, ".xera", ticket, "graph-input.json");
|
|
8201
|
+
if (!existsSync8(path))
|
|
8084
8202
|
return { modifiesAreas: [] };
|
|
8085
8203
|
try {
|
|
8086
|
-
return JSON.parse(
|
|
8204
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
8087
8205
|
} catch {
|
|
8088
8206
|
return { modifiesAreas: [] };
|
|
8089
8207
|
}
|
|
@@ -8132,11 +8250,11 @@ async function recordScript(repoRoot, ticket) {
|
|
|
8132
8250
|
}
|
|
8133
8251
|
async function recordExec(repoRoot, ticket, runId) {
|
|
8134
8252
|
const { normalizedPath } = resolveArtifactPaths(repoRoot, ticket).runPath(runId);
|
|
8135
|
-
if (!
|
|
8253
|
+
if (!existsSync8(normalizedPath)) {
|
|
8136
8254
|
console.error(`[graph-record exec] normalized.json missing`);
|
|
8137
8255
|
return 1;
|
|
8138
8256
|
}
|
|
8139
|
-
const data = JSON.parse(
|
|
8257
|
+
const data = JSON.parse(readFileSync7(normalizedPath, "utf8"));
|
|
8140
8258
|
const events = [];
|
|
8141
8259
|
for (const s of data.scenarios) {
|
|
8142
8260
|
if (s.outcome === "SKIPPED")
|
|
@@ -8155,12 +8273,12 @@ async function recordExec(repoRoot, ticket, runId) {
|
|
|
8155
8273
|
}
|
|
8156
8274
|
async function recordClassify(repoRoot, ticket, runId) {
|
|
8157
8275
|
const { ticketDir } = resolveArtifactPaths(repoRoot, ticket);
|
|
8158
|
-
const classifyPath =
|
|
8159
|
-
if (!
|
|
8276
|
+
const classifyPath = join10(ticketDir, "classifier-input.json");
|
|
8277
|
+
if (!existsSync8(classifyPath)) {
|
|
8160
8278
|
console.error(`[graph-record classify] classifier-input.json missing`);
|
|
8161
8279
|
return 1;
|
|
8162
8280
|
}
|
|
8163
|
-
const data = JSON.parse(
|
|
8281
|
+
const data = JSON.parse(readFileSync7(classifyPath, "utf8"));
|
|
8164
8282
|
const events = [];
|
|
8165
8283
|
for (const s of data.scenarios) {
|
|
8166
8284
|
const p = {
|
|
@@ -8302,11 +8420,11 @@ var exports_graph_backfill = {};
|
|
|
8302
8420
|
__export(exports_graph_backfill, {
|
|
8303
8421
|
graphBackfillCmd: () => graphBackfillCmd
|
|
8304
8422
|
});
|
|
8305
|
-
import { existsSync as
|
|
8306
|
-
import { join as
|
|
8423
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
|
|
8424
|
+
import { join as join11 } from "path";
|
|
8307
8425
|
async function backfillTicket(repoRoot, ticket, dryRun) {
|
|
8308
|
-
const storyPath =
|
|
8309
|
-
if (!
|
|
8426
|
+
const storyPath = join11(repoRoot, ".xera", ticket, "story.md");
|
|
8427
|
+
if (!existsSync9(storyPath))
|
|
8310
8428
|
return 0;
|
|
8311
8429
|
const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
|
|
8312
8430
|
if (dryRun) {
|
|
@@ -8320,8 +8438,8 @@ async function backfillTicket(repoRoot, ticket, dryRun) {
|
|
|
8320
8438
|
async function graphBackfillCmd(argv) {
|
|
8321
8439
|
const dryRun = argv.includes("--dry-run");
|
|
8322
8440
|
const repoRoot = process.cwd();
|
|
8323
|
-
const xeraDir =
|
|
8324
|
-
if (!
|
|
8441
|
+
const xeraDir = join11(repoRoot, ".xera");
|
|
8442
|
+
if (!existsSync9(xeraDir)) {
|
|
8325
8443
|
console.log("[backfill] no .xera/ directory");
|
|
8326
8444
|
return 0;
|
|
8327
8445
|
}
|
|
@@ -8350,109 +8468,246 @@ var init_graph_backfill = __esm(() => {
|
|
|
8350
8468
|
// bin/internal.ts
|
|
8351
8469
|
var import_dotenv = __toESM(require_main(), 1);
|
|
8352
8470
|
|
|
8353
|
-
// src/bin-internal/
|
|
8354
|
-
|
|
8471
|
+
// src/bin-internal/ac-coverage-backfill-finalize.ts
|
|
8472
|
+
init_store();
|
|
8473
|
+
init_ulid();
|
|
8474
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
8355
8475
|
import { join as join2 } from "path";
|
|
8476
|
+
import { z as z2 } from "zod";
|
|
8477
|
+
var DecisionsSchema = z2.object({
|
|
8478
|
+
mappings: z2.array(z2.object({
|
|
8479
|
+
scenarioId: z2.string().min(1),
|
|
8480
|
+
satisfiesAcs: z2.array(z2.number().int().nonnegative()),
|
|
8481
|
+
confidence: z2.number().min(0).max(1)
|
|
8482
|
+
}))
|
|
8483
|
+
});
|
|
8484
|
+
function parseArgs(argv) {
|
|
8485
|
+
const args = {};
|
|
8486
|
+
for (let i = 0;i < argv.length; i++) {
|
|
8487
|
+
const a = argv[i];
|
|
8488
|
+
if (a === "--input") {
|
|
8489
|
+
const v = argv[++i];
|
|
8490
|
+
if (v !== undefined)
|
|
8491
|
+
args.inputFile = v;
|
|
8492
|
+
} else if (a === "--snapshot-ts") {
|
|
8493
|
+
const v = argv[++i];
|
|
8494
|
+
if (v !== undefined)
|
|
8495
|
+
args.snapshotTs = v;
|
|
8496
|
+
} else if (a === "--help-stub") {} else {
|
|
8497
|
+
console.error(`[ac-coverage-backfill-finalize] unknown flag: ${a}`);
|
|
8498
|
+
return args;
|
|
8499
|
+
}
|
|
8500
|
+
}
|
|
8501
|
+
return args;
|
|
8502
|
+
}
|
|
8503
|
+
async function acCoverageBackfillFinalizeCmd(argv) {
|
|
8504
|
+
const args = parseArgs(argv);
|
|
8505
|
+
const cwd = process.cwd();
|
|
8506
|
+
const inputPath = args.inputFile ?? join2(cwd, ".xera/coverage/ac-backfill-decisions.json");
|
|
8507
|
+
if (!existsSync2(inputPath)) {
|
|
8508
|
+
console.error(`[ac-coverage-backfill-finalize] decisions file not found: ${inputPath}`);
|
|
8509
|
+
return 2;
|
|
8510
|
+
}
|
|
8511
|
+
let parsed;
|
|
8512
|
+
try {
|
|
8513
|
+
const raw = JSON.parse(readFileSync2(inputPath, "utf8"));
|
|
8514
|
+
parsed = DecisionsSchema.parse(raw);
|
|
8515
|
+
} catch (e) {
|
|
8516
|
+
console.error(`[ac-coverage-backfill-finalize] invalid decisions: ${e.message}`);
|
|
8517
|
+
return 2;
|
|
8518
|
+
}
|
|
8519
|
+
if (parsed.mappings.length === 0)
|
|
8520
|
+
return 0;
|
|
8521
|
+
const byTicket = {};
|
|
8522
|
+
for (const m of parsed.mappings) {
|
|
8523
|
+
const ticketId = m.scenarioId.split("#")[0];
|
|
8524
|
+
if (!ticketId)
|
|
8525
|
+
continue;
|
|
8526
|
+
if (!byTicket[ticketId])
|
|
8527
|
+
byTicket[ticketId] = [];
|
|
8528
|
+
byTicket[ticketId].push(m);
|
|
8529
|
+
}
|
|
8530
|
+
const ts = args.snapshotTs ?? new Date().toISOString();
|
|
8531
|
+
const now = new Date(ts);
|
|
8532
|
+
for (const [ticketId, mappings] of Object.entries(byTicket)) {
|
|
8533
|
+
const event = {
|
|
8534
|
+
event_id: ulid(),
|
|
8535
|
+
schema_version: SCHEMA_VERSION,
|
|
8536
|
+
ts,
|
|
8537
|
+
actor: "xera-coverage",
|
|
8538
|
+
type: "ac-coverage.backfilled",
|
|
8539
|
+
payload: { ts, ticketId, mappings }
|
|
8540
|
+
};
|
|
8541
|
+
appendEvents(cwd, [event], { skill: "ac-coverage", ticketId, now });
|
|
8542
|
+
}
|
|
8543
|
+
return 0;
|
|
8544
|
+
}
|
|
8545
|
+
|
|
8546
|
+
// src/bin-internal/ac-coverage-backfill-prepare.ts
|
|
8547
|
+
init_store();
|
|
8548
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
8549
|
+
import { join as join3 } from "path";
|
|
8550
|
+
function findUnmapped(snap) {
|
|
8551
|
+
const out = [];
|
|
8552
|
+
for (const ticket of Object.values(snap.tickets)) {
|
|
8553
|
+
if (ticket.ac.length === 0)
|
|
8554
|
+
continue;
|
|
8555
|
+
const ticketScenarios = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
|
|
8556
|
+
if (ticketScenarios.length === 0)
|
|
8557
|
+
continue;
|
|
8558
|
+
const acsForTicket = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticket.id);
|
|
8559
|
+
const hasAnyEdge = snap.edges.some((e) => e.kind === "satisfies" && acsForTicket.some((ac) => ac.id === e.to));
|
|
8560
|
+
if (hasAnyEdge)
|
|
8561
|
+
continue;
|
|
8562
|
+
out.push({
|
|
8563
|
+
id: ticket.id,
|
|
8564
|
+
summary: ticket.summary,
|
|
8565
|
+
acs: ticket.ac,
|
|
8566
|
+
scenarios: ticketScenarios.map((s) => ({
|
|
8567
|
+
id: s.id,
|
|
8568
|
+
name: s.name,
|
|
8569
|
+
gherkin: s.gherkin
|
|
8570
|
+
}))
|
|
8571
|
+
});
|
|
8572
|
+
}
|
|
8573
|
+
return { tickets: out };
|
|
8574
|
+
}
|
|
8575
|
+
function parseArgs2(argv) {
|
|
8576
|
+
const args = {};
|
|
8577
|
+
for (let i = 0;i < argv.length; i++) {
|
|
8578
|
+
const a = argv[i];
|
|
8579
|
+
if (a === "--output") {
|
|
8580
|
+
const v = argv[++i];
|
|
8581
|
+
if (v !== undefined)
|
|
8582
|
+
args.outputFile = v;
|
|
8583
|
+
} else if (a === "--help-stub") {} else {
|
|
8584
|
+
console.error(`[ac-coverage-backfill-prepare] unknown flag: ${a}`);
|
|
8585
|
+
return args;
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
return args;
|
|
8589
|
+
}
|
|
8590
|
+
async function acCoverageBackfillPrepareCmd(argv) {
|
|
8591
|
+
const args = parseArgs2(argv);
|
|
8592
|
+
const cwd = process.cwd();
|
|
8593
|
+
const snap = deriveSnapshot(loadAllEvents(cwd));
|
|
8594
|
+
const input = findUnmapped(snap);
|
|
8595
|
+
const outDir = join3(cwd, ".xera/coverage");
|
|
8596
|
+
mkdirSync2(outDir, { recursive: true });
|
|
8597
|
+
const outPath = args.outputFile ?? join3(outDir, "ac-backfill-input.json");
|
|
8598
|
+
writeFileSync2(outPath, JSON.stringify(input, null, 2));
|
|
8599
|
+
return 0;
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8602
|
+
// src/bin-internal/auth-setup.ts
|
|
8603
|
+
import { existsSync as existsSync4 } from "fs";
|
|
8604
|
+
import { join as join5 } from "path";
|
|
8356
8605
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
8357
8606
|
|
|
8358
8607
|
// src/config/load.ts
|
|
8359
|
-
import { existsSync } from "fs";
|
|
8360
|
-
import { join } from "path";
|
|
8608
|
+
import { existsSync as existsSync3 } from "fs";
|
|
8609
|
+
import { join as join4 } from "path";
|
|
8361
8610
|
import { pathToFileURL } from "url";
|
|
8362
8611
|
|
|
8363
8612
|
// src/config/schema.ts
|
|
8364
|
-
import { z } from "zod";
|
|
8365
|
-
var AuthRoleSchema =
|
|
8366
|
-
envEmail:
|
|
8367
|
-
envPassword:
|
|
8613
|
+
import { z as z3 } from "zod";
|
|
8614
|
+
var AuthRoleSchema = z3.object({
|
|
8615
|
+
envEmail: z3.string().min(1),
|
|
8616
|
+
envPassword: z3.string().min(1)
|
|
8368
8617
|
});
|
|
8369
|
-
var AuthSchema =
|
|
8370
|
-
strategy:
|
|
8371
|
-
ttl:
|
|
8372
|
-
refreshBuffer:
|
|
8373
|
-
setupScript:
|
|
8374
|
-
roles:
|
|
8618
|
+
var AuthSchema = z3.object({
|
|
8619
|
+
strategy: z3.enum(["storageState", "apiToken", "none"]).default("none"),
|
|
8620
|
+
ttl: z3.string().default("8h"),
|
|
8621
|
+
refreshBuffer: z3.string().default("30m"),
|
|
8622
|
+
setupScript: z3.string().optional(),
|
|
8623
|
+
roles: z3.record(z3.string(), AuthRoleSchema).default({})
|
|
8375
8624
|
});
|
|
8376
|
-
var WebSchema =
|
|
8377
|
-
baseUrl:
|
|
8625
|
+
var WebSchema = z3.object({
|
|
8626
|
+
baseUrl: z3.record(z3.string(), z3.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
8378
8627
|
message: "baseUrl must have at least one environment"
|
|
8379
8628
|
}),
|
|
8380
|
-
defaultEnv:
|
|
8629
|
+
defaultEnv: z3.string(),
|
|
8381
8630
|
auth: AuthSchema.prefault({}),
|
|
8382
|
-
testData:
|
|
8383
|
-
users:
|
|
8631
|
+
testData: z3.object({
|
|
8632
|
+
users: z3.record(z3.string(), z3.object({ fromAuth: z3.string() })).default({})
|
|
8384
8633
|
}).prefault({})
|
|
8385
8634
|
}).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
|
|
8386
8635
|
message: "defaultEnv must exist in baseUrl map",
|
|
8387
8636
|
path: ["defaultEnv"]
|
|
8388
8637
|
});
|
|
8389
|
-
var HttpAuthRoleSchema =
|
|
8390
|
-
tokenEnv:
|
|
8391
|
-
userEnv:
|
|
8392
|
-
passEnv:
|
|
8393
|
-
tokenUrl:
|
|
8394
|
-
clientIdEnv:
|
|
8395
|
-
clientSecretEnv:
|
|
8396
|
-
scope:
|
|
8638
|
+
var HttpAuthRoleSchema = z3.object({
|
|
8639
|
+
tokenEnv: z3.string().optional(),
|
|
8640
|
+
userEnv: z3.string().optional(),
|
|
8641
|
+
passEnv: z3.string().optional(),
|
|
8642
|
+
tokenUrl: z3.string().url().optional(),
|
|
8643
|
+
clientIdEnv: z3.string().optional(),
|
|
8644
|
+
clientSecretEnv: z3.string().optional(),
|
|
8645
|
+
scope: z3.string().optional()
|
|
8397
8646
|
});
|
|
8398
|
-
var HttpAuthSchema =
|
|
8399
|
-
strategy:
|
|
8400
|
-
ttl:
|
|
8401
|
-
refreshBuffer:
|
|
8402
|
-
roles:
|
|
8647
|
+
var HttpAuthSchema = z3.object({
|
|
8648
|
+
strategy: z3.enum(["bearer", "apiKey", "basic", "oauth-cc", "custom", "none"]).default("none"),
|
|
8649
|
+
ttl: z3.string().default("8h"),
|
|
8650
|
+
refreshBuffer: z3.string().default("30m"),
|
|
8651
|
+
roles: z3.record(z3.string(), HttpAuthRoleSchema).default({})
|
|
8403
8652
|
});
|
|
8404
|
-
var HttpSchema =
|
|
8405
|
-
baseUrl:
|
|
8653
|
+
var HttpSchema = z3.object({
|
|
8654
|
+
baseUrl: z3.record(z3.string(), z3.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
8406
8655
|
message: "baseUrl must have at least one environment"
|
|
8407
8656
|
}),
|
|
8408
|
-
defaultEnv:
|
|
8409
|
-
spec:
|
|
8657
|
+
defaultEnv: z3.string(),
|
|
8658
|
+
spec: z3.string().optional(),
|
|
8410
8659
|
auth: HttpAuthSchema.prefault({})
|
|
8411
8660
|
}).refine((h) => h.baseUrl[h.defaultEnv] !== undefined, {
|
|
8412
8661
|
message: "defaultEnv must exist in baseUrl map",
|
|
8413
8662
|
path: ["defaultEnv"]
|
|
8414
8663
|
});
|
|
8415
|
-
var JiraSchema =
|
|
8416
|
-
baseUrl:
|
|
8417
|
-
projectKeys:
|
|
8418
|
-
fields:
|
|
8419
|
-
story:
|
|
8420
|
-
acceptanceCriteria:
|
|
8421
|
-
attachments:
|
|
8664
|
+
var JiraSchema = z3.object({
|
|
8665
|
+
baseUrl: z3.string().url(),
|
|
8666
|
+
projectKeys: z3.array(z3.string().min(1)).min(1),
|
|
8667
|
+
fields: z3.object({
|
|
8668
|
+
story: z3.string().min(1),
|
|
8669
|
+
acceptanceCriteria: z3.string().optional(),
|
|
8670
|
+
attachments: z3.string().default("attachment")
|
|
8422
8671
|
})
|
|
8423
8672
|
});
|
|
8424
|
-
var AISchema =
|
|
8425
|
-
livePageSnapshot:
|
|
8426
|
-
confidenceThreshold:
|
|
8427
|
-
maxRetries:
|
|
8428
|
-
typecheck:
|
|
8429
|
-
lint:
|
|
8430
|
-
validateFeature:
|
|
8673
|
+
var AISchema = z3.object({
|
|
8674
|
+
livePageSnapshot: z3.boolean().default(true),
|
|
8675
|
+
confidenceThreshold: z3.enum(["low", "medium", "high"]).default("medium"),
|
|
8676
|
+
maxRetries: z3.object({
|
|
8677
|
+
typecheck: z3.number().int().min(0).max(5).default(2),
|
|
8678
|
+
lint: z3.number().int().min(0).max(5).default(2),
|
|
8679
|
+
validateFeature: z3.number().int().min(0).max(5).default(2)
|
|
8431
8680
|
}).prefault({})
|
|
8432
8681
|
}).prefault({});
|
|
8433
|
-
var ReportingSchema =
|
|
8434
|
-
language:
|
|
8435
|
-
postToJira:
|
|
8436
|
-
transition:
|
|
8437
|
-
onPass:
|
|
8438
|
-
onFail:
|
|
8682
|
+
var ReportingSchema = z3.object({
|
|
8683
|
+
language: z3.enum(["en", "vi"]).default("en"),
|
|
8684
|
+
postToJira: z3.boolean().default(true),
|
|
8685
|
+
transition: z3.object({
|
|
8686
|
+
onPass: z3.string().nullable().default(null),
|
|
8687
|
+
onFail: z3.string().nullable().default(null)
|
|
8439
8688
|
}).prefault({}),
|
|
8440
|
-
artifactLinks:
|
|
8689
|
+
artifactLinks: z3.enum(["git", "local"]).default("git")
|
|
8441
8690
|
}).prefault({});
|
|
8442
|
-
var RunSchema =
|
|
8443
|
-
autoImpact:
|
|
8444
|
-
enabled:
|
|
8445
|
-
threshold:
|
|
8691
|
+
var RunSchema = z3.object({
|
|
8692
|
+
autoImpact: z3.object({
|
|
8693
|
+
enabled: z3.boolean().default(true),
|
|
8694
|
+
threshold: z3.number().nonnegative().default(8)
|
|
8446
8695
|
}).prefault({})
|
|
8447
8696
|
}).prefault({});
|
|
8448
|
-
var
|
|
8697
|
+
var CoverageSchema = z3.object({
|
|
8698
|
+
staleAfterDays: z3.number().int().positive().default(30),
|
|
8699
|
+
criticalAreas: z3.array(z3.string().regex(/^[a-z0-9-]+$/)).default([]),
|
|
8700
|
+
autoSnapshotOnCoverage: z3.boolean().default(true)
|
|
8701
|
+
}).prefault({});
|
|
8702
|
+
var XeraConfigSchema = z3.object({
|
|
8449
8703
|
jira: JiraSchema,
|
|
8450
8704
|
web: WebSchema.optional(),
|
|
8451
8705
|
http: HttpSchema.optional(),
|
|
8452
8706
|
ai: AISchema,
|
|
8453
8707
|
reporting: ReportingSchema,
|
|
8454
8708
|
run: RunSchema.prefault({}),
|
|
8455
|
-
|
|
8709
|
+
coverage: CoverageSchema,
|
|
8710
|
+
adapters: z3.array(z3.enum(["web", "http"])).min(1).default(["web"])
|
|
8456
8711
|
}).refine((c) => c.web !== undefined || c.http !== undefined, {
|
|
8457
8712
|
message: "At least one of `web` or `http` must be configured"
|
|
8458
8713
|
}).refine((c) => c.adapters.every((a) => (a === "web" ? c.web : c.http) !== undefined), {
|
|
@@ -8462,8 +8717,8 @@ var XeraConfigSchema = z.object({
|
|
|
8462
8717
|
|
|
8463
8718
|
// src/config/load.ts
|
|
8464
8719
|
async function loadConfig(cwd) {
|
|
8465
|
-
const path =
|
|
8466
|
-
if (!
|
|
8720
|
+
const path = join4(cwd, "xera.config.ts");
|
|
8721
|
+
if (!existsSync3(path)) {
|
|
8467
8722
|
throw new Error(`xera.config.ts not found in ${cwd}`);
|
|
8468
8723
|
}
|
|
8469
8724
|
const mod = await import(pathToFileURL(path).href);
|
|
@@ -8492,8 +8747,8 @@ async function authSetupCmd(argv) {
|
|
|
8492
8747
|
const opts = parseOpts(argv);
|
|
8493
8748
|
const cwd = process.cwd();
|
|
8494
8749
|
const config = await loadConfig(cwd);
|
|
8495
|
-
const authSetupScript =
|
|
8496
|
-
if (!
|
|
8750
|
+
const authSetupScript = join5(cwd, "shared", "auth-setup.ts");
|
|
8751
|
+
if (!existsSync4(authSetupScript)) {
|
|
8497
8752
|
console.error(`[xera:auth-setup] auth-setup.ts not found at ${authSetupScript}. Run 'bunx @xera-ai/cli init' first.`);
|
|
8498
8753
|
return 1;
|
|
8499
8754
|
}
|
|
@@ -8519,7 +8774,7 @@ async function authSetupCmd(argv) {
|
|
|
8519
8774
|
role: roleName,
|
|
8520
8775
|
creds: { email, password },
|
|
8521
8776
|
setupScriptPath: authSetupScript,
|
|
8522
|
-
authDir:
|
|
8777
|
+
authDir: join5(cwd, ".xera", ".auth"),
|
|
8523
8778
|
browser
|
|
8524
8779
|
});
|
|
8525
8780
|
console.log(`[xera:auth-setup] \u2713 ${roleName}.json (web)`);
|
|
@@ -8540,7 +8795,7 @@ async function authSetupCmd(argv) {
|
|
|
8540
8795
|
continue;
|
|
8541
8796
|
try {
|
|
8542
8797
|
await runHttpAuthSetup({
|
|
8543
|
-
authDir:
|
|
8798
|
+
authDir: join5(cwd, ".xera", ".auth"),
|
|
8544
8799
|
role: roleName,
|
|
8545
8800
|
config: config.http,
|
|
8546
8801
|
setupFn: mod.http,
|
|
@@ -8556,6 +8811,401 @@ async function authSetupCmd(argv) {
|
|
|
8556
8811
|
return exitCode;
|
|
8557
8812
|
}
|
|
8558
8813
|
|
|
8814
|
+
// src/bin-internal/coverage-prepare.ts
|
|
8815
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8816
|
+
import { join as join6 } from "path";
|
|
8817
|
+
|
|
8818
|
+
// src/coverage/status.ts
|
|
8819
|
+
function daysBetween(a, b) {
|
|
8820
|
+
return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
|
|
8821
|
+
}
|
|
8822
|
+
function computeScenarioStatus(scenarioId, snap, windowDays, now) {
|
|
8823
|
+
const events = snap.classifications.filter((c) => c.scenarioId === scenarioId).sort((a, b) => a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
|
|
8824
|
+
const latest = events[0];
|
|
8825
|
+
if (!latest)
|
|
8826
|
+
return "NOT_PASSING";
|
|
8827
|
+
if (latest.classification !== "PASS")
|
|
8828
|
+
return "NOT_PASSING";
|
|
8829
|
+
if (daysBetween(now, new Date(latest.ts)) > windowDays)
|
|
8830
|
+
return "NOT_PASSING";
|
|
8831
|
+
return "PASSING";
|
|
8832
|
+
}
|
|
8833
|
+
function computeAreaStatus(areaId, snap, windowDays, now) {
|
|
8834
|
+
const coveringPoms = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
|
|
8835
|
+
if (coveringPoms.length === 0)
|
|
8836
|
+
return "UNCOVERED";
|
|
8837
|
+
const scenariosInArea = snap.edges.filter((e) => e.kind === "uses" && coveringPoms.includes(e.to)).map((e) => e.from);
|
|
8838
|
+
const anyPassing = scenariosInArea.some((sid) => computeScenarioStatus(sid, snap, windowDays, now) === "PASSING");
|
|
8839
|
+
return anyPassing ? "COVERED" : "STALE";
|
|
8840
|
+
}
|
|
8841
|
+
function computeAcStatus(acId, snap, windowDays, now) {
|
|
8842
|
+
const edges = snap.edges.filter((e) => e.kind === "satisfies" && e.to === acId);
|
|
8843
|
+
if (edges.length === 0)
|
|
8844
|
+
return "UNSATISFIED";
|
|
8845
|
+
const anyPassing = edges.some((e) => computeScenarioStatus(e.from, snap, windowDays, now) === "PASSING");
|
|
8846
|
+
return anyPassing ? "SATISFIED" : "UNSATISFIED";
|
|
8847
|
+
}
|
|
8848
|
+
function computeTicketStatus(ticketId, snap, windowDays, now) {
|
|
8849
|
+
const acIds = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId).map((ac) => ac.id);
|
|
8850
|
+
if (acIds.length === 0)
|
|
8851
|
+
return "COMPLETE";
|
|
8852
|
+
const allSatisfied = acIds.every((acId) => computeAcStatus(acId, snap, windowDays, now) === "SATISFIED");
|
|
8853
|
+
return allSatisfied ? "COMPLETE" : "INCOMPLETE";
|
|
8854
|
+
}
|
|
8855
|
+
|
|
8856
|
+
// src/coverage/risk.ts
|
|
8857
|
+
var RISK_WEIGHTS = {
|
|
8858
|
+
criticalBoost: 2,
|
|
8859
|
+
bugClassifications: new Set(["REAL_BUG", "TEST_OUTDATED"]),
|
|
8860
|
+
recencyBoosts: { recent: 2, withinWindow: 1, older: 0.5 },
|
|
8861
|
+
recencyThresholdDays: 7
|
|
8862
|
+
};
|
|
8863
|
+
function daysBetween2(a, b) {
|
|
8864
|
+
return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
|
|
8865
|
+
}
|
|
8866
|
+
function computeAreaRisk(areaId, snap, config, now) {
|
|
8867
|
+
const recentTickets = snap.edges.filter((e) => e.kind === "modifies" && e.to === areaId).map((e) => snap.tickets[e.from]).filter((t) => t !== undefined).filter((t) => daysBetween2(now, new Date(t.fetchedAt)) <= config.staleAfterDays).length;
|
|
8868
|
+
const pomsInArea = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
|
|
8869
|
+
const scenariosInArea = new Set(snap.edges.filter((e) => e.kind === "uses" && pomsInArea.includes(e.to)).map((e) => e.from));
|
|
8870
|
+
const recentBugs = snap.classifications.filter((c) => scenariosInArea.has(c.scenarioId)).filter((c) => RISK_WEIGHTS.bugClassifications.has(c.classification)).filter((c) => daysBetween2(now, new Date(c.ts)) <= config.staleAfterDays).length;
|
|
8871
|
+
const criticalBoost = config.criticalAreas.includes(areaId) ? RISK_WEIGHTS.criticalBoost : 1;
|
|
8872
|
+
return recentTickets * criticalBoost + recentBugs;
|
|
8873
|
+
}
|
|
8874
|
+
function computeAcGapScore(ticketId, snap, config, now) {
|
|
8875
|
+
const ticket = snap.tickets[ticketId];
|
|
8876
|
+
if (!ticket)
|
|
8877
|
+
return 0;
|
|
8878
|
+
const acs = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId);
|
|
8879
|
+
const unsatisfied = acs.filter((ac) => computeAcStatus(ac.id, snap, config.staleAfterDays, now) === "UNSATISFIED").length;
|
|
8880
|
+
if (unsatisfied === 0)
|
|
8881
|
+
return 0;
|
|
8882
|
+
const days = daysBetween2(now, new Date(ticket.fetchedAt));
|
|
8883
|
+
let boost;
|
|
8884
|
+
if (days <= RISK_WEIGHTS.recencyThresholdDays) {
|
|
8885
|
+
boost = RISK_WEIGHTS.recencyBoosts.recent;
|
|
8886
|
+
} else if (days <= config.staleAfterDays) {
|
|
8887
|
+
boost = RISK_WEIGHTS.recencyBoosts.withinWindow;
|
|
8888
|
+
} else {
|
|
8889
|
+
boost = RISK_WEIGHTS.recencyBoosts.older;
|
|
8890
|
+
}
|
|
8891
|
+
return unsatisfied * boost;
|
|
8892
|
+
}
|
|
8893
|
+
|
|
8894
|
+
// src/coverage/report.ts
|
|
8895
|
+
var STATUS_RANK = {
|
|
8896
|
+
UNCOVERED: 0,
|
|
8897
|
+
STALE: 1,
|
|
8898
|
+
COVERED: 2
|
|
8899
|
+
};
|
|
8900
|
+
function daysBetween3(a, b) {
|
|
8901
|
+
return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
|
|
8902
|
+
}
|
|
8903
|
+
function buildCoverageReport(snap, config, now) {
|
|
8904
|
+
const areas = Object.keys(snap.areas).map((areaId) => {
|
|
8905
|
+
const status = computeAreaStatus(areaId, snap, config.staleAfterDays, now);
|
|
8906
|
+
const risk = computeAreaRisk(areaId, snap, config, now);
|
|
8907
|
+
const recentTickets = snap.edges.filter((e) => e.kind === "modifies" && e.to === areaId).map((e) => snap.tickets[e.from]).filter((t) => t !== undefined).filter((t) => daysBetween3(now, new Date(t.fetchedAt)) <= config.staleAfterDays).length;
|
|
8908
|
+
const pomsInArea = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
|
|
8909
|
+
const scenariosInArea = new Set(snap.edges.filter((e) => e.kind === "uses" && pomsInArea.includes(e.to)).map((e) => e.from));
|
|
8910
|
+
const recentBugs = snap.classifications.filter((c) => scenariosInArea.has(c.scenarioId)).filter((c) => RISK_WEIGHTS.bugClassifications.has(c.classification)).filter((c) => daysBetween3(now, new Date(c.ts)) <= config.staleAfterDays).length;
|
|
8911
|
+
const criticalBoost = config.criticalAreas.includes(areaId) ? 2 : 1;
|
|
8912
|
+
return {
|
|
8913
|
+
id: areaId,
|
|
8914
|
+
status,
|
|
8915
|
+
risk,
|
|
8916
|
+
breakdown: { recentTickets, recentBugs, criticalBoost }
|
|
8917
|
+
};
|
|
8918
|
+
});
|
|
8919
|
+
areas.sort((a, b) => {
|
|
8920
|
+
if (STATUS_RANK[a.status] !== STATUS_RANK[b.status]) {
|
|
8921
|
+
return STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
8922
|
+
}
|
|
8923
|
+
if (a.status === "COVERED")
|
|
8924
|
+
return a.id.localeCompare(b.id);
|
|
8925
|
+
if (b.risk !== a.risk)
|
|
8926
|
+
return b.risk - a.risk;
|
|
8927
|
+
return a.id.localeCompare(b.id);
|
|
8928
|
+
});
|
|
8929
|
+
const tickets = Object.values(snap.tickets).filter((t) => computeTicketStatus(t.id, snap, config.staleAfterDays, now) === "INCOMPLETE").map((t) => {
|
|
8930
|
+
const acs = Object.values(snap.acNodes).filter((ac) => ac.ticketId === t.id).sort((a, b) => a.index - b.index);
|
|
8931
|
+
const unsatisfiedAcs = acs.filter((ac) => computeAcStatus(ac.id, snap, config.staleAfterDays, now) === "UNSATISFIED").map((ac) => ({ index: ac.index, text: ac.text }));
|
|
8932
|
+
return {
|
|
8933
|
+
id: t.id,
|
|
8934
|
+
summary: t.summary,
|
|
8935
|
+
acCount: acs.length,
|
|
8936
|
+
satisfiedCount: acs.length - unsatisfiedAcs.length,
|
|
8937
|
+
gapScore: computeAcGapScore(t.id, snap, config, now),
|
|
8938
|
+
unsatisfiedAcs
|
|
8939
|
+
};
|
|
8940
|
+
}).sort((a, b) => b.gapScore - a.gapScore || a.id.localeCompare(b.id));
|
|
8941
|
+
return {
|
|
8942
|
+
generatedAt: now.toISOString(),
|
|
8943
|
+
windowDays: config.staleAfterDays,
|
|
8944
|
+
areas,
|
|
8945
|
+
tickets,
|
|
8946
|
+
acBackfillNeeded: needsBackfill(snap)
|
|
8947
|
+
};
|
|
8948
|
+
}
|
|
8949
|
+
function needsBackfill(snap) {
|
|
8950
|
+
for (const ticket of Object.values(snap.tickets)) {
|
|
8951
|
+
const acsForTicket = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticket.id);
|
|
8952
|
+
if (acsForTicket.length === 0)
|
|
8953
|
+
continue;
|
|
8954
|
+
const scenariosForTicket = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
|
|
8955
|
+
if (scenariosForTicket.length === 0)
|
|
8956
|
+
continue;
|
|
8957
|
+
const hasAnyEdge = snap.edges.some((e) => e.kind === "satisfies" && acsForTicket.some((ac) => ac.id === e.to));
|
|
8958
|
+
if (!hasAnyEdge)
|
|
8959
|
+
return true;
|
|
8960
|
+
}
|
|
8961
|
+
return false;
|
|
8962
|
+
}
|
|
8963
|
+
function pad(s, n) {
|
|
8964
|
+
return s.length >= n ? s : `${s}${" ".repeat(n - s.length)}`;
|
|
8965
|
+
}
|
|
8966
|
+
function renderMarkdown(report, options = {}) {
|
|
8967
|
+
const lines = [];
|
|
8968
|
+
const dateOnly = report.generatedAt.slice(0, 10);
|
|
8969
|
+
lines.push("", `Coverage report \u2014 generated ${dateOnly} \xB7 window ${report.windowDays}d`, "");
|
|
8970
|
+
const uncovered = report.areas.filter((a) => a.status === "UNCOVERED");
|
|
8971
|
+
if (uncovered.length > 0) {
|
|
8972
|
+
lines.push(`UNCOVERED \u2014 ${uncovered.length} area${uncovered.length === 1 ? "" : "s"}, sorted by risk`);
|
|
8973
|
+
lines.push("");
|
|
8974
|
+
uncovered.forEach((a, i) => {
|
|
8975
|
+
const parts = [`${a.breakdown.recentTickets} tickets`];
|
|
8976
|
+
if (a.breakdown.recentBugs > 0)
|
|
8977
|
+
parts.push(`${a.breakdown.recentBugs} bugs`);
|
|
8978
|
+
if (a.breakdown.criticalBoost === 2)
|
|
8979
|
+
parts.push("critical \xD72");
|
|
8980
|
+
lines.push(` #${i + 1} ${pad(a.id, 10)} risk ${a.risk} ${parts.join(" \xB7 ")}`);
|
|
8981
|
+
});
|
|
8982
|
+
lines.push("");
|
|
8983
|
+
}
|
|
8984
|
+
const stale = report.areas.filter((a) => a.status === "STALE");
|
|
8985
|
+
if (stale.length > 0) {
|
|
8986
|
+
lines.push(`STALE \u2014 ${stale.length} area${stale.length === 1 ? "" : "s"}, has tests but no PASS in ${report.windowDays}d`);
|
|
8987
|
+
lines.push("");
|
|
8988
|
+
stale.forEach((a, i) => {
|
|
8989
|
+
lines.push(` #${i + 1} ${pad(a.id, 10)} (see --why ${a.id} for details)`);
|
|
8990
|
+
});
|
|
8991
|
+
lines.push("");
|
|
8992
|
+
}
|
|
8993
|
+
if (report.tickets.length > 0) {
|
|
8994
|
+
lines.push(`AC GAPS \u2014 ${report.tickets.length} ticket${report.tickets.length === 1 ? "" : "s"} with unsatisfied acceptance criteria`);
|
|
8995
|
+
lines.push("");
|
|
8996
|
+
for (const t of report.tickets) {
|
|
8997
|
+
lines.push(` ${t.id} ${t.satisfiedCount}/${t.acCount} ACs covered \xB7 gap_score ${t.gapScore}`);
|
|
8998
|
+
for (const ac of t.unsatisfiedAcs) {
|
|
8999
|
+
lines.push(` \u2717 AC-${ac.index} ${ac.text}`);
|
|
9000
|
+
}
|
|
9001
|
+
lines.push("");
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
const covered = report.areas.filter((a) => a.status === "COVERED");
|
|
9005
|
+
if (covered.length > 0) {
|
|
9006
|
+
if (options.includeCovered) {
|
|
9007
|
+
lines.push(`COVERED \u2014 ${covered.length} area${covered.length === 1 ? "" : "s"}`);
|
|
9008
|
+
lines.push("");
|
|
9009
|
+
covered.forEach((a, i) => {
|
|
9010
|
+
lines.push(` #${i + 1} ${pad(a.id, 10)} ok`);
|
|
9011
|
+
});
|
|
9012
|
+
lines.push("");
|
|
9013
|
+
} else {
|
|
9014
|
+
lines.push(`COVERED \u2014 ${covered.length} area${covered.length === 1 ? "" : "s"} (collapsed; show with --all)`);
|
|
9015
|
+
lines.push("");
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
return lines.join(`
|
|
9019
|
+
`);
|
|
9020
|
+
}
|
|
9021
|
+
// src/coverage/why.ts
|
|
9022
|
+
function daysBetween4(a, b) {
|
|
9023
|
+
return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
|
|
9024
|
+
}
|
|
9025
|
+
function pad2(s, n) {
|
|
9026
|
+
return s.length >= n ? s : `${s}${" ".repeat(n - s.length)}`;
|
|
9027
|
+
}
|
|
9028
|
+
function buildWhyArea(areaId, snap, config, now) {
|
|
9029
|
+
if (snap.areas[areaId] === undefined)
|
|
9030
|
+
return `Unknown area: ${areaId}
|
|
9031
|
+
`;
|
|
9032
|
+
const status = computeAreaStatus(areaId, snap, config.staleAfterDays, now);
|
|
9033
|
+
const isCritical = config.criticalAreas.includes(areaId);
|
|
9034
|
+
const heading = isCritical ? `${status}, critical` : status;
|
|
9035
|
+
const risk = computeAreaRisk(areaId, snap, config, now);
|
|
9036
|
+
const recentTickets = snap.edges.filter((e) => e.kind === "modifies" && e.to === areaId).map((e) => snap.tickets[e.from]).filter((t) => t !== undefined).filter((t) => daysBetween4(now, new Date(t.fetchedAt)) <= config.staleAfterDays);
|
|
9037
|
+
const pomsInArea = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
|
|
9038
|
+
const scenariosInArea = new Set(snap.edges.filter((e) => e.kind === "uses" && pomsInArea.includes(e.to)).map((e) => e.from));
|
|
9039
|
+
const recentBugs = snap.classifications.filter((c) => scenariosInArea.has(c.scenarioId)).filter((c) => RISK_WEIGHTS.bugClassifications.has(c.classification)).filter((c) => daysBetween4(now, new Date(c.ts)) <= config.staleAfterDays);
|
|
9040
|
+
const boost = isCritical ? 2 : 1;
|
|
9041
|
+
const lines = [
|
|
9042
|
+
"",
|
|
9043
|
+
`Area: ${areaId} (${heading})`,
|
|
9044
|
+
"",
|
|
9045
|
+
`Risk score: ${risk}`,
|
|
9046
|
+
" recent_tickets \xD7 critical_boost + recent_bugs",
|
|
9047
|
+
` = ${recentTickets.length} \xD7 ${boost} + ${recentBugs.length} = ${risk}`,
|
|
9048
|
+
"",
|
|
9049
|
+
`Recent tickets (${recentTickets.length}, last ${config.staleAfterDays}d):`
|
|
9050
|
+
];
|
|
9051
|
+
for (const t of recentTickets) {
|
|
9052
|
+
lines.push(` ${t.id} ${t.fetchedAt.slice(0, 10)} ${t.summary}`);
|
|
9053
|
+
}
|
|
9054
|
+
if (recentTickets.length === 0)
|
|
9055
|
+
lines.push(" (none)");
|
|
9056
|
+
lines.push("");
|
|
9057
|
+
if (recentBugs.length > 0) {
|
|
9058
|
+
lines.push(`Recent bugs (${recentBugs.length}, last ${config.staleAfterDays}d):`);
|
|
9059
|
+
for (const b of recentBugs) {
|
|
9060
|
+
lines.push(` ${b.ts.slice(0, 10)} ${pad2(b.classification, 14)} scenario ${b.scenarioId}`);
|
|
9061
|
+
}
|
|
9062
|
+
lines.push("");
|
|
9063
|
+
}
|
|
9064
|
+
if (status === "UNCOVERED") {
|
|
9065
|
+
lines.push("No POM covers this area. To draft scenarios:");
|
|
9066
|
+
lines.push(` /xera-fill-gap ${areaId}`);
|
|
9067
|
+
}
|
|
9068
|
+
return `${lines.join(`
|
|
9069
|
+
`)}
|
|
9070
|
+
`;
|
|
9071
|
+
}
|
|
9072
|
+
function buildWhyTicket(ticketId, snap, config, now) {
|
|
9073
|
+
const ticket = snap.tickets[ticketId];
|
|
9074
|
+
if (!ticket)
|
|
9075
|
+
return `Unknown ticket: ${ticketId}
|
|
9076
|
+
`;
|
|
9077
|
+
const status = computeTicketStatus(ticketId, snap, config.staleAfterDays, now);
|
|
9078
|
+
const acs = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId).sort((a, b) => a.index - b.index);
|
|
9079
|
+
const satisfiedCount = acs.filter((ac) => computeAcStatus(ac.id, snap, config.staleAfterDays, now) === "SATISFIED").length;
|
|
9080
|
+
const gapScore = computeAcGapScore(ticketId, snap, config, now);
|
|
9081
|
+
const days = daysBetween4(now, new Date(ticket.fetchedAt));
|
|
9082
|
+
let boostLabel;
|
|
9083
|
+
if (days <= RISK_WEIGHTS.recencyThresholdDays)
|
|
9084
|
+
boostLabel = "\xD72.0";
|
|
9085
|
+
else if (days <= config.staleAfterDays)
|
|
9086
|
+
boostLabel = "\xD71.0";
|
|
9087
|
+
else
|
|
9088
|
+
boostLabel = "\xD70.5";
|
|
9089
|
+
const lines = [
|
|
9090
|
+
"",
|
|
9091
|
+
`Ticket: ${ticketId} (${status}, ${satisfiedCount}/${acs.length} ACs covered)`,
|
|
9092
|
+
` Title: ${ticket.summary}`,
|
|
9093
|
+
` Fetched: ${ticket.fetchedAt.slice(0, 10)} (${Math.floor(days)}d ago, recency boost ${boostLabel})`,
|
|
9094
|
+
` AC gap score: ${gapScore}`,
|
|
9095
|
+
"",
|
|
9096
|
+
"Acceptance Criteria:"
|
|
9097
|
+
];
|
|
9098
|
+
for (const ac of acs) {
|
|
9099
|
+
const acStatus = computeAcStatus(ac.id, snap, config.staleAfterDays, now);
|
|
9100
|
+
const marker = acStatus === "SATISFIED" ? "\u2713" : "\u2717";
|
|
9101
|
+
const satisfyingScenarios = snap.edges.filter((e) => e.kind === "satisfies" && e.to === ac.id).map((e) => e.from);
|
|
9102
|
+
const scenarioRef = satisfyingScenarios.length > 0 ? ` \u2014 scenario "${satisfyingScenarios[0]}"` : "";
|
|
9103
|
+
lines.push(` ${marker} AC-${ac.index} ${ac.text}${scenarioRef}`);
|
|
9104
|
+
}
|
|
9105
|
+
lines.push("");
|
|
9106
|
+
if (status === "INCOMPLETE") {
|
|
9107
|
+
lines.push("To draft scenarios for unsatisfied ACs:");
|
|
9108
|
+
lines.push(` /xera-fill-gap --ticket ${ticketId}`);
|
|
9109
|
+
}
|
|
9110
|
+
return `${lines.join(`
|
|
9111
|
+
`)}
|
|
9112
|
+
`;
|
|
9113
|
+
}
|
|
9114
|
+
// src/bin-internal/coverage-prepare.ts
|
|
9115
|
+
init_store();
|
|
9116
|
+
init_ulid();
|
|
9117
|
+
function parseArgs3(argv) {
|
|
9118
|
+
const args = { emitEvent: true, json: false, all: false };
|
|
9119
|
+
for (let i = 0;i < argv.length; i++) {
|
|
9120
|
+
const a = argv[i];
|
|
9121
|
+
if (a === "--snapshot-ts") {
|
|
9122
|
+
const v = argv[++i];
|
|
9123
|
+
if (v !== undefined)
|
|
9124
|
+
args.snapshotTs = v;
|
|
9125
|
+
} else if (a === "--no-emit-event")
|
|
9126
|
+
args.emitEvent = false;
|
|
9127
|
+
else if (a === "--why") {
|
|
9128
|
+
const v = argv[++i];
|
|
9129
|
+
if (v !== undefined)
|
|
9130
|
+
args.why = v;
|
|
9131
|
+
} else if (a === "--json")
|
|
9132
|
+
args.json = true;
|
|
9133
|
+
else if (a === "--all")
|
|
9134
|
+
args.all = true;
|
|
9135
|
+
else if (a === "--snapshot-file") {
|
|
9136
|
+
const v = argv[++i];
|
|
9137
|
+
if (v !== undefined)
|
|
9138
|
+
args.snapshotFile = v;
|
|
9139
|
+
} else if (a === "--help-stub") {} else {
|
|
9140
|
+
console.error(`[coverage-prepare] unknown flag: ${a}`);
|
|
9141
|
+
return { ...args, emitEvent: false };
|
|
9142
|
+
}
|
|
9143
|
+
}
|
|
9144
|
+
return args;
|
|
9145
|
+
}
|
|
9146
|
+
var TICKET_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
9147
|
+
async function coveragePrepareCmd(argv) {
|
|
9148
|
+
const args = parseArgs3(argv);
|
|
9149
|
+
const cwd = process.cwd();
|
|
9150
|
+
let config;
|
|
9151
|
+
try {
|
|
9152
|
+
config = await loadConfig(cwd);
|
|
9153
|
+
} catch (e) {
|
|
9154
|
+
console.error(`[coverage-prepare] ${e.message}`);
|
|
9155
|
+
return 2;
|
|
9156
|
+
}
|
|
9157
|
+
let snap;
|
|
9158
|
+
if (args.snapshotFile) {
|
|
9159
|
+
snap = JSON.parse(readFileSync3(args.snapshotFile, "utf8"));
|
|
9160
|
+
} else {
|
|
9161
|
+
snap = deriveSnapshot(loadAllEvents(cwd));
|
|
9162
|
+
}
|
|
9163
|
+
const now = args.snapshotTs ? new Date(args.snapshotTs) : new Date;
|
|
9164
|
+
if (args.why) {
|
|
9165
|
+
const out = TICKET_RE.test(args.why) ? buildWhyTicket(args.why, snap, config.coverage, now) : buildWhyArea(args.why, snap, config.coverage, now);
|
|
9166
|
+
process.stdout.write(out);
|
|
9167
|
+
return 0;
|
|
9168
|
+
}
|
|
9169
|
+
const report = buildCoverageReport(snap, config.coverage, now);
|
|
9170
|
+
if (args.json) {
|
|
9171
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
9172
|
+
`);
|
|
9173
|
+
return 0;
|
|
9174
|
+
}
|
|
9175
|
+
const outDir = join6(cwd, ".xera/coverage");
|
|
9176
|
+
mkdirSync3(outDir, { recursive: true });
|
|
9177
|
+
writeFileSync3(join6(outDir, "report.json"), JSON.stringify(report, null, 2));
|
|
9178
|
+
const renderOpts = { includeCovered: args.all };
|
|
9179
|
+
writeFileSync3(join6(outDir, "report.md"), renderMarkdown(report, renderOpts));
|
|
9180
|
+
if (args.emitEvent && config.coverage.autoSnapshotOnCoverage) {
|
|
9181
|
+
const event = {
|
|
9182
|
+
event_id: ulid(),
|
|
9183
|
+
schema_version: 1,
|
|
9184
|
+
ts: now.toISOString(),
|
|
9185
|
+
actor: "xera-coverage",
|
|
9186
|
+
type: "coverage.snapshot",
|
|
9187
|
+
payload: {
|
|
9188
|
+
ts: now.toISOString(),
|
|
9189
|
+
windowDays: config.coverage.staleAfterDays,
|
|
9190
|
+
areas: report.areas.map((a) => ({
|
|
9191
|
+
id: a.id,
|
|
9192
|
+
status: a.status,
|
|
9193
|
+
risk: a.risk,
|
|
9194
|
+
breakdown: a.breakdown
|
|
9195
|
+
})),
|
|
9196
|
+
tickets: report.tickets.map((t) => ({
|
|
9197
|
+
id: t.id,
|
|
9198
|
+
acCount: t.acCount,
|
|
9199
|
+
satisfiedCount: t.satisfiedCount,
|
|
9200
|
+
gapScore: t.gapScore
|
|
9201
|
+
}))
|
|
9202
|
+
}
|
|
9203
|
+
};
|
|
9204
|
+
appendEvents(cwd, [event], { skill: "coverage", ticketId: "session", now });
|
|
9205
|
+
}
|
|
9206
|
+
return 0;
|
|
9207
|
+
}
|
|
9208
|
+
|
|
8559
9209
|
// src/bin-internal/disputes.ts
|
|
8560
9210
|
init_store();
|
|
8561
9211
|
function parseDuration(s) {
|
|
@@ -8632,19 +9282,19 @@ async function disputesCmd(argv) {
|
|
|
8632
9282
|
}
|
|
8633
9283
|
|
|
8634
9284
|
// src/bin-internal/doctor.ts
|
|
8635
|
-
import { existsSync as
|
|
8636
|
-
import { join as
|
|
9285
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
9286
|
+
import { join as join12 } from "path";
|
|
8637
9287
|
|
|
8638
9288
|
// src/graph/cost.ts
|
|
8639
|
-
import { appendFileSync, existsSync as
|
|
9289
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
8640
9290
|
init_paths();
|
|
8641
9291
|
function summarizeCost(repoRoot, daysBack) {
|
|
8642
9292
|
const paths = graphPaths(repoRoot);
|
|
8643
9293
|
const result = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
|
|
8644
|
-
if (!
|
|
9294
|
+
if (!existsSync5(paths.costLog))
|
|
8645
9295
|
return result;
|
|
8646
9296
|
const cutoff = Date.now() - daysBack * 86400 * 1000;
|
|
8647
|
-
for (const line of
|
|
9297
|
+
for (const line of readFileSync4(paths.costLog, "utf8").split(`
|
|
8648
9298
|
`)) {
|
|
8649
9299
|
if (!line.trim())
|
|
8650
9300
|
continue;
|
|
@@ -8671,8 +9321,8 @@ function summarizeCost(repoRoot, daysBack) {
|
|
|
8671
9321
|
init_store();
|
|
8672
9322
|
|
|
8673
9323
|
// src/bin-internal/verify-prompts.ts
|
|
8674
|
-
import { existsSync as
|
|
8675
|
-
import { join as
|
|
9324
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
9325
|
+
import { join as join7 } from "path";
|
|
8676
9326
|
var IN_SCOPE_PROMPTS = [
|
|
8677
9327
|
"feature-from-story.md",
|
|
8678
9328
|
"script-from-feature-web.md",
|
|
@@ -8680,23 +9330,25 @@ var IN_SCOPE_PROMPTS = [
|
|
|
8680
9330
|
"heal-locator.md",
|
|
8681
9331
|
"extract-areas.md",
|
|
8682
9332
|
"similarity-match.md",
|
|
8683
|
-
"classify-outdated.md"
|
|
9333
|
+
"classify-outdated.md",
|
|
9334
|
+
"map-ac-to-scenarios.md",
|
|
9335
|
+
"propose-scenarios.md"
|
|
8684
9336
|
];
|
|
8685
9337
|
var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
|
|
8686
9338
|
var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
|
|
8687
9339
|
function verifyPrompts(repoRoot) {
|
|
8688
|
-
const promptsDir =
|
|
9340
|
+
const promptsDir = join7(repoRoot, "packages/prompts");
|
|
8689
9341
|
const results = [];
|
|
8690
9342
|
for (const filename of IN_SCOPE_PROMPTS) {
|
|
8691
|
-
const path =
|
|
8692
|
-
if (!
|
|
9343
|
+
const path = join7(promptsDir, filename);
|
|
9344
|
+
if (!existsSync6(path)) {
|
|
8693
9345
|
results.push({
|
|
8694
9346
|
ok: false,
|
|
8695
9347
|
message: `${filename}: file missing at packages/prompts/${filename}`
|
|
8696
9348
|
});
|
|
8697
9349
|
continue;
|
|
8698
9350
|
}
|
|
8699
|
-
const text =
|
|
9351
|
+
const text = readFileSync5(path, "utf8");
|
|
8700
9352
|
if (!text.includes(REQUIRED_SECTION_HEADING)) {
|
|
8701
9353
|
results.push({
|
|
8702
9354
|
ok: false,
|
|
@@ -8744,8 +9396,8 @@ function frontmatterField(content, field) {
|
|
|
8744
9396
|
return m?.[1] ?? null;
|
|
8745
9397
|
}
|
|
8746
9398
|
function checkGoldenEvalDir(repoRoot) {
|
|
8747
|
-
const root =
|
|
8748
|
-
if (!
|
|
9399
|
+
const root = join12(repoRoot, "fixtures/golden-eval");
|
|
9400
|
+
if (!existsSync10(root))
|
|
8749
9401
|
return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
|
|
8750
9402
|
const dirs = readdirSync4(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
8751
9403
|
const results = [];
|
|
@@ -8756,15 +9408,15 @@ function checkGoldenEvalDir(repoRoot) {
|
|
|
8756
9408
|
});
|
|
8757
9409
|
}
|
|
8758
9410
|
for (const entry of dirs) {
|
|
8759
|
-
const dir =
|
|
8760
|
-
const metaPath =
|
|
8761
|
-
if (!
|
|
9411
|
+
const dir = join12(root, entry.name);
|
|
9412
|
+
const metaPath = join12(dir, "meta.json");
|
|
9413
|
+
if (!existsSync10(metaPath)) {
|
|
8762
9414
|
results.push({ ok: false, message: `${entry.name}: meta.json missing` });
|
|
8763
9415
|
continue;
|
|
8764
9416
|
}
|
|
8765
9417
|
let meta;
|
|
8766
9418
|
try {
|
|
8767
|
-
meta = JSON.parse(
|
|
9419
|
+
meta = JSON.parse(readFileSync8(metaPath, "utf8"));
|
|
8768
9420
|
} catch (err) {
|
|
8769
9421
|
results.push({
|
|
8770
9422
|
ok: false,
|
|
@@ -8775,12 +9427,12 @@ function checkGoldenEvalDir(repoRoot) {
|
|
|
8775
9427
|
const stages = Array.isArray(meta.stages) ? meta.stages : [];
|
|
8776
9428
|
if (stages.length === 0)
|
|
8777
9429
|
results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
|
|
8778
|
-
if (!
|
|
9430
|
+
if (!existsSync10(join12(dir, "story.md")))
|
|
8779
9431
|
results.push({ ok: false, message: `${entry.name}: story.md missing` });
|
|
8780
9432
|
for (const stage of stages) {
|
|
8781
9433
|
const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
|
|
8782
9434
|
for (const rel of required) {
|
|
8783
|
-
if (!
|
|
9435
|
+
if (!existsSync10(join12(dir, rel))) {
|
|
8784
9436
|
results.push({
|
|
8785
9437
|
ok: false,
|
|
8786
9438
|
message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
|
|
@@ -8792,10 +9444,10 @@ function checkGoldenEvalDir(repoRoot) {
|
|
|
8792
9444
|
return results;
|
|
8793
9445
|
}
|
|
8794
9446
|
function checkRubricPrompt(repoRoot) {
|
|
8795
|
-
const path =
|
|
8796
|
-
if (!
|
|
9447
|
+
const path = join12(repoRoot, "packages/prompts/eval-rubric.md");
|
|
9448
|
+
if (!existsSync10(path))
|
|
8797
9449
|
return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
|
|
8798
|
-
const text =
|
|
9450
|
+
const text = readFileSync8(path, "utf8");
|
|
8799
9451
|
const id = frontmatterField(text, "id");
|
|
8800
9452
|
const version = frontmatterField(text, "version");
|
|
8801
9453
|
if (id !== "eval-rubric")
|
|
@@ -8805,10 +9457,10 @@ function checkRubricPrompt(repoRoot) {
|
|
|
8805
9457
|
return [];
|
|
8806
9458
|
}
|
|
8807
9459
|
function checkEvalSkill(repoRoot) {
|
|
8808
|
-
const path =
|
|
8809
|
-
if (!
|
|
9460
|
+
const path = join12(repoRoot, "packages/skills/xera-eval.md");
|
|
9461
|
+
if (!existsSync10(path))
|
|
8810
9462
|
return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
|
|
8811
|
-
const text =
|
|
9463
|
+
const text = readFileSync8(path, "utf8");
|
|
8812
9464
|
if (!frontmatterField(text, "name"))
|
|
8813
9465
|
return [{ ok: false, message: 'xera-eval.md frontmatter "name" missing' }];
|
|
8814
9466
|
return [];
|
|
@@ -8817,16 +9469,16 @@ function checkPromptInjectionPreamble(repoRoot) {
|
|
|
8817
9469
|
return verifyPrompts(repoRoot);
|
|
8818
9470
|
}
|
|
8819
9471
|
function checkRootScripts(repoRoot) {
|
|
8820
|
-
const path =
|
|
8821
|
-
if (!
|
|
9472
|
+
const path = join12(repoRoot, "package.json");
|
|
9473
|
+
if (!existsSync10(path))
|
|
8822
9474
|
return [{ ok: false, message: "root package.json missing" }];
|
|
8823
|
-
const pkg = JSON.parse(
|
|
9475
|
+
const pkg = JSON.parse(readFileSync8(path, "utf8"));
|
|
8824
9476
|
const scripts = pkg.scripts ?? {};
|
|
8825
9477
|
const missing = REQUIRED_SCRIPTS.filter((s) => typeof scripts[s] !== "string");
|
|
8826
9478
|
return missing.map((s) => ({ ok: false, message: `root package.json missing script: ${s}` }));
|
|
8827
9479
|
}
|
|
8828
9480
|
function isXeraMonorepo(repoRoot) {
|
|
8829
|
-
return
|
|
9481
|
+
return existsSync10(join12(repoRoot, "packages/skills")) && existsSync10(join12(repoRoot, "packages/prompts"));
|
|
8830
9482
|
}
|
|
8831
9483
|
async function doctorCmd(argv, opts = {}) {
|
|
8832
9484
|
const repoRoot = opts.cwd ?? process.cwd();
|
|
@@ -8848,8 +9500,8 @@ async function doctorCmd(argv, opts = {}) {
|
|
|
8848
9500
|
if (top)
|
|
8849
9501
|
console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
|
|
8850
9502
|
}
|
|
8851
|
-
const xeraDir =
|
|
8852
|
-
if (
|
|
9503
|
+
const xeraDir = join12(repoRoot, ".xera");
|
|
9504
|
+
if (existsSync10(xeraDir)) {
|
|
8853
9505
|
const ticketDirs = readdirSync4(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
|
|
8854
9506
|
if (ticketDirs.length > 0) {
|
|
8855
9507
|
const events = loadAllEvents(repoRoot);
|
|
@@ -8884,115 +9536,115 @@ async function doctorCmd(argv, opts = {}) {
|
|
|
8884
9536
|
}
|
|
8885
9537
|
|
|
8886
9538
|
// src/bin-internal/eval-deterministic.ts
|
|
8887
|
-
import { existsSync as
|
|
8888
|
-
import { join as
|
|
9539
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
9540
|
+
import { join as join14 } from "path";
|
|
8889
9541
|
import { validateGherkin } from "@xera-ai/web";
|
|
8890
9542
|
|
|
8891
9543
|
// src/eval/paths.ts
|
|
8892
|
-
import { join as
|
|
9544
|
+
import { join as join13 } from "path";
|
|
8893
9545
|
function resolveEvalPaths(cwd, runId) {
|
|
8894
|
-
const root =
|
|
9546
|
+
const root = join13(cwd, ".xera", "eval", runId);
|
|
8895
9547
|
return {
|
|
8896
9548
|
root,
|
|
8897
|
-
manifest:
|
|
8898
|
-
lock:
|
|
8899
|
-
deterministicScores:
|
|
8900
|
-
judgeScores:
|
|
8901
|
-
report:
|
|
8902
|
-
summary:
|
|
8903
|
-
inputsDir:
|
|
8904
|
-
actualDir:
|
|
8905
|
-
ticketInputsDir: (ticket) =>
|
|
8906
|
-
ticketActualDir: (ticket) =>
|
|
9549
|
+
manifest: join13(root, "manifest.json"),
|
|
9550
|
+
lock: join13(root, ".lock"),
|
|
9551
|
+
deterministicScores: join13(root, "deterministic-scores.json"),
|
|
9552
|
+
judgeScores: join13(root, "judge-scores.json"),
|
|
9553
|
+
report: join13(root, "report.md"),
|
|
9554
|
+
summary: join13(root, "summary.json"),
|
|
9555
|
+
inputsDir: join13(root, "inputs"),
|
|
9556
|
+
actualDir: join13(root, "actual"),
|
|
9557
|
+
ticketInputsDir: (ticket) => join13(root, "inputs", ticket),
|
|
9558
|
+
ticketActualDir: (ticket) => join13(root, "actual", ticket)
|
|
8907
9559
|
};
|
|
8908
9560
|
}
|
|
8909
9561
|
|
|
8910
9562
|
// src/eval/types.ts
|
|
8911
|
-
import { z as
|
|
9563
|
+
import { z as z4 } from "zod";
|
|
8912
9564
|
var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
|
|
8913
|
-
var StageSchema =
|
|
8914
|
-
var VerdictSchema =
|
|
8915
|
-
var PromptVersionsSchema =
|
|
8916
|
-
"feature-from-story":
|
|
8917
|
-
"script-from-feature":
|
|
8918
|
-
"diagnose-failure":
|
|
8919
|
-
"eval-rubric":
|
|
9565
|
+
var StageSchema = z4.enum(STAGES);
|
|
9566
|
+
var VerdictSchema = z4.enum(["PASS", "FAIL", "NA"]);
|
|
9567
|
+
var PromptVersionsSchema = z4.object({
|
|
9568
|
+
"feature-from-story": z4.string(),
|
|
9569
|
+
"script-from-feature": z4.string(),
|
|
9570
|
+
"diagnose-failure": z4.string(),
|
|
9571
|
+
"eval-rubric": z4.string()
|
|
8920
9572
|
});
|
|
8921
|
-
var ManifestSchema =
|
|
8922
|
-
run_id:
|
|
8923
|
-
started_at:
|
|
8924
|
-
git_sha:
|
|
8925
|
-
tickets:
|
|
8926
|
-
stages:
|
|
8927
|
-
ticket_stages:
|
|
9573
|
+
var ManifestSchema = z4.object({
|
|
9574
|
+
run_id: z4.string(),
|
|
9575
|
+
started_at: z4.string(),
|
|
9576
|
+
git_sha: z4.string(),
|
|
9577
|
+
tickets: z4.array(z4.string()).min(1),
|
|
9578
|
+
stages: z4.array(StageSchema).min(1),
|
|
9579
|
+
ticket_stages: z4.record(z4.string(), z4.array(StageSchema).min(1)),
|
|
8928
9580
|
prompt_versions: PromptVersionsSchema,
|
|
8929
|
-
flags:
|
|
8930
|
-
force:
|
|
9581
|
+
flags: z4.object({
|
|
9582
|
+
force: z4.boolean(),
|
|
8931
9583
|
only_prompt: StageSchema.nullable(),
|
|
8932
|
-
only_ticket:
|
|
8933
|
-
judge_only:
|
|
9584
|
+
only_ticket: z4.string().nullable(),
|
|
9585
|
+
judge_only: z4.boolean()
|
|
8934
9586
|
})
|
|
8935
9587
|
});
|
|
8936
|
-
var DimensionSchema =
|
|
8937
|
-
name:
|
|
9588
|
+
var DimensionSchema = z4.object({
|
|
9589
|
+
name: z4.string(),
|
|
8938
9590
|
verdict: VerdictSchema,
|
|
8939
|
-
notes:
|
|
9591
|
+
notes: z4.string()
|
|
8940
9592
|
});
|
|
8941
|
-
var JudgmentSchema =
|
|
9593
|
+
var JudgmentSchema = z4.object({
|
|
8942
9594
|
stage: StageSchema,
|
|
8943
|
-
ticket:
|
|
8944
|
-
dimensions:
|
|
9595
|
+
ticket: z4.string(),
|
|
9596
|
+
dimensions: z4.array(DimensionSchema).min(1)
|
|
8945
9597
|
});
|
|
8946
|
-
var JudgeScoresSchema =
|
|
8947
|
-
run_id:
|
|
8948
|
-
judgments:
|
|
9598
|
+
var JudgeScoresSchema = z4.object({
|
|
9599
|
+
run_id: z4.string(),
|
|
9600
|
+
judgments: z4.array(JudgmentSchema)
|
|
8949
9601
|
});
|
|
8950
|
-
var DeterministicEntrySchema =
|
|
8951
|
-
ticket:
|
|
9602
|
+
var DeterministicEntrySchema = z4.object({
|
|
9603
|
+
ticket: z4.string(),
|
|
8952
9604
|
stage: StageSchema,
|
|
8953
|
-
passed:
|
|
8954
|
-
checks:
|
|
8955
|
-
error:
|
|
9605
|
+
passed: z4.boolean(),
|
|
9606
|
+
checks: z4.array(z4.string()),
|
|
9607
|
+
error: z4.string().optional()
|
|
8956
9608
|
});
|
|
8957
|
-
var DeterministicScoresSchema =
|
|
8958
|
-
run_id:
|
|
8959
|
-
entries:
|
|
9609
|
+
var DeterministicScoresSchema = z4.object({
|
|
9610
|
+
run_id: z4.string(),
|
|
9611
|
+
entries: z4.array(DeterministicEntrySchema)
|
|
8960
9612
|
});
|
|
8961
|
-
var ResultSchema =
|
|
8962
|
-
ticket:
|
|
9613
|
+
var ResultSchema = z4.object({
|
|
9614
|
+
ticket: z4.string(),
|
|
8963
9615
|
stage: StageSchema,
|
|
8964
|
-
deterministic:
|
|
8965
|
-
passed:
|
|
8966
|
-
checks:
|
|
8967
|
-
error:
|
|
9616
|
+
deterministic: z4.object({
|
|
9617
|
+
passed: z4.boolean(),
|
|
9618
|
+
checks: z4.array(z4.string()),
|
|
9619
|
+
error: z4.string().optional()
|
|
8968
9620
|
}),
|
|
8969
|
-
judge:
|
|
8970
|
-
passed:
|
|
8971
|
-
dimensions:
|
|
8972
|
-
score:
|
|
9621
|
+
judge: z4.object({
|
|
9622
|
+
passed: z4.boolean(),
|
|
9623
|
+
dimensions: z4.array(DimensionSchema),
|
|
9624
|
+
score: z4.number().min(0).max(1)
|
|
8973
9625
|
}).nullable(),
|
|
8974
|
-
skipped:
|
|
9626
|
+
skipped: z4.boolean().optional()
|
|
8975
9627
|
});
|
|
8976
|
-
var SummarySchema =
|
|
8977
|
-
run_id:
|
|
8978
|
-
git_sha:
|
|
9628
|
+
var SummarySchema = z4.object({
|
|
9629
|
+
run_id: z4.string(),
|
|
9630
|
+
git_sha: z4.string(),
|
|
8979
9631
|
prompt_versions: PromptVersionsSchema,
|
|
8980
|
-
results:
|
|
8981
|
-
overall:
|
|
8982
|
-
passed:
|
|
8983
|
-
failed:
|
|
8984
|
-
total:
|
|
8985
|
-
score:
|
|
9632
|
+
results: z4.array(ResultSchema),
|
|
9633
|
+
overall: z4.object({
|
|
9634
|
+
passed: z4.number().int().nonnegative(),
|
|
9635
|
+
failed: z4.number().int().nonnegative(),
|
|
9636
|
+
total: z4.number().int().nonnegative(),
|
|
9637
|
+
score: z4.number().min(0).max(1)
|
|
8986
9638
|
})
|
|
8987
9639
|
});
|
|
8988
9640
|
|
|
8989
9641
|
// src/bin-internal/eval-deterministic.ts
|
|
8990
9642
|
function checkFeatureFromStory(actualFeaturePath) {
|
|
8991
|
-
if (!
|
|
9643
|
+
if (!existsSync11(actualFeaturePath)) {
|
|
8992
9644
|
return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
|
|
8993
9645
|
}
|
|
8994
9646
|
try {
|
|
8995
|
-
const r = validateGherkin(
|
|
9647
|
+
const r = validateGherkin(readFileSync9(actualFeaturePath, "utf8"));
|
|
8996
9648
|
if (r.ok)
|
|
8997
9649
|
return { passed: true, checks: ["validate-feature"] };
|
|
8998
9650
|
return {
|
|
@@ -9005,31 +9657,31 @@ function checkFeatureFromStory(actualFeaturePath) {
|
|
|
9005
9657
|
}
|
|
9006
9658
|
}
|
|
9007
9659
|
function checkScriptFromFeature(actualTicketDir) {
|
|
9008
|
-
const specPath =
|
|
9009
|
-
if (!
|
|
9660
|
+
const specPath = join14(actualTicketDir, "spec.ts");
|
|
9661
|
+
if (!existsSync11(specPath)) {
|
|
9010
9662
|
return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
|
|
9011
9663
|
}
|
|
9012
9664
|
return { passed: true, checks: ["file-presence"] };
|
|
9013
9665
|
}
|
|
9014
9666
|
function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
|
|
9015
|
-
const inputPath =
|
|
9016
|
-
const actualPath =
|
|
9017
|
-
if (!
|
|
9667
|
+
const inputPath = join14(inputsTicketDir, "classifier-input.json");
|
|
9668
|
+
const actualPath = join14(actualTicketDir, "classification.json");
|
|
9669
|
+
if (!existsSync11(actualPath)) {
|
|
9018
9670
|
return {
|
|
9019
9671
|
passed: false,
|
|
9020
9672
|
checks: ["bucket-match"],
|
|
9021
9673
|
error: "actual missing: classification.json"
|
|
9022
9674
|
};
|
|
9023
9675
|
}
|
|
9024
|
-
if (!
|
|
9676
|
+
if (!existsSync11(inputPath)) {
|
|
9025
9677
|
return {
|
|
9026
9678
|
passed: false,
|
|
9027
9679
|
checks: ["bucket-match"],
|
|
9028
9680
|
error: "inputs missing: classifier-input.json"
|
|
9029
9681
|
};
|
|
9030
9682
|
}
|
|
9031
|
-
const golden = JSON.parse(
|
|
9032
|
-
const actual = JSON.parse(
|
|
9683
|
+
const golden = JSON.parse(readFileSync9(inputPath, "utf8"));
|
|
9684
|
+
const actual = JSON.parse(readFileSync9(actualPath, "utf8"));
|
|
9033
9685
|
const goldScens = golden.scenarios ?? [];
|
|
9034
9686
|
const actScens = actual.scenarios ?? [];
|
|
9035
9687
|
const mismatches = [];
|
|
@@ -9059,11 +9711,11 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
9059
9711
|
return 1;
|
|
9060
9712
|
}
|
|
9061
9713
|
const paths = resolveEvalPaths(cwd, runId);
|
|
9062
|
-
if (!
|
|
9714
|
+
if (!existsSync11(paths.manifest)) {
|
|
9063
9715
|
console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
|
|
9064
9716
|
return 1;
|
|
9065
9717
|
}
|
|
9066
|
-
const manifest = ManifestSchema.parse(JSON.parse(
|
|
9718
|
+
const manifest = ManifestSchema.parse(JSON.parse(readFileSync9(paths.manifest, "utf8")));
|
|
9067
9719
|
const entries = [];
|
|
9068
9720
|
for (const [ticket, ticketStages] of Object.entries(manifest.ticket_stages)) {
|
|
9069
9721
|
for (const stage of ticketStages) {
|
|
@@ -9071,7 +9723,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
9071
9723
|
const actualDir = paths.ticketActualDir(ticket);
|
|
9072
9724
|
let result;
|
|
9073
9725
|
if (stage === "feature-from-story") {
|
|
9074
|
-
result = checkFeatureFromStory(
|
|
9726
|
+
result = checkFeatureFromStory(join14(actualDir, "test.feature"));
|
|
9075
9727
|
} else if (stage === "script-from-feature") {
|
|
9076
9728
|
result = checkScriptFromFeature(actualDir);
|
|
9077
9729
|
} else {
|
|
@@ -9090,7 +9742,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
9090
9742
|
}
|
|
9091
9743
|
const scores = { run_id: runId, entries };
|
|
9092
9744
|
DeterministicScoresSchema.parse(scores);
|
|
9093
|
-
|
|
9745
|
+
writeFileSync4(paths.deterministicScores, JSON.stringify(scores, null, 2));
|
|
9094
9746
|
console.log(`[xera:eval-deterministic] wrote ${entries.length} entries`);
|
|
9095
9747
|
return 0;
|
|
9096
9748
|
}
|
|
@@ -9098,13 +9750,13 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
9098
9750
|
// src/bin-internal/eval-prepare.ts
|
|
9099
9751
|
import {
|
|
9100
9752
|
copyFileSync,
|
|
9101
|
-
existsSync as
|
|
9102
|
-
mkdirSync as
|
|
9753
|
+
existsSync as existsSync13,
|
|
9754
|
+
mkdirSync as mkdirSync6,
|
|
9103
9755
|
readdirSync as readdirSync5,
|
|
9104
|
-
readFileSync as
|
|
9105
|
-
writeFileSync as
|
|
9756
|
+
readFileSync as readFileSync11,
|
|
9757
|
+
writeFileSync as writeFileSync6
|
|
9106
9758
|
} from "fs";
|
|
9107
|
-
import { join as
|
|
9759
|
+
import { join as join15 } from "path";
|
|
9108
9760
|
|
|
9109
9761
|
// src/eval/run-id.ts
|
|
9110
9762
|
import { execSync } from "child_process";
|
|
@@ -9115,27 +9767,27 @@ function defaultGetGitSha() {
|
|
|
9115
9767
|
return null;
|
|
9116
9768
|
}
|
|
9117
9769
|
}
|
|
9118
|
-
function
|
|
9770
|
+
function pad3(n) {
|
|
9119
9771
|
return n.toString().padStart(2, "0");
|
|
9120
9772
|
}
|
|
9121
9773
|
function generateRunId2(opts = {}) {
|
|
9122
9774
|
const getGitSha = opts.getGitSha ?? defaultGetGitSha;
|
|
9123
9775
|
const now = (opts.now ?? (() => new Date))();
|
|
9124
|
-
const date = `${now.getUTCFullYear()}${
|
|
9125
|
-
const time = `${
|
|
9776
|
+
const date = `${now.getUTCFullYear()}${pad3(now.getUTCMonth() + 1)}${pad3(now.getUTCDate())}`;
|
|
9777
|
+
const time = `${pad3(now.getUTCHours())}${pad3(now.getUTCMinutes())}${pad3(now.getUTCSeconds())}`;
|
|
9126
9778
|
const sha = getGitSha();
|
|
9127
9779
|
const short = sha ? sha.slice(0, 7) : "nogit";
|
|
9128
9780
|
return `${date}-${time}-${short}`;
|
|
9129
9781
|
}
|
|
9130
9782
|
|
|
9131
9783
|
// src/lock/file-lock.ts
|
|
9132
|
-
import { existsSync as
|
|
9784
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
9133
9785
|
import { hostname } from "os";
|
|
9134
9786
|
import { dirname as dirname2 } from "path";
|
|
9135
9787
|
function acquireLock(path, runId) {
|
|
9136
|
-
if (
|
|
9788
|
+
if (existsSync12(path))
|
|
9137
9789
|
return false;
|
|
9138
|
-
|
|
9790
|
+
mkdirSync5(dirname2(path), { recursive: true });
|
|
9139
9791
|
const data = {
|
|
9140
9792
|
pid: process.pid,
|
|
9141
9793
|
hostname: hostname(),
|
|
@@ -9143,20 +9795,20 @@ function acquireLock(path, runId) {
|
|
|
9143
9795
|
run_id: runId
|
|
9144
9796
|
};
|
|
9145
9797
|
try {
|
|
9146
|
-
|
|
9798
|
+
writeFileSync5(path, JSON.stringify(data), { flag: "wx" });
|
|
9147
9799
|
return true;
|
|
9148
9800
|
} catch {
|
|
9149
9801
|
return false;
|
|
9150
9802
|
}
|
|
9151
9803
|
}
|
|
9152
9804
|
function releaseLock(path) {
|
|
9153
|
-
if (
|
|
9805
|
+
if (existsSync12(path))
|
|
9154
9806
|
unlinkSync(path);
|
|
9155
9807
|
}
|
|
9156
9808
|
function readLock(path) {
|
|
9157
|
-
if (!
|
|
9809
|
+
if (!existsSync12(path))
|
|
9158
9810
|
return null;
|
|
9159
|
-
return JSON.parse(
|
|
9811
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
9160
9812
|
}
|
|
9161
9813
|
function isLockStale(path) {
|
|
9162
9814
|
const lock = readLock(path);
|
|
@@ -9197,16 +9849,16 @@ function parseFlags2(argv) {
|
|
|
9197
9849
|
return flags;
|
|
9198
9850
|
}
|
|
9199
9851
|
function readPromptVersion(repoRoot, name) {
|
|
9200
|
-
const path =
|
|
9201
|
-
if (!
|
|
9852
|
+
const path = join15(repoRoot, "packages/prompts", `${name}.md`);
|
|
9853
|
+
if (!existsSync13(path))
|
|
9202
9854
|
return "0.0.0";
|
|
9203
|
-
const text =
|
|
9855
|
+
const text = readFileSync11(path, "utf8");
|
|
9204
9856
|
const m = /^version:\s*(\S+)\s*$/m.exec(text);
|
|
9205
9857
|
return m?.[1] ?? "0.0.0";
|
|
9206
9858
|
}
|
|
9207
9859
|
function discoverEvalTickets(repoRoot) {
|
|
9208
|
-
const root =
|
|
9209
|
-
if (!
|
|
9860
|
+
const root = join15(repoRoot, "fixtures/golden-eval");
|
|
9861
|
+
if (!existsSync13(root))
|
|
9210
9862
|
return [];
|
|
9211
9863
|
const out = [];
|
|
9212
9864
|
for (const entry of readdirSync5(root, { withFileTypes: true })) {
|
|
@@ -9214,25 +9866,25 @@ function discoverEvalTickets(repoRoot) {
|
|
|
9214
9866
|
continue;
|
|
9215
9867
|
if (entry.name === "README.md" || entry.name.startsWith("."))
|
|
9216
9868
|
continue;
|
|
9217
|
-
const dir =
|
|
9218
|
-
const metaPath =
|
|
9219
|
-
if (!
|
|
9869
|
+
const dir = join15(root, entry.name);
|
|
9870
|
+
const metaPath = join15(dir, "meta.json");
|
|
9871
|
+
if (!existsSync13(metaPath))
|
|
9220
9872
|
continue;
|
|
9221
|
-
const meta = JSON.parse(
|
|
9873
|
+
const meta = JSON.parse(readFileSync11(metaPath, "utf8"));
|
|
9222
9874
|
out.push({ id: meta.id, dir, stages: meta.stages });
|
|
9223
9875
|
}
|
|
9224
9876
|
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
9225
9877
|
}
|
|
9226
9878
|
function discoverClassifierTickets(repoRoot) {
|
|
9227
|
-
const root =
|
|
9228
|
-
if (!
|
|
9879
|
+
const root = join15(repoRoot, "fixtures/golden-tickets");
|
|
9880
|
+
if (!existsSync13(root))
|
|
9229
9881
|
return [];
|
|
9230
9882
|
const out = [];
|
|
9231
9883
|
for (const entry of readdirSync5(root, { withFileTypes: true })) {
|
|
9232
9884
|
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
9233
9885
|
continue;
|
|
9234
|
-
const path =
|
|
9235
|
-
const data = JSON.parse(
|
|
9886
|
+
const path = join15(root, entry.name);
|
|
9887
|
+
const data = JSON.parse(readFileSync11(path, "utf8"));
|
|
9236
9888
|
if (typeof data.ticket === "string")
|
|
9237
9889
|
out.push({ id: data.ticket, path });
|
|
9238
9890
|
}
|
|
@@ -9291,25 +9943,25 @@ async function evalPrepareCmd(argv, opts = {}) {
|
|
|
9291
9943
|
...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
|
|
9292
9944
|
});
|
|
9293
9945
|
const paths = resolveEvalPaths(repoRoot, runId);
|
|
9294
|
-
if (
|
|
9946
|
+
if (existsSync13(paths.root) && !flags.force) {
|
|
9295
9947
|
console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
|
|
9296
9948
|
return 1;
|
|
9297
9949
|
}
|
|
9298
|
-
|
|
9299
|
-
|
|
9950
|
+
mkdirSync6(paths.inputsDir, { recursive: true });
|
|
9951
|
+
mkdirSync6(paths.actualDir, { recursive: true });
|
|
9300
9952
|
for (const ticket of selectedTickets) {
|
|
9301
9953
|
const ticketInputs = paths.ticketInputsDir(ticket);
|
|
9302
|
-
|
|
9954
|
+
mkdirSync6(ticketInputs, { recursive: true });
|
|
9303
9955
|
const evalT = evalTickets.find((t) => t.id === ticket);
|
|
9304
9956
|
const classT = classifierTickets.find((t) => t.id === ticket);
|
|
9305
9957
|
if (evalT) {
|
|
9306
|
-
copyFileSync(
|
|
9307
|
-
const featurePath =
|
|
9308
|
-
if (
|
|
9309
|
-
copyFileSync(featurePath,
|
|
9958
|
+
copyFileSync(join15(evalT.dir, "story.md"), join15(ticketInputs, "story.md"));
|
|
9959
|
+
const featurePath = join15(evalT.dir, "golden/test.feature");
|
|
9960
|
+
if (existsSync13(featurePath))
|
|
9961
|
+
copyFileSync(featurePath, join15(ticketInputs, "test.feature"));
|
|
9310
9962
|
}
|
|
9311
9963
|
if (classT) {
|
|
9312
|
-
copyFileSync(classT.path,
|
|
9964
|
+
copyFileSync(classT.path, join15(ticketInputs, "classifier-input.json"));
|
|
9313
9965
|
}
|
|
9314
9966
|
}
|
|
9315
9967
|
const now = (opts.now ?? (() => new Date))();
|
|
@@ -9334,7 +9986,7 @@ async function evalPrepareCmd(argv, opts = {}) {
|
|
|
9334
9986
|
}
|
|
9335
9987
|
};
|
|
9336
9988
|
ManifestSchema.parse(manifest);
|
|
9337
|
-
|
|
9989
|
+
writeFileSync6(paths.manifest, JSON.stringify(manifest, null, 2));
|
|
9338
9990
|
if (!acquireLock(paths.lock, runId)) {
|
|
9339
9991
|
console.error(`[xera:eval-prepare] failed to acquire lock at ${paths.lock}`);
|
|
9340
9992
|
return 4;
|
|
@@ -9345,7 +9997,7 @@ async function evalPrepareCmd(argv, opts = {}) {
|
|
|
9345
9997
|
}
|
|
9346
9998
|
|
|
9347
9999
|
// src/bin-internal/eval-report.ts
|
|
9348
|
-
import { existsSync as
|
|
10000
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
9349
10001
|
function scoreJudgment(j) {
|
|
9350
10002
|
const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
|
|
9351
10003
|
if (nonNa.length === 0)
|
|
@@ -9400,22 +10052,22 @@ async function evalReportCmd(argv, opts = {}) {
|
|
|
9400
10052
|
return 1;
|
|
9401
10053
|
}
|
|
9402
10054
|
const paths = resolveEvalPaths(cwd, runId);
|
|
9403
|
-
if (!
|
|
10055
|
+
if (!existsSync14(paths.manifest)) {
|
|
9404
10056
|
console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
|
|
9405
10057
|
return 1;
|
|
9406
10058
|
}
|
|
9407
|
-
const manifest = ManifestSchema.parse(JSON.parse(
|
|
10059
|
+
const manifest = ManifestSchema.parse(JSON.parse(readFileSync12(paths.manifest, "utf8")));
|
|
9408
10060
|
try {
|
|
9409
10061
|
let det;
|
|
9410
10062
|
let judge;
|
|
9411
10063
|
try {
|
|
9412
|
-
det = DeterministicScoresSchema.parse(JSON.parse(
|
|
10064
|
+
det = DeterministicScoresSchema.parse(JSON.parse(readFileSync12(paths.deterministicScores, "utf8")));
|
|
9413
10065
|
} catch (err) {
|
|
9414
10066
|
console.error(`[xera:eval-report] invalid deterministic-scores.json: ${err.message}`);
|
|
9415
10067
|
return 2;
|
|
9416
10068
|
}
|
|
9417
10069
|
try {
|
|
9418
|
-
judge = JudgeScoresSchema.parse(JSON.parse(
|
|
10070
|
+
judge = JudgeScoresSchema.parse(JSON.parse(readFileSync12(paths.judgeScores, "utf8")));
|
|
9419
10071
|
} catch (err) {
|
|
9420
10072
|
console.error(`[xera:eval-report] invalid judge-scores.json: ${err.message}`);
|
|
9421
10073
|
return 2;
|
|
@@ -9477,8 +10129,8 @@ async function evalReportCmd(argv, opts = {}) {
|
|
|
9477
10129
|
overall: { passed: passedCount, failed: failedCount, total: counted.length, score: avgScore }
|
|
9478
10130
|
};
|
|
9479
10131
|
SummarySchema.parse(summary);
|
|
9480
|
-
|
|
9481
|
-
|
|
10132
|
+
writeFileSync7(paths.summary, JSON.stringify(summary, null, 2));
|
|
10133
|
+
writeFileSync7(paths.report, renderReport(summary));
|
|
9482
10134
|
console.log(`[xera:eval-report] ${passedCount}/${counted.length} PASS (avg ${(avgScore * 100).toFixed(0)}%)`);
|
|
9483
10135
|
return 0;
|
|
9484
10136
|
} finally {
|
|
@@ -9487,37 +10139,37 @@ async function evalReportCmd(argv, opts = {}) {
|
|
|
9487
10139
|
}
|
|
9488
10140
|
|
|
9489
10141
|
// src/bin-internal/exec.ts
|
|
9490
|
-
import { existsSync as
|
|
9491
|
-
import { join as
|
|
10142
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "fs";
|
|
10143
|
+
import { join as join17 } from "path";
|
|
9492
10144
|
import { chromium } from "@playwright/test";
|
|
9493
10145
|
import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
|
|
9494
10146
|
|
|
9495
10147
|
// src/artifact/meta.ts
|
|
9496
|
-
import { existsSync as
|
|
10148
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
9497
10149
|
import { dirname as dirname3 } from "path";
|
|
9498
|
-
import { z as
|
|
9499
|
-
var MetaJsonSchema =
|
|
9500
|
-
ticket:
|
|
9501
|
-
adapter:
|
|
9502
|
-
xera_version:
|
|
9503
|
-
prompts_version:
|
|
9504
|
-
fetched_at:
|
|
9505
|
-
story_hash:
|
|
9506
|
-
feature_generated_at:
|
|
9507
|
-
feature_generated_from_story_hash:
|
|
9508
|
-
feature_hash:
|
|
9509
|
-
script_generated_at:
|
|
9510
|
-
script_generated_from_feature_hash:
|
|
9511
|
-
script_warnings:
|
|
10150
|
+
import { z as z5 } from "zod";
|
|
10151
|
+
var MetaJsonSchema = z5.object({
|
|
10152
|
+
ticket: z5.string(),
|
|
10153
|
+
adapter: z5.string(),
|
|
10154
|
+
xera_version: z5.string(),
|
|
10155
|
+
prompts_version: z5.string(),
|
|
10156
|
+
fetched_at: z5.string().optional(),
|
|
10157
|
+
story_hash: z5.string().optional(),
|
|
10158
|
+
feature_generated_at: z5.string().optional(),
|
|
10159
|
+
feature_generated_from_story_hash: z5.string().optional(),
|
|
10160
|
+
feature_hash: z5.string().optional(),
|
|
10161
|
+
script_generated_at: z5.string().optional(),
|
|
10162
|
+
script_generated_from_feature_hash: z5.string().optional(),
|
|
10163
|
+
script_warnings: z5.array(z5.string()).optional()
|
|
9512
10164
|
});
|
|
9513
10165
|
function readMeta(path) {
|
|
9514
|
-
if (!
|
|
10166
|
+
if (!existsSync15(path))
|
|
9515
10167
|
return null;
|
|
9516
|
-
return MetaJsonSchema.parse(JSON.parse(
|
|
10168
|
+
return MetaJsonSchema.parse(JSON.parse(readFileSync13(path, "utf8")));
|
|
9517
10169
|
}
|
|
9518
10170
|
function writeMeta(path, meta) {
|
|
9519
|
-
|
|
9520
|
-
|
|
10171
|
+
mkdirSync7(dirname3(path), { recursive: true });
|
|
10172
|
+
writeFileSync8(path, JSON.stringify(meta, null, 2));
|
|
9521
10173
|
}
|
|
9522
10174
|
function updateMeta(path, patch) {
|
|
9523
10175
|
const existing = readMeta(path);
|
|
@@ -9561,9 +10213,9 @@ function needsRefresh(entry, policy, now = new Date) {
|
|
|
9561
10213
|
}
|
|
9562
10214
|
|
|
9563
10215
|
// src/auth/state.ts
|
|
9564
|
-
import { existsSync as
|
|
9565
|
-
import { join as
|
|
9566
|
-
import { z as
|
|
10216
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
10217
|
+
import { join as join16 } from "path";
|
|
10218
|
+
import { z as z6 } from "zod";
|
|
9567
10219
|
|
|
9568
10220
|
// src/auth/encrypt.ts
|
|
9569
10221
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
@@ -9620,39 +10272,39 @@ function resolveAuthKey() {
|
|
|
9620
10272
|
}
|
|
9621
10273
|
|
|
9622
10274
|
// src/auth/state.ts
|
|
9623
|
-
var AuthStateEntrySchema =
|
|
9624
|
-
role:
|
|
9625
|
-
strategy:
|
|
9626
|
-
created_at:
|
|
9627
|
-
expires_at:
|
|
9628
|
-
payload:
|
|
10275
|
+
var AuthStateEntrySchema = z6.object({
|
|
10276
|
+
role: z6.string(),
|
|
10277
|
+
strategy: z6.enum(["storageState", "apiToken"]),
|
|
10278
|
+
created_at: z6.string(),
|
|
10279
|
+
expires_at: z6.string(),
|
|
10280
|
+
payload: z6.record(z6.string(), z6.unknown())
|
|
9629
10281
|
});
|
|
9630
10282
|
function pathFor(authDir, role) {
|
|
9631
|
-
return
|
|
10283
|
+
return join16(authDir, `${role}.json`);
|
|
9632
10284
|
}
|
|
9633
10285
|
function writeAuthState(authDir, entry) {
|
|
9634
|
-
|
|
10286
|
+
mkdirSync8(authDir, { recursive: true });
|
|
9635
10287
|
const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
|
|
9636
|
-
|
|
10288
|
+
writeFileSync9(pathFor(authDir, entry.role), ct);
|
|
9637
10289
|
}
|
|
9638
10290
|
function readAuthState(authDir, role) {
|
|
9639
10291
|
const p = pathFor(authDir, role);
|
|
9640
|
-
if (!
|
|
10292
|
+
if (!existsSync16(p))
|
|
9641
10293
|
return null;
|
|
9642
|
-
const txt =
|
|
10294
|
+
const txt = readFileSync14(p, "utf8");
|
|
9643
10295
|
const plain = decrypt(txt, resolveAuthKey());
|
|
9644
10296
|
return AuthStateEntrySchema.parse(JSON.parse(plain));
|
|
9645
10297
|
}
|
|
9646
10298
|
|
|
9647
10299
|
// src/logging/ndjson-logger.ts
|
|
9648
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
10300
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync15 } from "fs";
|
|
9649
10301
|
import { dirname as dirname4 } from "path";
|
|
9650
10302
|
|
|
9651
10303
|
class NdjsonLogger {
|
|
9652
10304
|
path;
|
|
9653
10305
|
constructor(path) {
|
|
9654
10306
|
this.path = path;
|
|
9655
|
-
|
|
10307
|
+
mkdirSync9(dirname4(path), { recursive: true });
|
|
9656
10308
|
}
|
|
9657
10309
|
log(payload) {
|
|
9658
10310
|
const entry = { ts: new Date().toISOString(), ...payload };
|
|
@@ -9660,9 +10312,9 @@ class NdjsonLogger {
|
|
|
9660
10312
|
`);
|
|
9661
10313
|
}
|
|
9662
10314
|
static readAll(path) {
|
|
9663
|
-
if (!
|
|
10315
|
+
if (!existsSync17(path))
|
|
9664
10316
|
return [];
|
|
9665
|
-
const txt =
|
|
10317
|
+
const txt = readFileSync15(path, "utf8").trim();
|
|
9666
10318
|
if (!txt)
|
|
9667
10319
|
return [];
|
|
9668
10320
|
return txt.split(`
|
|
@@ -9742,7 +10394,7 @@ async function execCmd(argv) {
|
|
|
9742
10394
|
await runAuthSetup({
|
|
9743
10395
|
role: roleName,
|
|
9744
10396
|
creds: { email, password },
|
|
9745
|
-
setupScriptPath:
|
|
10397
|
+
setupScriptPath: join17(cwd, webConfig.auth.setupScript),
|
|
9746
10398
|
authDir: paths.authDir,
|
|
9747
10399
|
browser
|
|
9748
10400
|
});
|
|
@@ -9760,16 +10412,16 @@ async function execCmd(argv) {
|
|
|
9760
10412
|
}
|
|
9761
10413
|
}
|
|
9762
10414
|
}
|
|
9763
|
-
const cfgPath =
|
|
9764
|
-
if (!
|
|
10415
|
+
const cfgPath = join17(cwd, "playwright.config.ts");
|
|
10416
|
+
if (!existsSync18(cfgPath)) {
|
|
9765
10417
|
console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
|
|
9766
10418
|
return 1;
|
|
9767
10419
|
}
|
|
9768
10420
|
const runDir = paths.runPath(runId).runDir;
|
|
9769
|
-
|
|
10421
|
+
mkdirSync10(runDir, { recursive: true });
|
|
9770
10422
|
const envName = process.env.XERA_ENV ?? webConfig.defaultEnv;
|
|
9771
10423
|
const baseURL = webConfig.baseUrl[envName] ?? webConfig.baseUrl[webConfig.defaultEnv];
|
|
9772
|
-
const reportJsonPath =
|
|
10424
|
+
const reportJsonPath = join17(runDir, "report.json");
|
|
9773
10425
|
log.log({ step: "exec.start", runId, env: envName, baseURL });
|
|
9774
10426
|
const r = await runPlaywright({
|
|
9775
10427
|
specPath: paths.specPath,
|
|
@@ -9791,20 +10443,20 @@ async function execCmd(argv) {
|
|
|
9791
10443
|
}
|
|
9792
10444
|
|
|
9793
10445
|
// src/bin-internal/fetch.ts
|
|
9794
|
-
import { mkdirSync as
|
|
10446
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
9795
10447
|
import { dirname as dirname5 } from "path";
|
|
9796
10448
|
|
|
9797
10449
|
// src/artifact/hash.ts
|
|
9798
10450
|
import { createHash as createHash4 } from "crypto";
|
|
9799
|
-
import { existsSync as
|
|
10451
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
9800
10452
|
function hashString(s) {
|
|
9801
10453
|
return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
|
|
9802
10454
|
}
|
|
9803
10455
|
function hashFile(path) {
|
|
9804
|
-
return hashString(
|
|
10456
|
+
return hashString(readFileSync16(path, "utf8"));
|
|
9805
10457
|
}
|
|
9806
10458
|
function hashFileIfExists(path) {
|
|
9807
|
-
if (!
|
|
10459
|
+
if (!existsSync19(path))
|
|
9808
10460
|
return null;
|
|
9809
10461
|
return hashFile(path);
|
|
9810
10462
|
}
|
|
@@ -9813,33 +10465,33 @@ function hashFileIfExists(path) {
|
|
|
9813
10465
|
init_paths2();
|
|
9814
10466
|
|
|
9815
10467
|
// src/jira/mcp-backend.ts
|
|
9816
|
-
import { existsSync as
|
|
10468
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
9817
10469
|
import { tmpdir } from "os";
|
|
9818
|
-
import { join as
|
|
10470
|
+
import { join as join18 } from "path";
|
|
9819
10471
|
var MCP_ENV = "XERA_MCP_JIRA";
|
|
9820
10472
|
async function createMcpBackend(_baseUrl) {
|
|
9821
10473
|
if (process.env[MCP_ENV] !== "1")
|
|
9822
10474
|
return null;
|
|
9823
|
-
const tmpDir =
|
|
9824
|
-
|
|
10475
|
+
const tmpDir = join18(tmpdir(), "xera-mcp");
|
|
10476
|
+
mkdirSync11(tmpDir, { recursive: true });
|
|
9825
10477
|
return {
|
|
9826
10478
|
backend: "mcp",
|
|
9827
10479
|
async fetchTicket(key, _fields) {
|
|
9828
|
-
const cachePath =
|
|
9829
|
-
if (!
|
|
10480
|
+
const cachePath = join18(tmpDir, `${key}.json`);
|
|
10481
|
+
if (!existsSync20(cachePath)) {
|
|
9830
10482
|
throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
|
|
9831
10483
|
}
|
|
9832
|
-
const parsed = JSON.parse(
|
|
10484
|
+
const parsed = JSON.parse(readFileSync17(cachePath, "utf8"));
|
|
9833
10485
|
return parsed;
|
|
9834
10486
|
},
|
|
9835
10487
|
async postComment(key, body) {
|
|
9836
|
-
const outPath =
|
|
9837
|
-
|
|
10488
|
+
const outPath = join18(tmpDir, `${key}.comment.json`);
|
|
10489
|
+
writeFileSync10(outPath, JSON.stringify({ key, body }));
|
|
9838
10490
|
return { id: "mcp-pending" };
|
|
9839
10491
|
},
|
|
9840
10492
|
async transitionStatus(key, statusName) {
|
|
9841
|
-
const outPath =
|
|
9842
|
-
|
|
10493
|
+
const outPath = join18(tmpDir, `${key}.transition.json`);
|
|
10494
|
+
writeFileSync10(outPath, JSON.stringify({ key, statusName }));
|
|
9843
10495
|
},
|
|
9844
10496
|
async listFields(_sampleKey) {
|
|
9845
10497
|
throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
|
|
@@ -9992,8 +10644,8 @@ async function fetchCmd(argv, opts = {}) {
|
|
|
9992
10644
|
const storyHash = hashString(body);
|
|
9993
10645
|
const acLines = parseAcLines(t.acceptanceCriteria);
|
|
9994
10646
|
const full = renderStory(t.key, t.summary, storyHash, acLines, body);
|
|
9995
|
-
|
|
9996
|
-
|
|
10647
|
+
mkdirSync12(dirname5(paths.storyPath), { recursive: true });
|
|
10648
|
+
writeFileSync11(paths.storyPath, full);
|
|
9997
10649
|
const existing = readMeta(paths.metaPath);
|
|
9998
10650
|
writeMeta(paths.metaPath, {
|
|
9999
10651
|
ticket,
|
|
@@ -10053,24 +10705,223 @@ function renderStory(key, summary, storyHash, acLines, body) {
|
|
|
10053
10705
|
`) + body;
|
|
10054
10706
|
}
|
|
10055
10707
|
|
|
10708
|
+
// src/bin-internal/fill-gap-finalize.ts
|
|
10709
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
|
|
10710
|
+
import { join as join19 } from "path";
|
|
10711
|
+
import { z as z7 } from "zod";
|
|
10712
|
+
var ProposalsSchema = z7.object({
|
|
10713
|
+
proposals: z7.array(z7.object({
|
|
10714
|
+
id: z7.string().min(1),
|
|
10715
|
+
ticketId: z7.string().min(1),
|
|
10716
|
+
title: z7.string().min(1),
|
|
10717
|
+
rationale: z7.string().min(1),
|
|
10718
|
+
gherkin: z7.string().min(1),
|
|
10719
|
+
satisfiesAcs: z7.array(z7.number().int().nonnegative())
|
|
10720
|
+
}))
|
|
10721
|
+
});
|
|
10722
|
+
function parseArgs4(argv) {
|
|
10723
|
+
let accept;
|
|
10724
|
+
let ticket;
|
|
10725
|
+
let source;
|
|
10726
|
+
let force = false;
|
|
10727
|
+
for (let i = 0;i < argv.length; i++) {
|
|
10728
|
+
const a = argv[i];
|
|
10729
|
+
if (a === "--accept") {
|
|
10730
|
+
const v = argv[++i];
|
|
10731
|
+
if (v !== undefined)
|
|
10732
|
+
accept = v;
|
|
10733
|
+
} else if (a === "--ticket") {
|
|
10734
|
+
const v = argv[++i];
|
|
10735
|
+
if (v !== undefined)
|
|
10736
|
+
ticket = v;
|
|
10737
|
+
} else if (a === "--source") {
|
|
10738
|
+
const v = argv[++i];
|
|
10739
|
+
if (v !== undefined)
|
|
10740
|
+
source = v;
|
|
10741
|
+
} else if (a === "--force") {
|
|
10742
|
+
force = true;
|
|
10743
|
+
} else if (a === "--help-stub") {} else {
|
|
10744
|
+
return { error: `unknown flag: ${a}` };
|
|
10745
|
+
}
|
|
10746
|
+
}
|
|
10747
|
+
if (!accept || !ticket)
|
|
10748
|
+
return { error: "required: --accept <proposal-id> --ticket <TICKET>" };
|
|
10749
|
+
const out = { accept, ticket, force };
|
|
10750
|
+
if (source !== undefined)
|
|
10751
|
+
out.source = source;
|
|
10752
|
+
return out;
|
|
10753
|
+
}
|
|
10754
|
+
function formatDraft(ticketId, proposal) {
|
|
10755
|
+
const lines = [
|
|
10756
|
+
`# Draft scenario for ${ticketId}`,
|
|
10757
|
+
"",
|
|
10758
|
+
`> ${proposal.rationale}`,
|
|
10759
|
+
"",
|
|
10760
|
+
proposal.gherkin,
|
|
10761
|
+
""
|
|
10762
|
+
];
|
|
10763
|
+
if (proposal.satisfiesAcs.length > 0) {
|
|
10764
|
+
lines.push(`<!-- satisfiesAcs: [${proposal.satisfiesAcs.join(", ")}] -->`);
|
|
10765
|
+
lines.push("");
|
|
10766
|
+
}
|
|
10767
|
+
return lines.join(`
|
|
10768
|
+
`);
|
|
10769
|
+
}
|
|
10770
|
+
async function fillGapFinalizeCmd(argv) {
|
|
10771
|
+
if (argv.includes("--help-stub")) {}
|
|
10772
|
+
const parsed = parseArgs4(argv);
|
|
10773
|
+
if ("error" in parsed) {
|
|
10774
|
+
console.error(`[fill-gap-finalize] ${parsed.error}`);
|
|
10775
|
+
return 1;
|
|
10776
|
+
}
|
|
10777
|
+
const cwd = process.cwd();
|
|
10778
|
+
const sourcePath = parsed.source ?? join19(cwd, ".xera/coverage/proposals.json");
|
|
10779
|
+
if (!existsSync21(sourcePath)) {
|
|
10780
|
+
console.error(`[fill-gap-finalize] source not found: ${sourcePath}`);
|
|
10781
|
+
return 2;
|
|
10782
|
+
}
|
|
10783
|
+
let proposals;
|
|
10784
|
+
try {
|
|
10785
|
+
const raw = JSON.parse(readFileSync18(sourcePath, "utf8"));
|
|
10786
|
+
proposals = ProposalsSchema.parse(raw);
|
|
10787
|
+
} catch (e) {
|
|
10788
|
+
console.error(`[fill-gap-finalize] invalid proposals: ${e.message}`);
|
|
10789
|
+
return 2;
|
|
10790
|
+
}
|
|
10791
|
+
const proposal = proposals.proposals.find((p) => p.id === parsed.accept);
|
|
10792
|
+
if (!proposal) {
|
|
10793
|
+
console.error(`[fill-gap-finalize] proposal id "${parsed.accept}" not in source`);
|
|
10794
|
+
return 2;
|
|
10795
|
+
}
|
|
10796
|
+
const ticketDir = join19(cwd, ".xera", parsed.ticket);
|
|
10797
|
+
mkdirSync13(ticketDir, { recursive: true });
|
|
10798
|
+
const draftPath = join19(ticketDir, "feature.draft.md");
|
|
10799
|
+
if (existsSync21(draftPath) && !parsed.force) {
|
|
10800
|
+
console.error(`[fill-gap-finalize] ${draftPath} exists; pass --force to overwrite`);
|
|
10801
|
+
return 3;
|
|
10802
|
+
}
|
|
10803
|
+
writeFileSync12(draftPath, formatDraft(parsed.ticket, proposal));
|
|
10804
|
+
return 0;
|
|
10805
|
+
}
|
|
10806
|
+
|
|
10807
|
+
// src/bin-internal/fill-gap-prepare.ts
|
|
10808
|
+
init_store();
|
|
10809
|
+
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
|
|
10810
|
+
import { join as join20 } from "path";
|
|
10811
|
+
function parseArgs5(argv) {
|
|
10812
|
+
const args = {};
|
|
10813
|
+
for (let i = 0;i < argv.length; i++) {
|
|
10814
|
+
const a = argv[i];
|
|
10815
|
+
if (a === "--area") {
|
|
10816
|
+
const v = argv[++i];
|
|
10817
|
+
if (v !== undefined)
|
|
10818
|
+
args.area = v;
|
|
10819
|
+
} else if (a === "--ticket") {
|
|
10820
|
+
const v = argv[++i];
|
|
10821
|
+
if (v !== undefined)
|
|
10822
|
+
args.ticket = v;
|
|
10823
|
+
} else if (a === "--output-dir") {
|
|
10824
|
+
const v = argv[++i];
|
|
10825
|
+
if (v !== undefined)
|
|
10826
|
+
args.outputDir = v;
|
|
10827
|
+
} else if (a === "--help-stub") {} else {
|
|
10828
|
+
return { error: `unknown flag: ${a}` };
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
if (!args.area && !args.ticket)
|
|
10832
|
+
return { error: "one of --area or --ticket required" };
|
|
10833
|
+
if (args.area && args.ticket)
|
|
10834
|
+
return { error: "--area and --ticket are mutually exclusive" };
|
|
10835
|
+
return args;
|
|
10836
|
+
}
|
|
10837
|
+
function buildAreaContext(snap, area) {
|
|
10838
|
+
const edgeTicketIds = new Set(snap.edges.filter((e) => e.kind === "modifies" && e.to === area).map((e) => e.from));
|
|
10839
|
+
const ticketsForArea = Object.values(snap.tickets).filter((t) => t.modifiesAreas.includes(area) || edgeTicketIds.has(t.id));
|
|
10840
|
+
if (ticketsForArea.length === 0)
|
|
10841
|
+
return null;
|
|
10842
|
+
const scenariosFromOtherAreas = Object.values(snap.scenarios).filter((s) => {
|
|
10843
|
+
const t = snap.tickets[s.ticketId];
|
|
10844
|
+
if (!t)
|
|
10845
|
+
return false;
|
|
10846
|
+
return !t.modifiesAreas.includes(area) && t.modifiesAreas.length > 0;
|
|
10847
|
+
}).slice(0, 3).map((s) => {
|
|
10848
|
+
const ownerTicket = snap.tickets[s.ticketId];
|
|
10849
|
+
const areaSlug = ownerTicket?.modifiesAreas[0] ?? "unknown";
|
|
10850
|
+
return { areaSlug, gherkin: s.gherkin };
|
|
10851
|
+
});
|
|
10852
|
+
return {
|
|
10853
|
+
mode: "area",
|
|
10854
|
+
area,
|
|
10855
|
+
tickets: ticketsForArea.map((t) => ({ id: t.id, summary: t.summary, ac: t.ac })),
|
|
10856
|
+
existingScenarios: scenariosFromOtherAreas
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
10859
|
+
function buildTicketContext(snap, ticketId) {
|
|
10860
|
+
const ticket = snap.tickets[ticketId];
|
|
10861
|
+
if (!ticket)
|
|
10862
|
+
return null;
|
|
10863
|
+
const acNodesForTicket = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId).sort((a, b) => a.index - b.index);
|
|
10864
|
+
const satisfiedAcIds = new Set(snap.edges.filter((e) => e.kind === "satisfies" && acNodesForTicket.some((ac) => ac.id === e.to)).map((e) => e.to));
|
|
10865
|
+
const unsatisfiedAcs = acNodesForTicket.filter((ac) => !satisfiedAcIds.has(ac.id)).map((ac) => ({ index: ac.index, text: ac.text }));
|
|
10866
|
+
if (unsatisfiedAcs.length === 0)
|
|
10867
|
+
return null;
|
|
10868
|
+
const existingScenarios = Object.values(snap.scenarios).filter((s) => s.ticketId === ticketId).map((s) => ({ scenarioId: s.id, name: s.name, gherkin: s.gherkin }));
|
|
10869
|
+
return {
|
|
10870
|
+
mode: "ticket",
|
|
10871
|
+
ticket: { id: ticket.id, summary: ticket.summary, ac: ticket.ac },
|
|
10872
|
+
unsatisfiedAcs,
|
|
10873
|
+
existingScenarios
|
|
10874
|
+
};
|
|
10875
|
+
}
|
|
10876
|
+
async function fillGapPrepareCmd(argv) {
|
|
10877
|
+
const parsed = parseArgs5(argv);
|
|
10878
|
+
if ("error" in parsed) {
|
|
10879
|
+
console.error(`[fill-gap-prepare] ${parsed.error}`);
|
|
10880
|
+
return 1;
|
|
10881
|
+
}
|
|
10882
|
+
const cwd = process.cwd();
|
|
10883
|
+
const snap = deriveSnapshot(loadAllEvents(cwd));
|
|
10884
|
+
let context;
|
|
10885
|
+
let scope;
|
|
10886
|
+
if (parsed.area) {
|
|
10887
|
+
context = buildAreaContext(snap, parsed.area);
|
|
10888
|
+
scope = parsed.area;
|
|
10889
|
+
if (!context) {
|
|
10890
|
+
console.error(`[fill-gap-prepare] area "${parsed.area}" has no tickets modifying it; cannot fill`);
|
|
10891
|
+
return 2;
|
|
10892
|
+
}
|
|
10893
|
+
} else {
|
|
10894
|
+
context = buildTicketContext(snap, parsed.ticket);
|
|
10895
|
+
scope = parsed.ticket;
|
|
10896
|
+
if (!context) {
|
|
10897
|
+
console.error(`[fill-gap-prepare] ticket "${parsed.ticket}" not found or has no unsatisfied ACs`);
|
|
10898
|
+
return 2;
|
|
10899
|
+
}
|
|
10900
|
+
}
|
|
10901
|
+
const outDir = parsed.outputDir ?? join20(cwd, ".xera/coverage", scope);
|
|
10902
|
+
mkdirSync14(outDir, { recursive: true });
|
|
10903
|
+
writeFileSync13(join20(outDir, "context.json"), JSON.stringify(context, null, 2));
|
|
10904
|
+
return 0;
|
|
10905
|
+
}
|
|
10906
|
+
|
|
10056
10907
|
// src/bin-internal/index.ts
|
|
10057
10908
|
init_graph_backfill();
|
|
10058
10909
|
|
|
10059
10910
|
// src/graph/enrich.ts
|
|
10060
10911
|
init_store();
|
|
10061
10912
|
init_ulid();
|
|
10062
|
-
import { existsSync as
|
|
10063
|
-
import { join as
|
|
10064
|
-
import { z as
|
|
10913
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
|
|
10914
|
+
import { join as join21 } from "path";
|
|
10915
|
+
import { z as z8 } from "zod";
|
|
10065
10916
|
var MAX_SIMILAR_EDGES = 10;
|
|
10066
10917
|
var MIN_CONFIDENCE = 0.7;
|
|
10067
|
-
var SimilarEntrySchema =
|
|
10068
|
-
ticketId:
|
|
10069
|
-
confidence:
|
|
10070
|
-
reason:
|
|
10918
|
+
var SimilarEntrySchema = z8.object({
|
|
10919
|
+
ticketId: z8.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
10920
|
+
confidence: z8.number(),
|
|
10921
|
+
reason: z8.string()
|
|
10071
10922
|
});
|
|
10072
|
-
var EnrichmentInputSchema =
|
|
10073
|
-
similar:
|
|
10923
|
+
var EnrichmentInputSchema = z8.object({
|
|
10924
|
+
similar: z8.array(SimilarEntrySchema)
|
|
10074
10925
|
});
|
|
10075
10926
|
var nowIso3 = () => new Date().toISOString();
|
|
10076
10927
|
var mk2 = (actor, type, payload) => ({
|
|
@@ -10082,11 +10933,11 @@ var mk2 = (actor, type, payload) => ({
|
|
|
10082
10933
|
payload
|
|
10083
10934
|
});
|
|
10084
10935
|
async function enrichTicket(repoRoot, ticketId, opts) {
|
|
10085
|
-
const inputPath =
|
|
10086
|
-
if (!
|
|
10936
|
+
const inputPath = join21(repoRoot, ".xera", ticketId, "enrichment-input.json");
|
|
10937
|
+
if (!existsSync22(inputPath)) {
|
|
10087
10938
|
throw new Error(`enrichment-input.json not found at ${inputPath}`);
|
|
10088
10939
|
}
|
|
10089
|
-
const raw = JSON.parse(
|
|
10940
|
+
const raw = JSON.parse(readFileSync19(inputPath, "utf8"));
|
|
10090
10941
|
const parsed = EnrichmentInputSchema.safeParse(raw);
|
|
10091
10942
|
if (!parsed.success) {
|
|
10092
10943
|
throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
|
|
@@ -10195,12 +11046,12 @@ async function graphQueryCmd(argv) {
|
|
|
10195
11046
|
init_graph_record();
|
|
10196
11047
|
|
|
10197
11048
|
// src/bin-internal/graph-render.ts
|
|
10198
|
-
import { mkdirSync as
|
|
10199
|
-
import { dirname as dirname7, join as
|
|
11049
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync21, renameSync as renameSync2, writeFileSync as writeFileSync14 } from "fs";
|
|
11050
|
+
import { dirname as dirname7, join as join23 } from "path";
|
|
10200
11051
|
|
|
10201
11052
|
// src/graph/render.ts
|
|
10202
|
-
import { readFileSync as
|
|
10203
|
-
import { dirname as dirname6, join as
|
|
11053
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
11054
|
+
import { dirname as dirname6, join as join22 } from "path";
|
|
10204
11055
|
import { fileURLToPath } from "url";
|
|
10205
11056
|
var COLORS = {
|
|
10206
11057
|
ticket: "#3B82F6",
|
|
@@ -10419,9 +11270,9 @@ function transformForVisNetwork(snap, opts) {
|
|
|
10419
11270
|
}
|
|
10420
11271
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
10421
11272
|
var __dirname2 = dirname6(__filename2);
|
|
10422
|
-
var TEMPLATES_DIR =
|
|
11273
|
+
var TEMPLATES_DIR = join22(__dirname2, "templates");
|
|
10423
11274
|
function loadTemplate(name) {
|
|
10424
|
-
return
|
|
11275
|
+
return readFileSync20(join22(TEMPLATES_DIR, name), "utf8");
|
|
10425
11276
|
}
|
|
10426
11277
|
function statsToHuman(s) {
|
|
10427
11278
|
return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
|
|
@@ -10433,7 +11284,10 @@ function renderHtml(input) {
|
|
|
10433
11284
|
const visNetwork = loadTemplate("vis-network.min.js");
|
|
10434
11285
|
const graphJson = JSON.stringify(input.data);
|
|
10435
11286
|
const statsHuman = statsToHuman(input.stats);
|
|
10436
|
-
|
|
11287
|
+
const coverageTabButton = input.coverage ? '<button data-tab="coverage">Coverage</button>' : "";
|
|
11288
|
+
const coverageTabPanel = input.coverage ? loadTemplate("coverage-panel.html.fragment") : "";
|
|
11289
|
+
const coverageJson = input.coverage ? JSON.stringify(input.coverage) : "null";
|
|
11290
|
+
return template.replace("{{CSS}}", () => css).replace("{{STATS}}", () => statsHuman).replace("{{GENERATED_AT}}", () => input.generatedAt).replace("{{VIS_NETWORK_JS}}", () => visNetwork).replace("{{GRAPH_DATA}}", () => graphJson).replace("{{INTERACTION_JS}}", () => js).replace("{{COVERAGE_TAB_BUTTON}}", () => coverageTabButton).replace("{{COVERAGE_TAB_PANEL}}", () => coverageTabPanel).replace("{{COVERAGE_DATA}}", () => coverageJson);
|
|
10437
11291
|
}
|
|
10438
11292
|
|
|
10439
11293
|
// src/bin-internal/graph-render.ts
|
|
@@ -10456,6 +11310,7 @@ async function graphRenderCmd(argv) {
|
|
|
10456
11310
|
let ticketId;
|
|
10457
11311
|
let since;
|
|
10458
11312
|
let depth = 2;
|
|
11313
|
+
let includeCoverage = false;
|
|
10459
11314
|
for (let i = 0;i < argv.length; i++) {
|
|
10460
11315
|
if (argv[i] === "--out")
|
|
10461
11316
|
outPath = argv[++i];
|
|
@@ -10465,16 +11320,19 @@ async function graphRenderCmd(argv) {
|
|
|
10465
11320
|
since = argv[++i];
|
|
10466
11321
|
else if (argv[i] === "--depth")
|
|
10467
11322
|
depth = parseDepth(argv[++i]);
|
|
11323
|
+
else if (argv[i] === "--include-coverage")
|
|
11324
|
+
includeCoverage = true;
|
|
10468
11325
|
}
|
|
10469
11326
|
const repoRoot = process.cwd();
|
|
10470
|
-
const finalPath = outPath ??
|
|
10471
|
-
const
|
|
11327
|
+
const finalPath = outPath ?? join23(repoRoot, ".xera/graph.html");
|
|
11328
|
+
const events = loadAllEvents(repoRoot);
|
|
11329
|
+
const snap = deriveSnapshot(events);
|
|
10472
11330
|
const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
|
|
10473
11331
|
const performanceMode = decidePerformanceMode(totalNodeCount);
|
|
10474
11332
|
if (performanceMode === "text-fallback") {
|
|
10475
11333
|
const txtPath = finalPath.replace(/\.html$/, ".txt");
|
|
10476
|
-
|
|
10477
|
-
|
|
11334
|
+
mkdirSync15(dirname7(txtPath), { recursive: true });
|
|
11335
|
+
writeFileSync14(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
|
|
10478
11336
|
`);
|
|
10479
11337
|
console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
|
|
10480
11338
|
return 0;
|
|
@@ -10484,11 +11342,29 @@ async function graphRenderCmd(argv) {
|
|
|
10484
11342
|
opts.ticketId = ticketId;
|
|
10485
11343
|
if (since)
|
|
10486
11344
|
opts.since = since;
|
|
11345
|
+
let coverage;
|
|
11346
|
+
if (includeCoverage) {
|
|
11347
|
+
const reportPath = join23(repoRoot, ".xera/coverage/report.json");
|
|
11348
|
+
if (existsSync23(reportPath)) {
|
|
11349
|
+
const report = JSON.parse(readFileSync21(reportPath, "utf8"));
|
|
11350
|
+
const snapshots = events.filter((e) => e.type === "coverage.snapshot").map((e) => e.payload);
|
|
11351
|
+
coverage = { report, snapshots };
|
|
11352
|
+
} else {
|
|
11353
|
+
console.warn("[graph-render] --include-coverage: report.json not found; run /xera-coverage first");
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
10487
11356
|
const data = transformForVisNetwork(snap, opts);
|
|
10488
|
-
const
|
|
10489
|
-
|
|
11357
|
+
const renderInput = {
|
|
11358
|
+
data,
|
|
11359
|
+
stats: data.stats,
|
|
11360
|
+
generatedAt: new Date().toISOString()
|
|
11361
|
+
};
|
|
11362
|
+
if (coverage)
|
|
11363
|
+
renderInput.coverage = coverage;
|
|
11364
|
+
const html = renderHtml(renderInput);
|
|
11365
|
+
mkdirSync15(dirname7(finalPath), { recursive: true });
|
|
10490
11366
|
const tmpPath = `${finalPath}.tmp`;
|
|
10491
|
-
|
|
11367
|
+
writeFileSync14(tmpPath, html);
|
|
10492
11368
|
renameSync2(tmpPath, finalPath);
|
|
10493
11369
|
console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
|
|
10494
11370
|
return 0;
|
|
@@ -10519,8 +11395,8 @@ async function graphSnapshotCmd(argv) {
|
|
|
10519
11395
|
}
|
|
10520
11396
|
|
|
10521
11397
|
// src/bin-internal/heal-prepare.ts
|
|
10522
|
-
import { existsSync as
|
|
10523
|
-
import { join as
|
|
11398
|
+
import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
|
|
11399
|
+
import { join as join24 } from "path";
|
|
10524
11400
|
import { scrubFreeText } from "@xera-ai/web";
|
|
10525
11401
|
|
|
10526
11402
|
// ../../node_modules/fflate/esm/index.mjs
|
|
@@ -10867,15 +11743,15 @@ function strFromU8(dat, latin1) {
|
|
|
10867
11743
|
var slzh = function(d, b) {
|
|
10868
11744
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
10869
11745
|
};
|
|
10870
|
-
var zh = function(d, b,
|
|
11746
|
+
var zh = function(d, b, z9) {
|
|
10871
11747
|
var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
|
|
10872
|
-
var _a2 = z64hs(d, es, efl,
|
|
11748
|
+
var _a2 = z64hs(d, es, efl, z9, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
10873
11749
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
10874
11750
|
};
|
|
10875
|
-
var z64hs = function(d, b, l,
|
|
11751
|
+
var z64hs = function(d, b, l, z9, sc, su, off) {
|
|
10876
11752
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
10877
11753
|
var nf = nsc + nsu + noff;
|
|
10878
|
-
if (
|
|
11754
|
+
if (z9 && nf) {
|
|
10879
11755
|
for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
10880
11756
|
if (b2(d, b) == 1) {
|
|
10881
11757
|
return [
|
|
@@ -10886,7 +11762,7 @@ var z64hs = function(d, b, l, z7, sc, su, off) {
|
|
|
10886
11762
|
];
|
|
10887
11763
|
}
|
|
10888
11764
|
}
|
|
10889
|
-
if (
|
|
11765
|
+
if (z9 < 2)
|
|
10890
11766
|
err(13);
|
|
10891
11767
|
}
|
|
10892
11768
|
return [sc, su, off, 0];
|
|
@@ -10902,18 +11778,18 @@ function unzipSync(data, opts) {
|
|
|
10902
11778
|
if (!c)
|
|
10903
11779
|
return {};
|
|
10904
11780
|
var o = b4(data, e + 16);
|
|
10905
|
-
var
|
|
10906
|
-
if (
|
|
11781
|
+
var z9 = b4(data, e - 20) == 117853008;
|
|
11782
|
+
if (z9) {
|
|
10907
11783
|
var ze = b4(data, e - 12);
|
|
10908
|
-
|
|
10909
|
-
if (
|
|
11784
|
+
z9 = b4(data, ze) == 101075792;
|
|
11785
|
+
if (z9) {
|
|
10910
11786
|
c = b4(data, ze + 32);
|
|
10911
11787
|
o = b4(data, ze + 48);
|
|
10912
11788
|
}
|
|
10913
11789
|
}
|
|
10914
11790
|
var fltr = opts && opts.filter;
|
|
10915
11791
|
for (var i2 = 0;i2 < c; ++i2) {
|
|
10916
|
-
var _a2 = zh(data, o,
|
|
11792
|
+
var _a2 = zh(data, o, z9), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
10917
11793
|
o = no;
|
|
10918
11794
|
if (!fltr || fltr({
|
|
10919
11795
|
name: fn,
|
|
@@ -10949,9 +11825,9 @@ function classifyKind(raw) {
|
|
|
10949
11825
|
return "other";
|
|
10950
11826
|
}
|
|
10951
11827
|
function extractDomSnapshot(tracePath) {
|
|
10952
|
-
if (!
|
|
11828
|
+
if (!existsSync24(tracePath))
|
|
10953
11829
|
return "";
|
|
10954
|
-
const buf =
|
|
11830
|
+
const buf = readFileSync22(tracePath);
|
|
10955
11831
|
const entries = unzipSync(buf);
|
|
10956
11832
|
const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
|
|
10957
11833
|
let chosenKey = null;
|
|
@@ -10999,16 +11875,16 @@ function extractDomSnapshot(tracePath) {
|
|
|
10999
11875
|
return scrubFreeText(html);
|
|
11000
11876
|
}
|
|
11001
11877
|
function findPomLine(ticketDir, rawLocator) {
|
|
11002
|
-
const pomDir =
|
|
11878
|
+
const pomDir = join24(ticketDir, "page-objects");
|
|
11003
11879
|
const candidates = [];
|
|
11004
|
-
if (
|
|
11880
|
+
if (existsSync24(pomDir)) {
|
|
11005
11881
|
for (const name of readdirSync6(pomDir)) {
|
|
11006
11882
|
if (name.endsWith(".ts"))
|
|
11007
|
-
candidates.push(
|
|
11883
|
+
candidates.push(join24(pomDir, name));
|
|
11008
11884
|
}
|
|
11009
11885
|
}
|
|
11010
11886
|
for (const file of candidates) {
|
|
11011
|
-
const text =
|
|
11887
|
+
const text = readFileSync22(file, "utf8");
|
|
11012
11888
|
const lines = text.split(`
|
|
11013
11889
|
`);
|
|
11014
11890
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
@@ -11046,13 +11922,13 @@ function findGherkinStep(featureText, rawLocator) {
|
|
|
11046
11922
|
}
|
|
11047
11923
|
function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
11048
11924
|
const paths = resolveArtifactPaths(repoRoot, ticket);
|
|
11049
|
-
const classifierPath =
|
|
11050
|
-
const classifier = JSON.parse(
|
|
11925
|
+
const classifierPath = join24(paths.ticketDir, "classifier-input.json");
|
|
11926
|
+
const classifier = JSON.parse(readFileSync22(classifierPath, "utf8"));
|
|
11051
11927
|
const cls = classifier.scenarios.find((s) => s.name === scenarioName);
|
|
11052
11928
|
if (!cls)
|
|
11053
11929
|
throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
|
|
11054
|
-
const runDir =
|
|
11055
|
-
const normalized = JSON.parse(
|
|
11930
|
+
const runDir = join24(paths.runsDir, runId);
|
|
11931
|
+
const normalized = JSON.parse(readFileSync22(join24(runDir, "normalized.json"), "utf8"));
|
|
11056
11932
|
const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
|
|
11057
11933
|
if (!normSc?.failure)
|
|
11058
11934
|
throw new Error(`no failure recorded for scenario "${scenarioName}"`);
|
|
@@ -11063,9 +11939,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
|
11063
11939
|
const raw = m[1].trim();
|
|
11064
11940
|
const kind = classifyKind(raw);
|
|
11065
11941
|
const pomLoc = findPomLine(paths.ticketDir, raw);
|
|
11066
|
-
const featureText =
|
|
11942
|
+
const featureText = readFileSync22(paths.featurePath, "utf8");
|
|
11067
11943
|
const gherkinStep = findGherkinStep(featureText, raw);
|
|
11068
|
-
const domSnapshotAtFailure = extractDomSnapshot(
|
|
11944
|
+
const domSnapshotAtFailure = extractDomSnapshot(join24(runDir, "trace.zip"));
|
|
11069
11945
|
return {
|
|
11070
11946
|
ticket,
|
|
11071
11947
|
runId,
|
|
@@ -11085,8 +11961,8 @@ async function healPrepareCmd(argv) {
|
|
|
11085
11961
|
try {
|
|
11086
11962
|
const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
|
|
11087
11963
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
11088
|
-
const outPath =
|
|
11089
|
-
|
|
11964
|
+
const outPath = join24(paths.runsDir, runId, "heal-input.json");
|
|
11965
|
+
writeFileSync15(outPath, JSON.stringify(result, null, 2));
|
|
11090
11966
|
console.log(`[xera:heal-prepare] wrote ${outPath}`);
|
|
11091
11967
|
return 0;
|
|
11092
11968
|
} catch (err2) {
|
|
@@ -11096,8 +11972,8 @@ async function healPrepareCmd(argv) {
|
|
|
11096
11972
|
}
|
|
11097
11973
|
|
|
11098
11974
|
// src/bin-internal/impact-prepare.ts
|
|
11099
|
-
import { mkdirSync as
|
|
11100
|
-
import { join as
|
|
11975
|
+
import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync16 } from "fs";
|
|
11976
|
+
import { join as join25 } from "path";
|
|
11101
11977
|
|
|
11102
11978
|
// src/graph/impact.ts
|
|
11103
11979
|
var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
|
|
@@ -11347,11 +12223,11 @@ async function impactPrepareCmd(argv) {
|
|
|
11347
12223
|
scenarios,
|
|
11348
12224
|
generatedAt: new Date().toISOString()
|
|
11349
12225
|
};
|
|
11350
|
-
const impactDir =
|
|
11351
|
-
|
|
11352
|
-
|
|
12226
|
+
const impactDir = join25(repoRoot, ".xera/impact");
|
|
12227
|
+
mkdirSync16(impactDir, { recursive: true });
|
|
12228
|
+
writeFileSync16(join25(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
|
|
11353
12229
|
if (!quiet) {
|
|
11354
|
-
|
|
12230
|
+
writeFileSync16(join25(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
|
|
11355
12231
|
}
|
|
11356
12232
|
return 0;
|
|
11357
12233
|
}
|
|
@@ -11377,8 +12253,8 @@ async function lintCmd(argv) {
|
|
|
11377
12253
|
}
|
|
11378
12254
|
|
|
11379
12255
|
// src/bin-internal/normalize.ts
|
|
11380
|
-
import { existsSync as
|
|
11381
|
-
import { join as
|
|
12256
|
+
import { existsSync as existsSync25, readdirSync as readdirSync7 } from "fs";
|
|
12257
|
+
import { join as join26 } from "path";
|
|
11382
12258
|
init_paths2();
|
|
11383
12259
|
async function normalizeCmd(argv) {
|
|
11384
12260
|
const ticket = argv[0];
|
|
@@ -11393,8 +12269,8 @@ async function normalizeCmd(argv) {
|
|
|
11393
12269
|
console.error("[xera:normalize] no run found");
|
|
11394
12270
|
return 1;
|
|
11395
12271
|
}
|
|
11396
|
-
const runDir =
|
|
11397
|
-
if (!
|
|
12272
|
+
const runDir = join26(paths.runsDir, runId);
|
|
12273
|
+
if (!existsSync25(runDir)) {
|
|
11398
12274
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
11399
12275
|
return 1;
|
|
11400
12276
|
}
|
|
@@ -11414,14 +12290,14 @@ async function normalizeCmd(argv) {
|
|
|
11414
12290
|
|
|
11415
12291
|
// src/bin-internal/post.ts
|
|
11416
12292
|
init_paths2();
|
|
11417
|
-
import { existsSync as
|
|
11418
|
-
import { join as
|
|
12293
|
+
import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
|
|
12294
|
+
import { join as join27 } from "path";
|
|
11419
12295
|
|
|
11420
12296
|
// src/artifact/status.ts
|
|
11421
|
-
import { existsSync as
|
|
12297
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync17 } from "fs";
|
|
11422
12298
|
import { dirname as dirname8 } from "path";
|
|
11423
|
-
import { z as
|
|
11424
|
-
var ClassificationEnum =
|
|
12299
|
+
import { z as z9 } from "zod";
|
|
12300
|
+
var ClassificationEnum = z9.enum([
|
|
11425
12301
|
"PASS",
|
|
11426
12302
|
"REAL_BUG",
|
|
11427
12303
|
"SELECTOR_DRIFT",
|
|
@@ -11432,37 +12308,37 @@ var ClassificationEnum = z7.enum([
|
|
|
11432
12308
|
"RATE_LIMITED",
|
|
11433
12309
|
"AUTH_EXPIRED"
|
|
11434
12310
|
]);
|
|
11435
|
-
var ResultEnum =
|
|
11436
|
-
var ConfidenceEnum =
|
|
11437
|
-
var HistoryEntrySchema =
|
|
11438
|
-
ts:
|
|
12311
|
+
var ResultEnum = z9.enum(["PASS", "FAIL"]);
|
|
12312
|
+
var ConfidenceEnum = z9.enum(["low", "medium", "high"]);
|
|
12313
|
+
var HistoryEntrySchema = z9.object({
|
|
12314
|
+
ts: z9.string(),
|
|
11439
12315
|
result: ResultEnum,
|
|
11440
12316
|
class: ClassificationEnum
|
|
11441
12317
|
});
|
|
11442
|
-
var StatusJsonSchema =
|
|
11443
|
-
ticket:
|
|
11444
|
-
lastRun:
|
|
12318
|
+
var StatusJsonSchema = z9.object({
|
|
12319
|
+
ticket: z9.string(),
|
|
12320
|
+
lastRun: z9.string(),
|
|
11445
12321
|
result: ResultEnum,
|
|
11446
12322
|
classification: ClassificationEnum,
|
|
11447
12323
|
confidence: ConfidenceEnum,
|
|
11448
|
-
scenarios:
|
|
11449
|
-
total:
|
|
11450
|
-
passed:
|
|
11451
|
-
failed:
|
|
11452
|
-
skipped:
|
|
12324
|
+
scenarios: z9.object({
|
|
12325
|
+
total: z9.number().int().nonnegative(),
|
|
12326
|
+
passed: z9.number().int().nonnegative(),
|
|
12327
|
+
failed: z9.number().int().nonnegative(),
|
|
12328
|
+
skipped: z9.number().int().nonnegative()
|
|
11453
12329
|
}),
|
|
11454
|
-
history:
|
|
11455
|
-
last_jira_comment_id:
|
|
12330
|
+
history: z9.array(HistoryEntrySchema).default([]),
|
|
12331
|
+
last_jira_comment_id: z9.string().optional()
|
|
11456
12332
|
});
|
|
11457
12333
|
var HISTORY_CAP = 20;
|
|
11458
12334
|
function readStatus(path) {
|
|
11459
|
-
if (!
|
|
12335
|
+
if (!existsSync26(path))
|
|
11460
12336
|
return null;
|
|
11461
|
-
return StatusJsonSchema.parse(JSON.parse(
|
|
12337
|
+
return StatusJsonSchema.parse(JSON.parse(readFileSync23(path, "utf8")));
|
|
11462
12338
|
}
|
|
11463
12339
|
function writeStatus(path, status) {
|
|
11464
|
-
|
|
11465
|
-
|
|
12340
|
+
mkdirSync17(dirname8(path), { recursive: true });
|
|
12341
|
+
writeFileSync17(path, JSON.stringify(status, null, 2));
|
|
11466
12342
|
}
|
|
11467
12343
|
function appendHistory(path, entry) {
|
|
11468
12344
|
const s = readStatus(path);
|
|
@@ -11488,12 +12364,12 @@ async function postCmd(argv) {
|
|
|
11488
12364
|
return 0;
|
|
11489
12365
|
}
|
|
11490
12366
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
11491
|
-
const draftPath =
|
|
11492
|
-
if (!
|
|
12367
|
+
const draftPath = join27(paths.ticketDir, "jira-comment.draft.md");
|
|
12368
|
+
if (!existsSync27(draftPath)) {
|
|
11493
12369
|
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
11494
12370
|
return 1;
|
|
11495
12371
|
}
|
|
11496
|
-
const body =
|
|
12372
|
+
const body = readFileSync24(draftPath, "utf8");
|
|
11497
12373
|
const client = await createJiraClient({
|
|
11498
12374
|
baseUrl: config.jira.baseUrl,
|
|
11499
12375
|
preferMcp: true,
|
|
@@ -11521,8 +12397,8 @@ async function promoteCmd(argv) {
|
|
|
11521
12397
|
}
|
|
11522
12398
|
|
|
11523
12399
|
// src/bin-internal/report.ts
|
|
11524
|
-
import { existsSync as
|
|
11525
|
-
import { join as
|
|
12400
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync18 } from "fs";
|
|
12401
|
+
import { join as join28 } from "path";
|
|
11526
12402
|
init_paths2();
|
|
11527
12403
|
|
|
11528
12404
|
// src/classifier/aggregate.ts
|
|
@@ -11789,11 +12665,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
|
|
|
11789
12665
|
}
|
|
11790
12666
|
|
|
11791
12667
|
// src/reporter/status-writer.ts
|
|
11792
|
-
import { existsSync as
|
|
12668
|
+
import { existsSync as existsSync28 } from "fs";
|
|
11793
12669
|
function writeStatusFromClassification(path, input) {
|
|
11794
12670
|
const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
|
|
11795
12671
|
const entry = { ts: input.runTs, result, class: input.classification.overall };
|
|
11796
|
-
if (!
|
|
12672
|
+
if (!existsSync28(path)) {
|
|
11797
12673
|
writeStatus(path, {
|
|
11798
12674
|
ticket: input.ticket,
|
|
11799
12675
|
lastRun: input.runTs,
|
|
@@ -11827,22 +12703,22 @@ async function reportCmd(argv) {
|
|
|
11827
12703
|
}
|
|
11828
12704
|
const cwd = process.cwd();
|
|
11829
12705
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
11830
|
-
const input = JSON.parse(
|
|
12706
|
+
const input = JSON.parse(readFileSync25(inputArg.slice("--input=".length), "utf8"));
|
|
11831
12707
|
let httpRuleOverride = null;
|
|
11832
12708
|
const meta = readMeta(paths.metaPath);
|
|
11833
12709
|
if (meta?.adapter === "http") {
|
|
11834
12710
|
const config = await loadConfig(cwd);
|
|
11835
12711
|
if (config.http) {
|
|
11836
|
-
const normalizedPath =
|
|
11837
|
-
if (
|
|
11838
|
-
const norm = JSON.parse(
|
|
12712
|
+
const normalizedPath = join28(paths.ticketDir, "runs", input.runId, "normalized.json");
|
|
12713
|
+
if (existsSync29(normalizedPath)) {
|
|
12714
|
+
const norm = JSON.parse(readFileSync25(normalizedPath, "utf8"));
|
|
11839
12715
|
const calls = norm.http?.calls ?? [];
|
|
11840
12716
|
const rate = classifyRateLimited({ calls });
|
|
11841
12717
|
if (rate)
|
|
11842
12718
|
httpRuleOverride = rate;
|
|
11843
12719
|
if (!httpRuleOverride) {
|
|
11844
12720
|
const authFiles = {};
|
|
11845
|
-
const httpAuthDir =
|
|
12721
|
+
const httpAuthDir = join28(cwd, ".xera", ".auth", "http");
|
|
11846
12722
|
for (const role of Object.keys(config.http.auth.roles)) {
|
|
11847
12723
|
const entry = readAuthState(httpAuthDir, role);
|
|
11848
12724
|
if (entry) {
|
|
@@ -11887,8 +12763,8 @@ async function reportCmd(argv) {
|
|
|
11887
12763
|
confidence: "high"
|
|
11888
12764
|
} : s) : input.scenarios;
|
|
11889
12765
|
const aggregated = aggregateScenarios(scenariosForAggregation);
|
|
11890
|
-
const decisionsPath =
|
|
11891
|
-
const decisions =
|
|
12766
|
+
const decisionsPath = join28(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
|
|
12767
|
+
const decisions = existsSync29(decisionsPath) ? JSON.parse(readFileSync25(decisionsPath, "utf8")) : {};
|
|
11892
12768
|
const graph = deriveSnapshot(loadAllEvents(process.cwd()));
|
|
11893
12769
|
const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
11894
12770
|
const scenarioIdByName = {};
|
|
@@ -11936,8 +12812,8 @@ async function reportCmd(argv) {
|
|
|
11936
12812
|
xeraVersion: XERA_VERSION,
|
|
11937
12813
|
promptsVersion: PROMPTS_VERSION
|
|
11938
12814
|
});
|
|
11939
|
-
const draftPath =
|
|
11940
|
-
|
|
12815
|
+
const draftPath = join28(paths.ticketDir, "jira-comment.draft.md");
|
|
12816
|
+
writeFileSync18(draftPath, md);
|
|
11941
12817
|
console.log(`[xera:report] wrote status.json and ${draftPath}`);
|
|
11942
12818
|
return 0;
|
|
11943
12819
|
}
|
|
@@ -12008,7 +12884,7 @@ async function unlockCmd(argv) {
|
|
|
12008
12884
|
|
|
12009
12885
|
// src/bin-internal/validate-feature.ts
|
|
12010
12886
|
init_paths2();
|
|
12011
|
-
import { existsSync as
|
|
12887
|
+
import { existsSync as existsSync30, readFileSync as readFileSync26 } from "fs";
|
|
12012
12888
|
import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
|
|
12013
12889
|
async function validateFeatureCmd(argv) {
|
|
12014
12890
|
const ticket = argv[0];
|
|
@@ -12017,11 +12893,11 @@ async function validateFeatureCmd(argv) {
|
|
|
12017
12893
|
return 1;
|
|
12018
12894
|
}
|
|
12019
12895
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
12020
|
-
if (!
|
|
12896
|
+
if (!existsSync30(paths.featurePath)) {
|
|
12021
12897
|
console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
|
|
12022
12898
|
return 1;
|
|
12023
12899
|
}
|
|
12024
|
-
const r = validateGherkin2(
|
|
12900
|
+
const r = validateGherkin2(readFileSync26(paths.featurePath, "utf8"));
|
|
12025
12901
|
if (r.ok) {
|
|
12026
12902
|
console.log("[xera:validate-feature] ok");
|
|
12027
12903
|
return 0;
|
|
@@ -12033,13 +12909,18 @@ async function validateFeatureCmd(argv) {
|
|
|
12033
12909
|
|
|
12034
12910
|
// src/bin-internal/index.ts
|
|
12035
12911
|
var COMMANDS = {
|
|
12912
|
+
"ac-coverage-backfill-finalize": acCoverageBackfillFinalizeCmd,
|
|
12913
|
+
"ac-coverage-backfill-prepare": acCoverageBackfillPrepareCmd,
|
|
12036
12914
|
"auth-setup": authSetupCmd,
|
|
12915
|
+
"coverage-prepare": coveragePrepareCmd,
|
|
12037
12916
|
disputes: disputesCmd,
|
|
12038
12917
|
doctor: doctorCmd,
|
|
12039
12918
|
"eval-deterministic": evalDeterministicCmd,
|
|
12040
12919
|
"eval-prepare": evalPrepareCmd,
|
|
12041
12920
|
"eval-report": evalReportCmd,
|
|
12042
12921
|
exec: execCmd,
|
|
12922
|
+
"fill-gap-finalize": fillGapFinalizeCmd,
|
|
12923
|
+
"fill-gap-prepare": fillGapPrepareCmd,
|
|
12043
12924
|
fetch: fetchCmd,
|
|
12044
12925
|
"graph-backfill": graphBackfillCmd,
|
|
12045
12926
|
"graph-enrich": graphEnrichCmd,
|