@tea-agent/loop-agent 0.39.2 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.39.2",
4
- "gitSha": "307af1c538597568c7afc8cb976183cf622eb547",
5
- "builtAt": "2026-08-28T07:27:33.141Z"
3
+ "version": "0.39.3",
4
+ "gitSha": "048826da6ab8dba7f01daa635ab582f68c539d06",
5
+ "builtAt": "2026-08-28T09:06:21.116Z"
6
6
  }
@@ -92,6 +92,18 @@ function isAtxHeading(line) {
92
92
  return null;
93
93
  return { level: match[1].length, text: match[2].trim() };
94
94
  }
95
+ /** Find the nearest explicit AC heading in a nested Markdown heading path.
96
+ * A subsection such as "异常场景" belongs to its enclosing `#### AC-*` block,
97
+ * even though the subsection heading itself has no ID. */
98
+ function nearestAncestorExplicitAc(headingPath) {
99
+ const ancestors = headingPath.split(" > ").slice(0, -1).reverse();
100
+ for (const heading of ancestors) {
101
+ const matched = matchExplicitAcHeading(heading);
102
+ if (matched)
103
+ return matched;
104
+ }
105
+ return null;
106
+ }
95
107
  function isListItem(line) {
96
108
  return /^\s*(?:[-*+]|\d+[.)])\s+(?:\[(?: |x|X)\]\s+)?/.test(line);
97
109
  }
@@ -173,6 +185,7 @@ export function splitDocumentIntoFragments(doc) {
173
185
  // V4 显式 AC 标题(`#### AC-*`)本身即验收段:标题行的 ID 原样保留。
174
186
  const lastHeadingSegment = section.headingPath.split(" > ").pop() ?? section.headingPath;
175
187
  const explicitSectionAc = matchExplicitAcHeading(lastHeadingSegment);
188
+ const ancestorExplicitAc = nearestAncestorExplicitAc(section.headingPath);
176
189
  const sectionKind = classifySectionHeading(section.headingPath);
177
190
  const effectiveKind = explicitSectionAc ? "acceptance" : sectionKind;
178
191
  const bodyStart = section.headingPath
@@ -181,7 +194,7 @@ export function splitDocumentIntoFragments(doc) {
181
194
  const bodyEnd = section.endLine;
182
195
  const bodyLines = lines.slice(Math.max(0, bodyStart - 1), Math.min(bodyEnd, lines.length));
183
196
  // 当前验收章节内最近的显式 AC ID(V4 标题或 AC 条目),正文片段继承它。
184
- let pendingAcId = null;
197
+ let pendingAcId = explicitSectionAc?.id ?? ancestorExplicitAc?.id ?? null;
185
198
  const emit = (fragment) => {
186
199
  const explicit = fragment.explicitId ?? null;
187
200
  let id = explicit;
@@ -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.2",
3
+ "version": "0.39.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",