@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("; ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.28.1",
3
+ "version": "0.28.2-beta.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",