petari 0.7.0 → 0.9.0
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/README.md +23 -2
- package/dist/commands/apply.js +7 -1
- package/dist/core/matcher.js +45 -1
- package/dist/core/nearest.js +15 -7
- package/dist/core/report.js +25 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,25 @@ petari # 返答 (changes.md) を適用
|
|
|
48
48
|
petari の更新で規約文が変わった場合は、`petari init` を再実行すると protocol.md の
|
|
49
49
|
差分を検出して更新を提案します (slnmix 側の更新は不要)。
|
|
50
50
|
|
|
51
|
+
#### 新規 .vb ファイルの作成 (v0.9.0)
|
|
52
|
+
|
|
53
|
+
AI が `create` で `.vb` を新規作成すると、petari が次を自動で行います (Visual Studio での
|
|
54
|
+
手作業をなくすため)。いずれも失敗しても create 自体は成功し、理由を結果表示に出します。
|
|
55
|
+
|
|
56
|
+
- **旧スタイル .vbproj への登録**: 作成先から上へ辿って最寄りの `.vbproj` を探し、
|
|
57
|
+
`<Compile Include="...">` を追加します (`Inherits Form` なら `<SubType>Form</SubType>`、
|
|
58
|
+
`X.Designer.vb` なら `<DependentUpon>X.vb</DependentUpon>` 付き)。追加行以外のバイト列は
|
|
59
|
+
変えません (Shift_JIS / CRLF / BOM そのまま)。SDK スタイルの `.vbproj` は登録不要のため
|
|
60
|
+
触りません。登録は履歴に含まれ `petari undo` で戻ります。無効化は `--no-vbproj` か
|
|
61
|
+
`.petari/config.json` の `"vbproj": { "register": false }`
|
|
62
|
+
- **エンコーディングの推定**: `.petari/config.json` の `"newFile": { "encoding": "auto" }`
|
|
63
|
+
(`petari init` の雛形の既定) で、同じディレクトリの既存ファイルの多数決で
|
|
64
|
+
文字コード / BOM / 改行を決めます。`.vb` を BOM なし UTF-8 で作ることはありません
|
|
65
|
+
(vbc / VS で日本語が化けるため)。従来どおり `"utf8"` / `"shift_jis"` の明示指定も使えます
|
|
66
|
+
|
|
67
|
+
規約文 v4 では「SEARCH は一意に特定できる範囲のうち最小 (目安 3〜8 行)」を追加しました。
|
|
68
|
+
`petari init` の再実行で protocol.md を更新してください。
|
|
69
|
+
|
|
51
70
|
### 適用が繰り返し失敗するとき
|
|
52
71
|
|
|
53
72
|
失敗レポート (自動でクリップボードにコピーされます) をそのまま AI に貼り返すのが
|
|
@@ -59,7 +78,8 @@ slnmix の出力) が実ファイルとずれている**可能性が高いです
|
|
|
59
78
|
長い添付を要約・検索処理で部分的にしか読まないことがあり、AI が見えなかった箇所を
|
|
60
79
|
もっともらしく補って出力することがあります。SEARCH の空白調整や言い換えを繰り返す
|
|
61
80
|
より、スナップショットを取り直して渡し直すのが確実です (petari 側は空白・インデント・
|
|
62
|
-
|
|
81
|
+
空行の位置や数・文字コードの差を吸収して照合するため、それらが原因で失敗することは
|
|
82
|
+
ありません)。
|
|
63
83
|
|
|
64
84
|
### コマンド
|
|
65
85
|
|
|
@@ -73,7 +93,8 @@ slnmix の出力) が実ファイルとずれている**可能性が高いです
|
|
|
73
93
|
| `petari init` | プロジェクト初回セットアップ (`--yes` で全提案に同意) |
|
|
74
94
|
|
|
75
95
|
主なオプション: `--dry-run` (検証と差分プレビューのみ) / `--partial` (成功分のみ適用) /
|
|
76
|
-
`--root <dir>` / `--yes` / `--clip-report` (失敗レポートをクリップボードへ。v0.5.0 から既定で自動コピーされるため、config で無効化した場合の個別指定用)
|
|
96
|
+
`--root <dir>` / `--yes` / `--clip-report` (失敗レポートをクリップボードへ。v0.5.0 から既定で自動コピーされるため、config で無効化した場合の個別指定用) /
|
|
97
|
+
`--no-vbproj` (create した .vb の .vbproj 自動登録を今回だけ無効化)
|
|
77
98
|
|
|
78
99
|
## 特徴
|
|
79
100
|
|
package/dist/commands/apply.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
import { invalidPathReason, planChangeSet, } from "../core/applier.js";
|
|
10
10
|
import { PRESENCE_STAGE_LABEL } from "../core/matcher.js";
|
|
11
11
|
import { parseChangesRecovering } from "../core/parser.js";
|
|
12
|
-
import { buildFailureReport, buildParseErrorReport } from "../core/report.js";
|
|
12
|
+
import { buildBlankInsensitiveNote, buildFailureReport, buildParseErrorReport, } from "../core/report.js";
|
|
13
13
|
import { readClipboard, writeClipboard } from "../infra/clipboard.js";
|
|
14
14
|
import { loadConfig } from "../infra/config.js";
|
|
15
15
|
import { findRecentChangesFiles, resolveDownloadsDir, } from "../infra/downloads.js";
|
|
@@ -224,6 +224,9 @@ export async function applyCommand(argv) {
|
|
|
224
224
|
out(`dry-run: 適用予定 ${applicable.length} ファイル (書き込みなし)`);
|
|
225
225
|
printPreview(applicable);
|
|
226
226
|
printAlreadyApplied(plan.outcomes);
|
|
227
|
+
const dryRunNote = buildBlankInsensitiveNote(plan.outcomes);
|
|
228
|
+
if (dryRunNote !== null)
|
|
229
|
+
out(dryRunNote);
|
|
227
230
|
if (plan.failures.length > 0) {
|
|
228
231
|
out("");
|
|
229
232
|
await emitReport(buildFailureReport(plan.failures, { outcomes: plan.outcomes, nothingWritten: true }));
|
|
@@ -312,6 +315,9 @@ export async function applyCommand(argv) {
|
|
|
312
315
|
for (const o of applicable)
|
|
313
316
|
out(` ${opLabel(o)}`);
|
|
314
317
|
printAlreadyApplied(plan.outcomes);
|
|
318
|
+
const appliedNote = buildBlankInsensitiveNote(plan.outcomes);
|
|
319
|
+
if (appliedNote !== null)
|
|
320
|
+
out(appliedNote);
|
|
315
321
|
if (plan.failures.length > 0) {
|
|
316
322
|
out(`スキップした失敗 ${plan.failures.length} 件のレポート:`);
|
|
317
323
|
out("");
|
package/dist/core/matcher.js
CHANGED
|
@@ -2,6 +2,7 @@ export const STAGE_LABEL = {
|
|
|
2
2
|
"exact": "完全一致",
|
|
3
3
|
"trim-end": "行末空白無視",
|
|
4
4
|
"trim-all": "インデント無視",
|
|
5
|
+
"blank-insensitive": "空行差無視",
|
|
5
6
|
};
|
|
6
7
|
const STAGES = [
|
|
7
8
|
{ stage: "exact", eq: (a, b) => a === b },
|
|
@@ -89,7 +90,47 @@ export function reindent(replaceLines, searchBase, fileBase) {
|
|
|
89
90
|
return fileBase + line.trimStart();
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* 4 段目 blank-insensitive (§6): SEARCH と実ファイルの双方から空行を除いた行列を
|
|
95
|
+
* trim-all 比較 (a.trim() === b.trim()) で連続一致探索する。マッチ範囲は最初と最後の
|
|
96
|
+
* 非空行で囲まれた元ファイル範囲 (内部の空行は含む、外側の空行は含まない)。
|
|
97
|
+
* SEARCH の先頭・末尾の空行は照合範囲に含めず、実ファイル側の外側の空行はそのまま残る。
|
|
98
|
+
* REPLACE 側の空行はそのまま挿入する (意図的な空行挿入を潰さないため、勝手に削らない)。
|
|
99
|
+
* 見つからないときは null (呼び出し元が not-found にする)。
|
|
100
|
+
*/
|
|
101
|
+
function matchBlankInsensitive(lines, block) {
|
|
102
|
+
const searchNonBlank = block.search.filter((l) => l.trim() !== "");
|
|
103
|
+
// SEARCH が空行のみなら試行しない (どこにでも一致し得るため)
|
|
104
|
+
if (searchNonBlank.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
const fileNonBlank = [];
|
|
107
|
+
const origIdx = [];
|
|
108
|
+
lines.forEach((line, i) => {
|
|
109
|
+
if (line.trim() !== "") {
|
|
110
|
+
fileNonBlank.push(line);
|
|
111
|
+
origIdx.push(i);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const found = findMatches(fileNonBlank, searchNonBlank, (a, b) => a.trim() === b.trim());
|
|
115
|
+
if (found.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
if (found.length > 1) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
block,
|
|
121
|
+
reason: "ambiguous",
|
|
122
|
+
stage: "blank-insensitive",
|
|
123
|
+
count: found.length,
|
|
124
|
+
positions: found.map((h) => origIdx[h]),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const hit = found[0];
|
|
128
|
+
const start = origIdx[hit];
|
|
129
|
+
const end = origIdx[hit + searchNonBlank.length - 1] + 1;
|
|
130
|
+
const replacement = reindent(block.replace, baseIndent(block.search), baseIndent(lines.slice(start, end)));
|
|
131
|
+
return { ok: true, block, stage: "blank-insensitive", start, end, replacement };
|
|
132
|
+
}
|
|
133
|
+
/** 1 ブロックを段階フォールバック (STAGES → blank-insensitive) でマッチさせ、適用後の置換行も算出する */
|
|
93
134
|
export function matchBlock(lines, block) {
|
|
94
135
|
for (const { stage, eq } of STAGES) {
|
|
95
136
|
const found = findMatches(lines, block.search, eq);
|
|
@@ -105,6 +146,9 @@ export function matchBlock(lines, block) {
|
|
|
105
146
|
return { ok: false, block, reason: "ambiguous", stage, count: found.length, positions: found };
|
|
106
147
|
}
|
|
107
148
|
}
|
|
149
|
+
const blankInsensitive = matchBlankInsensitive(lines, block);
|
|
150
|
+
if (blankInsensitive !== null)
|
|
151
|
+
return blankInsensitive;
|
|
108
152
|
return { ok: false, block, reason: "not-found" };
|
|
109
153
|
}
|
|
110
154
|
/**
|
package/dist/core/nearest.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* 1 行の過不足で位置合わせ全体がずれ、別の類似箇所 (End Sub 等) を抜粋してしまうため、
|
|
13
13
|
* ギャップ (行の挿入・欠落) を許容するアラインメント (fit alignment) へ変更した。
|
|
14
14
|
* 実ファイル側にだけある行は "+" として抜粋に含め、空行なら専用の注記と修正ヒントを出す。
|
|
15
|
+
*
|
|
16
|
+
* 2026-08-30 追記: matcher に 4 段目 blank-insensitive が入り、空行の差だけなら SEARCH
|
|
17
|
+
* マッチングの段階で自動吸収されるようになった。この診断に空行差が現れるのは「空行以外にも
|
|
18
|
+
* 差がある」ケースだけなので、注記は空行が原因と読めない文言にする (誤誘導防止)。
|
|
15
19
|
*/
|
|
16
20
|
/** 前後のコンテキストとして抜粋に含める行数 */
|
|
17
21
|
const CONTEXT_LINES = 4;
|
|
@@ -71,10 +75,10 @@ function noteFor(fileLine, searchLine, verdict, subject) {
|
|
|
71
75
|
const ft = fileLine.trim();
|
|
72
76
|
const st = searchLine.trim();
|
|
73
77
|
if (ft === "" && st !== "") {
|
|
74
|
-
return
|
|
78
|
+
return `実ファイル側のこの行は空行です (空行の位置・数の差は自動で吸収されるため、失敗の原因は空行ではなく他の行の差にあります)`;
|
|
75
79
|
}
|
|
76
80
|
if (st === "" && ft !== "") {
|
|
77
|
-
return `${subject} 側の対応行は空行ですが、実ファイルのこの行は空行ではありません (
|
|
81
|
+
return `${subject} 側の対応行は空行ですが、実ファイルのこの行は空行ではありません (実ファイル側の行をそのままコピーしてください。空行の差は自動で吸収されます)`;
|
|
78
82
|
}
|
|
79
83
|
if (collapseWs(foldConfusable(ft)) === collapseWs(foldConfusable(st))) {
|
|
80
84
|
return `見た目で区別しづらい文字の差です。${describeFirstDiff(fileLine, searchLine)}`;
|
|
@@ -227,7 +231,7 @@ export function analyzeNearest(fileLines, search, afterPriorBlocks = false, subj
|
|
|
227
231
|
if (blank)
|
|
228
232
|
extraBlankCount++;
|
|
229
233
|
const note = blank
|
|
230
|
-
?
|
|
234
|
+
? `実ファイル側にある空行です (空行の位置・数の差は自動で吸収されるため、この空行自体は失敗の原因ではありません)`
|
|
231
235
|
: `${subject} に含まれていない行です。この行も含めてコピーしてください`;
|
|
232
236
|
lines.push({
|
|
233
237
|
lineNo: step.fileIdx + 1,
|
|
@@ -285,7 +289,7 @@ function renderWindowBody(w, subject) {
|
|
|
285
289
|
}
|
|
286
290
|
for (const u of w.unmatchedSearch) {
|
|
287
291
|
out.push(u.blank
|
|
288
|
-
? `- ${subject} ${u.index} 行目の空行に対応する行がこの領域にありません (${subject}
|
|
292
|
+
? `- ${subject} ${u.index} 行目の空行に対応する行がこの領域にありません (${subject} 側だけの空行は自動で吸収されるため、失敗の原因は他の行にあります)`
|
|
289
293
|
: `- ${subject} ${u.index} 行目 (${u.text.trim()}) に対応する行がこの領域にありません`);
|
|
290
294
|
}
|
|
291
295
|
return out;
|
|
@@ -293,16 +297,20 @@ function renderWindowBody(w, subject) {
|
|
|
293
297
|
/**
|
|
294
298
|
* 修正のヒント (near-miss 自動サジェスト)。SEARCH の全行が順序どおり実ファイルに存在し、
|
|
295
299
|
* 間に SEARCH 側に無い行が挟まっているだけなら「抜粋のコピーで一致する」と機械的に言える。
|
|
296
|
-
*
|
|
300
|
+
* 空行だけの過不足は blank-insensitive 段階で自動吸収されるため、このヒントが空行のみを
|
|
301
|
+
* 指すのは行内空白差 (~) が併発しているときだけ — 原因を空行と誤読させない文言にする。
|
|
297
302
|
*/
|
|
298
303
|
function buildHint(w) {
|
|
299
304
|
if (w.extraCount === 0 || w.unmatchedSearch.length > 0)
|
|
300
305
|
return [];
|
|
301
306
|
if (w.lines.some((l) => l.verdict === "differ"))
|
|
302
307
|
return [];
|
|
308
|
+
const hasWsOnly = w.lines.some((l) => l.verdict === "ws-only");
|
|
303
309
|
const head = w.extraBlankCount === w.extraCount
|
|
304
|
-
?
|
|
305
|
-
|
|
310
|
+
? hasWsOnly
|
|
311
|
+
? `▶ 修正のヒント: 間に挟まる空行 ${w.extraCount} 行の差は自動で吸収されます。この失敗の原因は空行ではなく、空白の個数のみ異なる行 (~) です。`
|
|
312
|
+
: `▶ 修正のヒント: SEARCH の全行はこの順で実ファイルに存在しますが、間に空行 ${w.extraCount} 行が挟まっています。`
|
|
313
|
+
: `▶ 修正のヒント: SEARCH の全行はこの順で実ファイルに存在しますが、間に SEARCH にない行が ${w.extraCount} 行 (うち空行 ${w.extraBlankCount} 行) 挟まっています。空行の差は自動で吸収されますが、内容のある行の欠落は吸収されません。`;
|
|
306
314
|
return [
|
|
307
315
|
"",
|
|
308
316
|
head,
|
package/dist/core/report.js
CHANGED
|
@@ -3,14 +3,14 @@ import { renderNearest, renderNearestReplace } from "./nearest.js";
|
|
|
3
3
|
const MATCH_SPEC_NOTE = [
|
|
4
4
|
"## petari の照合仕様 (SEARCH を修正する前に必ず読んでください)",
|
|
5
5
|
"",
|
|
6
|
-
"- petari は行末空白・インデントの深さ (タブ/スペース混在含む)
|
|
7
|
-
" (UTF-8 / Shift_JIS) の違いを自動で吸収して照合しています",
|
|
8
|
-
"-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"-
|
|
6
|
+
"- petari は行末空白・インデントの深さ (タブ/スペース混在含む)・空行の位置と数・改行コード・",
|
|
7
|
+
" 文字コード (UTF-8 / Shift_JIS) の違いを自動で吸収して照合しています",
|
|
8
|
+
"- 空行の差は双方から空行を除いた照合で吸収されます。ただし空行を除くと複数箇所に一致して",
|
|
9
|
+
" しまう場合は曖昧エラーになります (前後の行を追加して一意に特定できる範囲にしてください)",
|
|
10
|
+
"- したがって「SEARCH が見つかりません」は空白・空行・インデント・エンコーディングの問題では",
|
|
11
|
+
" ありません。内容のある行の文字内容か、その過不足が現在のファイルと異なっています",
|
|
12
|
+
" (抜粋では実ファイル側にだけある行を + で示します)",
|
|
13
|
+
"- 行内空白や空行の調整・ASCII 行だけへの縮小・別アンカーへの乗り換えでは解決しません。各失敗に",
|
|
14
14
|
" 添付した「実ファイルの該当箇所」の抜粋から、行をそのままコピーしてください",
|
|
15
15
|
];
|
|
16
16
|
const RE_REQUEST = `## 依頼
|
|
@@ -99,6 +99,23 @@ function remainingSummary(outcomes) {
|
|
|
99
99
|
: `→ 全 ${totalUnits} ブロック中 ${passed} ブロックは検証を通過しています。残る ${failedUnits} ブロックの SEARCH を修正すれば全体が適用可能になります。`,
|
|
100
100
|
];
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* blank-insensitive (空行差無視) で一致したブロックがあるときの注記 (§6)。
|
|
104
|
+
* 空行の並びが SEARCH の指定どおりにならない可能性を dry-run と適用サマリの末尾で知らせる。
|
|
105
|
+
* 該当ブロックが無ければ null。
|
|
106
|
+
*/
|
|
107
|
+
export function buildBlankInsensitiveNote(outcomes) {
|
|
108
|
+
let count = 0;
|
|
109
|
+
for (const o of outcomes) {
|
|
110
|
+
for (const b of o.appliedBlocks) {
|
|
111
|
+
if (b.stage === "blank-insensitive")
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (count === 0)
|
|
116
|
+
return null;
|
|
117
|
+
return `注: ${count} 件のブロックは空行の差を吸収して適用しました (git diff で空行の並びを確認してください)`;
|
|
118
|
+
}
|
|
102
119
|
/** 検証失敗 (マッチング・パス・エンコーディング) のレポート */
|
|
103
120
|
export function buildFailureReport(failures, ctx = {}) {
|
|
104
121
|
const parts = ["以下の変更ブロックが現在のコードベースに適用できませんでした。"];
|