@tea-agent/loop-agent 0.28.1 → 0.28.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -570,6 +570,106 @@ function canonicalizeVerificationTargetAliases(value) {
|
|
|
570
570
|
: record.requirements;
|
|
571
571
|
return { ...record, requirements };
|
|
572
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Model-written evidence gaps are hypotheses, not authoritative coverage
|
|
575
|
+
* facts. If the plan contains a real verification target that explicitly
|
|
576
|
+
* names a requirement, the executor can prove that the requirement has a
|
|
577
|
+
* verification path even when the model forgot to wire the target into the
|
|
578
|
+
* requirement entry or conservatively marked its gap as blocking.
|
|
579
|
+
*
|
|
580
|
+
* Keep gaps blocking when that proof cannot be derived. This is deliberately
|
|
581
|
+
* narrow: it uses only sourceBinding requirement IDs and structurally present
|
|
582
|
+
* verification targets, and never invents a command, file, or target.
|
|
583
|
+
*/
|
|
584
|
+
function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
585
|
+
const record = asRecord(value);
|
|
586
|
+
if (!record)
|
|
587
|
+
return value;
|
|
588
|
+
const rawTargets = Array.isArray(record.verificationTargets)
|
|
589
|
+
? record.verificationTargets
|
|
590
|
+
: [];
|
|
591
|
+
const targetIds = new Set(rawTargets
|
|
592
|
+
.map((item) => asRecord(item))
|
|
593
|
+
.filter((item) => Boolean(item))
|
|
594
|
+
.map((item) => asString(item.id))
|
|
595
|
+
.filter(Boolean));
|
|
596
|
+
const targetIdsByRequirement = new Map();
|
|
597
|
+
for (const item of rawTargets) {
|
|
598
|
+
const target = asRecord(item);
|
|
599
|
+
if (!target)
|
|
600
|
+
continue;
|
|
601
|
+
const targetId = asString(target.id);
|
|
602
|
+
const commandLabel = asString(target.commandLabel) || asString(target.command);
|
|
603
|
+
const file = asString(target.file);
|
|
604
|
+
// A target is usable evidence only when it has an identity, command and
|
|
605
|
+
// file. The strict schema will validate the final shape afterwards.
|
|
606
|
+
if (!targetId || !commandLabel || !file)
|
|
607
|
+
continue;
|
|
608
|
+
for (const requirementId of asStringArray(target.requirementIds)) {
|
|
609
|
+
const canonicalId = canonicalizeRequirementId(requirementId);
|
|
610
|
+
if (!canonicalBinding.requirementIds.includes(canonicalId))
|
|
611
|
+
continue;
|
|
612
|
+
const ids = targetIdsByRequirement.get(canonicalId) ?? [];
|
|
613
|
+
if (!ids.includes(targetId))
|
|
614
|
+
ids.push(targetId);
|
|
615
|
+
targetIdsByRequirement.set(canonicalId, ids);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const provenRequirementIds = new Set(targetIdsByRequirement.keys());
|
|
619
|
+
const requirements = Array.isArray(record.requirements)
|
|
620
|
+
? record.requirements.map((item) => {
|
|
621
|
+
const requirement = asRecord(item);
|
|
622
|
+
if (!requirement)
|
|
623
|
+
return item;
|
|
624
|
+
const requirementId = canonicalizeRequirementId(asString(requirement.id));
|
|
625
|
+
const inferredTargetIds = targetIdsByRequirement.get(requirementId) ?? [];
|
|
626
|
+
const existingTargetIds = asStringArray(requirement.verificationTargetIds)
|
|
627
|
+
.filter((id) => targetIds.has(id));
|
|
628
|
+
const verificationTargetIds = [...new Set([
|
|
629
|
+
...existingTargetIds,
|
|
630
|
+
...inferredTargetIds,
|
|
631
|
+
])];
|
|
632
|
+
const evidenceGap = asRecord(requirement.evidenceGap);
|
|
633
|
+
const hasProof = provenRequirementIds.has(requirementId);
|
|
634
|
+
return {
|
|
635
|
+
...requirement,
|
|
636
|
+
...(verificationTargetIds.length > 0 ? { verificationTargetIds } : {}),
|
|
637
|
+
...(hasProof
|
|
638
|
+
? (evidenceGap ? { evidenceGap: { ...evidenceGap, blocking: false } } : {})
|
|
639
|
+
: {
|
|
640
|
+
evidenceGap: {
|
|
641
|
+
requirementId,
|
|
642
|
+
description: `No executable verification target can be derived for ${requirementId}`,
|
|
643
|
+
blocking: true,
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
};
|
|
647
|
+
})
|
|
648
|
+
: record.requirements;
|
|
649
|
+
const modelEvidenceGaps = Array.isArray(record.evidenceGaps)
|
|
650
|
+
? record.evidenceGaps.map((item) => {
|
|
651
|
+
const gap = asRecord(item);
|
|
652
|
+
if (!gap)
|
|
653
|
+
return item;
|
|
654
|
+
const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
|
|
655
|
+
// Model gaps are advisory. Blocking status is reconstructed below
|
|
656
|
+
// from the source binding and executable verification targets.
|
|
657
|
+
return { ...gap, blocking: false };
|
|
658
|
+
})
|
|
659
|
+
: [];
|
|
660
|
+
const derivedBlockingGaps = canonicalBinding.requirementIds
|
|
661
|
+
.filter((requirementId) => !provenRequirementIds.has(requirementId))
|
|
662
|
+
.map((requirementId) => ({
|
|
663
|
+
requirementId,
|
|
664
|
+
description: `No executable verification target can be derived for ${requirementId}`,
|
|
665
|
+
blocking: true,
|
|
666
|
+
}));
|
|
667
|
+
return {
|
|
668
|
+
...record,
|
|
669
|
+
requirements,
|
|
670
|
+
evidenceGaps: [...modelEvidenceGaps, ...derivedBlockingGaps],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
573
673
|
function assertFrontendContractPathsSafe(value) {
|
|
574
674
|
const record = asRecord(value);
|
|
575
675
|
if (!record)
|
|
@@ -1069,10 +1169,10 @@ export async function materializeFrontendImplementationContract(input) {
|
|
|
1069
1169
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1070
1170
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1071
1171
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1072
|
-
const candidate = {
|
|
1172
|
+
const candidate = deriveFrontendVerificationCoverage({
|
|
1073
1173
|
...(asRecord(normalizedContract) ?? parsed),
|
|
1074
1174
|
sourceBinding: canonicalBinding,
|
|
1075
|
-
};
|
|
1175
|
+
}, canonicalBinding);
|
|
1076
1176
|
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
1077
1177
|
if (!result.success)
|
|
1078
1178
|
throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|