@quolu/lattice 0.64.3 → 0.64.4

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.64.3",
3
+ "version": "0.64.4",
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",
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const CARRIAGE_RETURN = 0x0d;
4
+ const CR_BYTES = Buffer.from([CARRIAGE_RETURN]);
5
+
6
+ function digest(bytes) {
7
+ return createHash('sha256').update(bytes).digest('hex');
8
+ }
9
+
10
+ /**
11
+ * Gitのcheckout EOL変換だけを同一source lineとして扱う。
12
+ * 呼出側はLFで分割済みなので、差になり得る行末CR一byteだけを往復させる。
13
+ */
14
+ export function matchesTodoSourceLineDigest(lineBytes, expectedDigest) {
15
+ if (digest(lineBytes) === expectedDigest) return true;
16
+ if (lineBytes.at(-1) === CARRIAGE_RETURN) {
17
+ return digest(lineBytes.subarray(0, -1)) === expectedDigest;
18
+ }
19
+ return digest(Buffer.concat([lineBytes, CR_BYTES])) === expectedDigest;
20
+ }
@@ -44,6 +44,7 @@ import {
44
44
  } from './todo-structure-git-adapter.mjs';
45
45
  import { sha256Bytes, verifyLinearHashChain } from './hash-chain.mjs';
46
46
  import { gitCatFileBatch, gitSync } from './git-process.mjs';
47
+ import { matchesTodoSourceLineDigest } from './todo-source-line-eol.mjs';
47
48
  import {
48
49
  parseTodoSourceRef,
49
50
  todoCutoverArchiveSourceRef,
@@ -1455,7 +1456,8 @@ function verifyPlanNarrativeAnchors(repoRoot, plan, trustedPlan = null, cache =
1455
1456
  && canonicalizeTodoArtifact(previous) === canonicalizeTodoArtifact(anchor)) continue;
1456
1457
  try {
1457
1458
  const line = pinnedSourceLine(repoRoot, anchor, cache);
1458
- if (sha256Bytes(line) !== anchor.source_line_digest || markdownCheckboxState(line) === null) {
1459
+ if (!matchesTodoSourceLineDigest(line, anchor.source_line_digest)
1460
+ || markdownCheckboxState(line) === null) {
1459
1461
  throw new Error('anchor mismatch');
1460
1462
  }
1461
1463
  } catch {
@@ -3039,7 +3041,7 @@ async function sourceItemBytes(repoRoot, sourceRef) {
3039
3041
  async function verifyRevisionSources(repoRoot, inventory) {
3040
3042
  for (const entry of [...inventory.active, ...inventory.excluded_tombstones]) {
3041
3043
  const line = await sourceItemBytes(repoRoot, entry.source_ref);
3042
- if (sha256Bytes(line) !== entry.source_digest) {
3044
+ if (!matchesTodoSourceLineDigest(line, entry.source_digest)) {
3043
3045
  fail('RECONCILIATION_INCOMPLETE', 'source_digest_mismatch', { source_ref: entry.source_ref });
3044
3046
  }
3045
3047
  if (markdownCheckboxState(line) === null) {
@@ -3396,9 +3398,9 @@ async function buildPhaseV3SourceReceipt(repoRoot, revision, transactionRef) {
3396
3398
  archive_ref: archiveRef, replacement: operation.live_replacement,
3397
3399
  staged_source_bytes_digest: operation.source_digest,
3398
3400
  published_source_bytes_digest: sha256Bytes(publishedBytes),
3399
- archived_source_bytes_digest: sha256Bytes(archivedBytes), entry_digest: '' };
3401
+ archived_source_bytes_digest: operation.source_digest, entry_digest: '' };
3400
3402
  if (entry.published_source_bytes_digest !== sha256Bytes(expectedPublishedBytes)
3401
- || entry.archived_source_bytes_digest !== operation.source_digest) {
3403
+ || !matchesTodoSourceLineDigest(archivedBytes, operation.source_digest)) {
3402
3404
  fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_receipt_bytes_mismatch');
3403
3405
  }
3404
3406
  entry.entry_digest = todoSelfDigest(entry, 'entry_digest');
@@ -3472,10 +3474,11 @@ async function verifyPhaseV3SourceReceipt(repoRoot, revision, receipt) {
3472
3474
  await safeRepoFile(repoRoot, archive.path);
3473
3475
  const publishedBytes = await sourceItemBytes(repoRoot, entry.published_ref);
3474
3476
  const archivedBytes = await sourceItemBytes(repoRoot, entry.archive_ref);
3475
- if (sha256Bytes(publishedBytes) !== entry.published_source_bytes_digest
3476
- || sha256Bytes(archivedBytes) !== entry.archived_source_bytes_digest
3477
- || entry.published_source_bytes_digest !== sha256Bytes(phaseV3PublishedSourceBytes(
3478
- revision.source_cutover_batch.operations[index], archivedBytes))) return false;
3477
+ if (!matchesTodoSourceLineDigest(publishedBytes, entry.published_source_bytes_digest)
3478
+ || !matchesTodoSourceLineDigest(archivedBytes, entry.archived_source_bytes_digest)
3479
+ || !matchesTodoSourceLineDigest(phaseV3PublishedSourceBytes(
3480
+ revision.source_cutover_batch.operations[index], archivedBytes),
3481
+ entry.published_source_bytes_digest)) return false;
3479
3482
  }
3480
3483
  return true;
3481
3484
  }
@@ -3796,7 +3799,7 @@ async function buildSourceCutoverImages(repoRoot, revision) {
3796
3799
  if (line === undefined) fail('RECONCILIATION_INCOMPLETE', 'source_line_missing', {
3797
3800
  source_ref: operation.source_ref,
3798
3801
  });
3799
- if (sha256Bytes(line) !== operation.source_digest) {
3802
+ if (!matchesTodoSourceLineDigest(line, operation.source_digest)) {
3800
3803
  fail('RECONCILIATION_INCOMPLETE', 'source_digest_mismatch', { source_ref: operation.source_ref });
3801
3804
  }
3802
3805
  if (markdownCheckboxState(line) === null) {
@@ -3904,7 +3907,7 @@ async function loadSourceCutoverStage(repoRoot, transaction, revision) {
3904
3907
  const source = parseTodoSourceRef(operation.source_ref);
3905
3908
  if (source.path !== file.ref) continue;
3906
3909
  const line = lines[source.line - 1];
3907
- if (line === undefined || sha256Bytes(line.bytes) !== operation.source_digest
3910
+ if (line === undefined || !matchesTodoSourceLineDigest(line.bytes, operation.source_digest)
3908
3911
  || markdownCheckboxState(line.bytes) === null
3909
3912
  || !liveReplacementPreservesListStructure(line.bytes, operation.live_replacement)) {
3910
3913
  fail('SOURCE_CUTOVER_RECOVERY_REQUIRED', 'source_cutover_stage_invalid');