@quolu/lattice 0.64.2 → 0.64.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.64.2",
3
+ "version": "0.64.3",
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",
@@ -106,13 +106,30 @@ async function prepareProtection(repoRoot) {
106
106
  return { absolute, create: false };
107
107
  }
108
108
 
109
+ function stagedArtifactRefs(repoRoot, refs) {
110
+ const staged = new Set();
111
+ for (let offset = 0; offset < refs.length; offset += INDEX_REFRESH_BATCH_SIZE) {
112
+ const batch = refs.slice(offset, offset + INDEX_REFRESH_BATCH_SIZE);
113
+ const result = gitSpawnSync(['diff-index', '--cached', '--name-only', '-z', 'HEAD', '--', ...batch], {
114
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
115
+ });
116
+ if (result.error || result.signal || result.status !== 0) {
117
+ fail('git_index_artifacts_unreadable', { refs: batch, status: result.status ?? null });
118
+ }
119
+ for (const ref of (result.stdout ?? '').split('\0')) {
120
+ if (ref !== '') staged.add(ref.replaceAll('\\', '/'));
121
+ }
122
+ }
123
+ return staged;
124
+ }
125
+
109
126
  function refreshGitIndex(repoRoot, refs) {
110
127
  for (let offset = 0; offset < refs.length; offset += INDEX_REFRESH_BATCH_SIZE) {
111
128
  const batch = refs.slice(offset, offset + INDEX_REFRESH_BATCH_SIZE);
112
- const refresh = gitSpawnSync(['add', '--refresh', '--', ...batch], {
129
+ const refresh = gitSpawnSync(['update-index', '--refresh', '--', ...batch], {
113
130
  cwd: repoRoot, stdio: 'ignore',
114
131
  });
115
- // git add --refresh は内容をstageせずstat cacheだけを更新する。対象pathは下のdiff-filesで検証する。
132
+ // 対象は呼出前にindex blob=HEADを実証済み。update-index --refreshで内容をstageせずstatだけを揃える。
116
133
  if (refresh.error || refresh.signal || ![0, 1].includes(refresh.status)) {
117
134
  fail('git_index_refresh_failed', { refs: batch, status: refresh.status ?? null });
118
135
  }
@@ -132,52 +149,64 @@ function refreshGitIndex(repoRoot, refs) {
132
149
  export async function repairTodoStoreWorktreeEol({ repoRoot }) {
133
150
  const protection = await prepareProtection(repoRoot);
134
151
  const refs = await collectArtifactRefs(repoRoot);
135
- const converted = [];
152
+ const stagedRefs = stagedArtifactRefs(repoRoot, refs);
153
+ const artifacts = [];
136
154
  for (const ref of refs) {
137
155
  const absolute = path.join(repoRoot, ref);
138
156
  const stats = await safeRegularFileState(repoRoot, ref);
139
157
  const bytes = await readFile(absolute);
140
158
  const normalized = normalizePureCrlf(bytes, ref);
141
- if (normalized === null) continue;
142
- converted.push({
143
- ref, absolute, sourceBytes: bytes, bytes: normalized, mode: stats.mode,
159
+ artifacts.push({
160
+ ref, absolute, sourceBytes: bytes, bytes: normalized ?? bytes, mode: stats.mode,
161
+ converted: normalized !== null,
144
162
  });
145
163
  }
146
164
 
147
165
  let headArtifacts = [];
148
166
  let indexArtifacts = [];
149
- if (converted.length > 0) {
150
- const maxBodyBytes = Math.max(...converted.map(({ bytes }) => bytes.length));
167
+ if (artifacts.length > 0) {
168
+ const maxBodyBytes = Math.max(...artifacts.map(({ bytes }) => bytes.length));
151
169
  try {
152
- headArtifacts = gitCatFileBatch(converted.map(({ ref }) => `HEAD:${ref}`), {
170
+ headArtifacts = gitCatFileBatch(artifacts.map(({ ref }) => `HEAD:${ref}`), {
153
171
  cwd: repoRoot, maxBodyBytes,
154
172
  });
155
173
  } catch (error) {
156
174
  fail('git_head_artifacts_unreadable', { message: error.message });
157
175
  }
158
176
  try {
159
- indexArtifacts = gitCatFileBatch(converted.map(({ ref }) => `:${ref}`), {
177
+ indexArtifacts = gitCatFileBatch(artifacts.map(({ ref }) => `:${ref}`), {
160
178
  cwd: repoRoot, maxBodyBytes,
161
179
  });
162
180
  } catch (error) {
163
181
  fail('git_index_artifacts_unreadable', { message: error.message });
164
182
  }
165
183
  }
166
- for (const [index, artifact] of converted.entries()) {
184
+ const converted = [];
185
+ const refreshRefs = [];
186
+ for (const [index, artifact] of artifacts.entries()) {
167
187
  const head = headArtifacts[index];
168
- if (head?.type !== 'blob' || !head.bytes.equals(artifact.bytes)) {
169
- fail('artifact_not_pure_checkout_conversion', { ref: artifact.ref });
170
- }
171
188
  const staged = indexArtifacts[index];
172
- if (staged?.type !== 'blob' || !staged.bytes.equals(head.bytes)) {
173
- fail('artifact_staged_change_present', { ref: artifact.ref });
189
+ if (artifact.converted) {
190
+ if (head?.type !== 'blob' || !head.bytes.equals(artifact.bytes)) {
191
+ fail('artifact_not_pure_checkout_conversion', { ref: artifact.ref });
192
+ }
193
+ if (stagedRefs.has(artifact.ref)
194
+ || staged?.type !== 'blob' || !staged.bytes.equals(head.bytes)) {
195
+ fail('artifact_staged_change_present', { ref: artifact.ref });
196
+ }
197
+ converted.push(artifact);
198
+ }
199
+ if (!stagedRefs.has(artifact.ref) && head?.type === 'blob' && staged?.type === 'blob'
200
+ && staged.bytes.equals(head.bytes) && artifact.bytes.equals(head.bytes)) {
201
+ refreshRefs.push(artifact.ref);
174
202
  }
175
203
  }
176
204
 
177
205
  for (const artifact of converted) {
178
206
  await atomicReplace({ repoRoot, ...artifact });
179
207
  }
180
- refreshGitIndex(repoRoot, converted.map(({ ref }) => ref));
208
+ // 0.64.2がLF書換後のstat refreshで止まった部分状態も、再実行だけで回復させる。
209
+ refreshGitIndex(repoRoot, refreshRefs);
181
210
  if (protection.create) {
182
211
  await assertSafeDirectoryChain(repoRoot, path.posix.dirname(ATTRIBUTES_REF));
183
212
  await writeFile(protection.absolute, EOL_PROTECTION, { flag: 'wx' });