@quolu/lattice 0.63.7 → 0.63.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.63.7",
3
+ "version": "0.63.9",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
package/src/todo-cli.mjs CHANGED
@@ -437,24 +437,32 @@ function looksLikeEvidenceDescriptor(value) {
437
437
  return value !== null && typeof value === 'object' && !Array.isArray(value);
438
438
  }
439
439
 
440
- function writeEvidenceBlob(repoRoot, bytes) {
441
- const oid = gitSync(['hash-object', '-w', '--stdin'], {
440
+ function writeEvidenceBlob(repoRoot, bytes, blobPath) {
441
+ // worktree pathから作る証拠は`git add`と同じclean filterを通す。生のCRLF bytesを
442
+ // そのままhashすると、commit先はLFへ正規化される一方、descriptorはdangling blobを
443
+ // 指すため、commit後もhard検証を通せない。
444
+ const oid = gitSync(['hash-object', '-w', `--path=${blobPath}`, '--stdin'], {
442
445
  cwd: repoRoot, input: bytes, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'],
443
446
  }).trim();
447
+ const storedBytes = gitSync(['cat-file', 'blob', oid], {
448
+ cwd: repoRoot, stdio: ['ignore', 'pipe', 'ignore'],
449
+ });
444
450
  return {
445
451
  git_blob_oid: oid,
446
- content_digest: createHash('sha256').update(bytes).digest('hex'),
452
+ content_digest: createHash('sha256').update(storedBytes).digest('hex'),
447
453
  };
448
454
  }
449
455
 
450
456
  async function resolveDoneEvidence({ repoRoot, evidenceRef, evidenceMessage, planKey, taskId }) {
451
457
  if (typeof evidenceMessage === 'string') {
452
458
  const bytes = Buffer.from(evidenceMessage, 'utf8');
459
+ const sourceDigest = createHash('sha256').update(bytes).digest('hex');
460
+ const evidencePath = `.lattice/todo/evidence/${planKey}/${taskId}/${sourceDigest}.md`;
453
461
  const descriptor = {
454
462
  evidence_id: evidenceIdFor(planKey, taskId),
455
463
  repo_id: 'self',
456
- path: `.lattice/todo/evidence/${planKey}/${taskId}.md`,
457
- ...writeEvidenceBlob(repoRoot, bytes),
464
+ path: evidencePath,
465
+ ...writeEvidenceBlob(repoRoot, bytes, evidencePath),
458
466
  media_type: 'text/markdown',
459
467
  anchor_digest: null,
460
468
  };
@@ -486,7 +494,7 @@ async function resolveDoneEvidence({ repoRoot, evidenceRef, evidenceMessage, pla
486
494
  evidence_id: evidenceIdFor(planKey, taskId),
487
495
  repo_id: 'self',
488
496
  path: located.inputRef,
489
- ...writeEvidenceBlob(repoRoot, bytes),
497
+ ...writeEvidenceBlob(repoRoot, bytes, located.inputRef),
490
498
  media_type: mediaTypeFor(located.inputRef),
491
499
  anchor_digest: null,
492
500
  };
@@ -626,6 +634,14 @@ async function mutate({
626
634
  repoRoot, evidenceRef, evidenceMessage, planKey, taskId,
627
635
  });
628
636
  const testResult = testResultRef === null ? null : await readTestResultInput(repoRoot, testResultRef);
637
+ const materializedEvidence = typeof evidenceMessage === 'string'
638
+ ? {
639
+ ref: evidence.path,
640
+ bytes: gitSync(['cat-file', 'blob', evidence.git_blob_oid], {
641
+ cwd: repoRoot, stdio: ['ignore', 'pipe', 'ignore'],
642
+ }),
643
+ }
644
+ : null;
629
645
  let eventPayload = payload;
630
646
  if (kind === 'done' && payload === 'authored') {
631
647
  eventPayload = { evidence, ...(testResult === null ? {} : { test_result: testResult }) };
@@ -641,6 +657,7 @@ async function mutate({
641
657
  writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
642
658
  planKey,
643
659
  event: { kind, task_id: taskId, actor, payload: eventPayload },
660
+ materializedEvidence,
644
661
  });
645
662
  const task = snapshot.tasks.find(({ task_id: current }) => current === event.task_id);
646
663
  const authoredTask = plan.tasks.find(({ task_id: current }) => current === event.task_id);
@@ -128,7 +128,7 @@ function validateEdges(value, schema) {
128
128
  const basic = exactRecord(edge, ['from', 'to']);
129
129
  const refsValid = nodeRef(edge?.from) && nodeRef(edge?.to);
130
130
  const crossPlan = refsValid && isCrossPlanEdge(edge);
131
- const withReason = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema) && crossPlan
131
+ const withReason = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema)
132
132
  && exactRecord(edge, ['from', 'to', 'reason'])
133
133
  && boundedText(edge.reason);
134
134
  return (basic || withReason) && refsValid
@@ -2050,6 +2050,19 @@ export async function appendTodoEvent(options = {}) {
2050
2050
  eventDigest: event.event_digest,
2051
2051
  }),
2052
2052
  });
2053
+ if (options.materializedEvidence !== null && options.materializedEvidence !== undefined) {
2054
+ const asset = options.materializedEvidence;
2055
+ const descriptor = event.kind === 'done' ? event.payload.evidence : null;
2056
+ if (!exactRecord(asset, ['ref', 'bytes']) || !Buffer.isBuffer(asset.bytes)
2057
+ || descriptor === null || asset.ref !== descriptor.path
2058
+ || !asset.ref.startsWith(`${STORE_ROOT_REF}/evidence/`)
2059
+ || sha256Bytes(asset.bytes) !== descriptor.content_digest) {
2060
+ throw new TypeError('materialized evidence input invalid');
2061
+ }
2062
+ // prospective replayが成立した後、journalより先に実体を同じstore lock内へ置く。
2063
+ // content-addressed pathなので再試行と同文面の並行要求も同じbytesへ収束する。
2064
+ await atomicWrite(path.resolve(repoRoot, asset.ref), asset.bytes);
2065
+ }
2053
2066
  const eventBytes = canonicalLine(event);
2054
2067
  const activeRef = member.descriptor.journal_ref;
2055
2068
  const activeAbsolute = path.resolve(repoRoot, activeRef);