@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 +1 -1
- package/src/todo-source-line-eol.mjs +20 -0
- package/src/todo-store.mjs +13 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.64.
|
|
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
|
+
}
|
package/src/todo-store.mjs
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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:
|
|
3401
|
+
archived_source_bytes_digest: operation.source_digest, entry_digest: '' };
|
|
3400
3402
|
if (entry.published_source_bytes_digest !== sha256Bytes(expectedPublishedBytes)
|
|
3401
|
-
||
|
|
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 (
|
|
3476
|
-
||
|
|
3477
|
-
||
|
|
3478
|
-
revision.source_cutover_batch.operations[index], archivedBytes)
|
|
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 (
|
|
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 ||
|
|
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');
|