@tea-agent/loop-agent 0.39.1 → 0.39.3

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.
@@ -8,6 +8,7 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
+ import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
11
12
  import { isDagSourceBindingV2 } from "./types.js";
12
13
  import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
13
14
  export const frontendNormalizationActionSchema = z.enum([
@@ -17,6 +18,7 @@ export const frontendNormalizationActionSchema = z.enum([
17
18
  "canonicalize-verification-alias",
18
19
  "drop-unresolved-verification-symbols",
19
20
  "inject-source-binding",
21
+ "inject-source-fidelity-provenance",
20
22
  ]);
21
23
  export function sha256Hex(input) {
22
24
  return createHash("sha256").update(input, "utf8").digest("hex");
@@ -1155,6 +1157,74 @@ function withDerivedRequirementIdsWhenUnscoped(binding, parsed) {
1155
1157
  ? { ...binding, requirementIds: derivedIds }
1156
1158
  : binding;
1157
1159
  }
1160
+ /**
1161
+ * Restore source-fidelity evidence after an editable planner/revision patch
1162
+ * has replaced the requirements array. Provenance is runtime-owned: the
1163
+ * ledger, pinned by the DAG source binding, is the sole authority for a
1164
+ * requirement's sourceFragmentIds and sourceRefs. This deliberately
1165
+ * overwrites values emitted by a model instead of treating them as an
1166
+ * override, so a planner cannot omit, fabricate, or drift source evidence.
1167
+ */
1168
+ async function injectRuntimeOwnedSourceProvenance(input) {
1169
+ if (!isDagSourceBindingV2(input.sourceBinding)) {
1170
+ return { value: input.value, injected: false };
1171
+ }
1172
+ const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
1173
+ if (!workspaceRoot) {
1174
+ throw new Error("frontend source fidelity provenance injection requires a DAG run directory under .harness/dag-runs");
1175
+ }
1176
+ const ledgerPath = resolveDagTaskSourcePath({
1177
+ workspaceRoot,
1178
+ taskId: input.sourceBinding.taskId,
1179
+ sourcePath: input.sourceBinding.ledgerPath,
1180
+ });
1181
+ let rawLedger;
1182
+ try {
1183
+ rawLedger = await readFile(ledgerPath, "utf8");
1184
+ }
1185
+ catch (error) {
1186
+ throw new Error(`frontend source fidelity ledger unavailable for provenance injection: ${input.sourceBinding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
1187
+ }
1188
+ const observedSha256 = sha256Hex(rawLedger);
1189
+ if (observedSha256 !== input.sourceBinding.ledgerSha256) {
1190
+ throw new Error(`frontend source fidelity ledger is stale or tampered for provenance injection: ${input.sourceBinding.ledgerPath} (bound ${input.sourceBinding.ledgerSha256}, observed ${observedSha256})`);
1191
+ }
1192
+ const ledger = parseLedgerJson(rawLedger);
1193
+ if (ledger.taskId !== input.sourceBinding.taskId) {
1194
+ throw new Error(`frontend source fidelity ledger taskId mismatch for provenance injection: bound ${input.sourceBinding.taskId}, ledger ${ledger.taskId}`);
1195
+ }
1196
+ if (ledger.inputDigest !== input.sourceBinding.inputDigest) {
1197
+ throw new Error(`frontend source fidelity ledger inputDigest mismatch for provenance injection: bound ${input.sourceBinding.inputDigest}, ledger ${ledger.inputDigest}`);
1198
+ }
1199
+ const record = asRecord(input.value);
1200
+ if (!record || !Array.isArray(record.requirements)) {
1201
+ return { value: input.value, injected: false };
1202
+ }
1203
+ const provenanceByRequirementId = new Map(ledger.canonicalRequirements.map((requirement) => [
1204
+ requirement.id,
1205
+ {
1206
+ sourceFragmentIds: [...requirement.sourceFragmentIds],
1207
+ sourceRefs: [...requirement.sourceRefs],
1208
+ },
1209
+ ]));
1210
+ let injected = false;
1211
+ const requirements = record.requirements.map((item) => {
1212
+ const requirement = asRecord(item);
1213
+ if (!requirement)
1214
+ return item;
1215
+ const requirementId = canonicalizeRequirementId(asString(requirement.id));
1216
+ const provenance = provenanceByRequirementId.get(requirementId);
1217
+ if (!provenance)
1218
+ return item;
1219
+ injected = true;
1220
+ return {
1221
+ ...requirement,
1222
+ sourceFragmentIds: provenance.sourceFragmentIds,
1223
+ sourceRefs: provenance.sourceRefs,
1224
+ };
1225
+ });
1226
+ return { value: { ...record, requirements }, injected };
1227
+ }
1158
1228
  function looksLikeStrictFrontendContract(value) {
1159
1229
  const record = asRecord(value);
1160
1230
  if (!record)
@@ -1686,8 +1756,24 @@ export async function analyzeFrontendImplementationContract(input) {
1686
1756
  // There is exactly one post-security candidate. A fallback candidate would
1687
1757
  // allow malformed raw fields to bypass the boundary checks above.
1688
1758
  const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1759
+ let runtimeOwnedContract = normalizedContract;
1760
+ if (input.injectRuntimeOwnedSourceProvenance) {
1761
+ try {
1762
+ const injected = await injectRuntimeOwnedSourceProvenance({
1763
+ runDir: input.runDir,
1764
+ value: normalizedContract,
1765
+ sourceBinding: input.sourceBinding,
1766
+ });
1767
+ runtimeOwnedContract = injected.value;
1768
+ if (injected.injected)
1769
+ pushAction("inject-source-fidelity-provenance");
1770
+ }
1771
+ catch (error) {
1772
+ fail("blocked", error instanceof Error ? error.message : String(error), candidateJsonSha256);
1773
+ }
1774
+ }
1689
1775
  const candidate = deriveFrontendVerificationCoverage({
1690
- ...(asRecord(normalizedContract) ?? parsed),
1776
+ ...(asRecord(runtimeOwnedContract) ?? parsed),
1691
1777
  sourceBinding: canonicalBinding,
1692
1778
  }, canonicalBinding);
1693
1779
  const result = frontendImplementationContractSchema.safeParse(candidate);
@@ -1886,6 +1972,7 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1886
1972
  runDir: input.runDir,
1887
1973
  rawContractText: serializeDeterministicJson(merged),
1888
1974
  sourceBinding: input.sourceBinding,
1975
+ injectRuntimeOwnedSourceProvenance: true,
1889
1976
  });
1890
1977
  const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1891
1978
  await writeReport({
@@ -2053,6 +2140,7 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
2053
2140
  runDir: input.runDir,
2054
2141
  rawContractText: serializeDeterministicJson(merged),
2055
2142
  sourceBinding: input.sourceBinding,
2143
+ injectRuntimeOwnedSourceProvenance: true,
2056
2144
  });
2057
2145
  const candidateDir = path.posix.join("contracts", "candidates", nodeId);
2058
2146
  await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`), patch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.39.1",
3
+ "version": "0.39.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",