@quolu/lattice 0.32.0 → 0.34.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.ja.md +11 -1
- package/README.md +11 -2
- package/bin/lattice.mjs +12 -1
- package/package.json +1 -1
- package/sensor/dist/bin/lattice-sensor.js +72 -4
- package/sensor/dist/db/migrations.d.ts +1 -1
- package/sensor/dist/db/migrations.d.ts.map +1 -1
- package/sensor/dist/db/migrations.js +42 -2
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +18 -0
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +76 -9
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +10 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
- package/sensor/dist/extraction/extraction-version.js +5 -1
- package/sensor/dist/extraction/extraction-version.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +2 -0
- package/sensor/dist/extraction/index.d.ts.map +1 -1
- package/sensor/dist/extraction/index.js +38 -3
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +89 -16
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/index.d.ts +8 -1
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +9 -0
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/resolution/import-resolver.d.ts +11 -0
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +13 -5
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +11 -0
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/types.d.ts +4 -0
- package/sensor/dist/resolution/types.d.ts.map +1 -1
- package/sensor/dist/types.d.ts +22 -0
- package/sensor/dist/types.d.ts.map +1 -1
- package/src/artifact-contracts.mjs +3 -0
- package/src/cli-help.mjs +6 -0
- package/src/rc3-scripted-campaign.mjs +3 -1
- package/src/runtime-cli.mjs +280 -36
- package/src/runtime-contracts.mjs +1 -1
- package/src/runtime-control-store.mjs +24 -1
- package/src/runtime-decision-verifier.mjs +1 -1
- package/src/runtime-diff-observer.mjs +2 -2
- package/src/runtime-front-end.mjs +47 -13
- package/src/runtime-hold-recompile.mjs +31 -2
- package/src/runtime-io-sentinel.mjs +16 -9
- package/src/runtime-managed-supervisor.mjs +32 -3
- package/src/runtime-multi-epoch-store.mjs +2 -2
- package/src/runtime-projection.mjs +27 -0
- package/src/runtime-scripted-adapter-controller.mjs +22 -1
- package/src/runtime-seam-resolve.mjs +156 -13
- package/src/runtime-seam-treatment.mjs +2 -1
- package/src/seam-apply.mjs +101 -4
- package/src/seam-cost.mjs +322 -0
- package/src/seam-gate.mjs +146 -0
- package/src/seam-rewrite.mjs +102 -16
- package/src/seam-verification.mjs +26 -3
- package/src/sensor-adapter.mjs +6 -1
- package/src/todo-cli.mjs +55 -0
package/src/seam-rewrite.mjs
CHANGED
|
@@ -21,6 +21,10 @@ function fail(reasons) {
|
|
|
21
21
|
*
|
|
22
22
|
* 複数行importがあるので`from '...'`で終わる行までを1文とする。束縛名は、移した先で
|
|
23
23
|
* どのimportが要るかを語単位で判定するために使う。
|
|
24
|
+
*
|
|
25
|
+
* **書き換え本体はこれを使わない(sc-013)。** planSeamRewriteのimport面はsensorの
|
|
26
|
+
* AST観測(`joinImportSurface`)から受け取る。この正規表現走査が残っているのは
|
|
27
|
+
* seam-costのprofile投影(ESM限定とconfidenceで申告済み)のためだけである。
|
|
24
28
|
*/
|
|
25
29
|
export function scanImportStatements(lines) {
|
|
26
30
|
const statements = [];
|
|
@@ -41,7 +45,7 @@ export function scanImportStatements(lines) {
|
|
|
41
45
|
return { statements, endIndex: statements.length === 0 ? -1 : statements.at(-1).end };
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
function importBindings(text) {
|
|
48
|
+
export function importBindings(text) {
|
|
45
49
|
const bindings = [];
|
|
46
50
|
const namespace = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)/u.exec(text);
|
|
47
51
|
if (namespace) bindings.push(namespace[1]);
|
|
@@ -59,10 +63,39 @@ function importBindings(text) {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/** 語として現れるか。string中やcomment中の一致も拾うが、余分なimportは害にならない。 */
|
|
62
|
-
function mentions(text, name) {
|
|
66
|
+
export function mentions(text, name) {
|
|
63
67
|
return new RegExp(`(?<![\\w$])${name.replace(/[$]/gu, '\\$$')}(?![\\w$])`, 'u').test(text);
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* sensorの観測(`file-nodes`の`imports`と`import_bindings`)をimport文単位へ束ねる(sc-013)。
|
|
72
|
+
*
|
|
73
|
+
* importsは文の行範囲、import_bindingsは束縛ごとの`{local, form, imported, line}`で、
|
|
74
|
+
* 両者は行番号で結合できる(解決済み束縛はedge metadata、builtin等の未解決束縛は
|
|
75
|
+
* unresolved_refs由来——どちらもAST抽出であり、正規表現の再実装ではない)。
|
|
76
|
+
* どの文にも入らない束縛は`unassigned`として返し、黙って捨てない。
|
|
77
|
+
*
|
|
78
|
+
* @returns {{statements: Array<{startLine:number,endLine:number,bindings:string[]}>, unassigned: string[]}}
|
|
79
|
+
*/
|
|
80
|
+
export function joinImportSurface(importNodes = [], importBindings = []) {
|
|
81
|
+
const statements = importNodes
|
|
82
|
+
.filter((node) => Number.isSafeInteger(node?.startLine) && Number.isSafeInteger(node?.endLine)
|
|
83
|
+
&& node.startLine >= 1 && node.endLine >= node.startLine)
|
|
84
|
+
.map((node) => ({ startLine: node.startLine, endLine: node.endLine, bindings: [] }))
|
|
85
|
+
.sort((left, right) => left.startLine - right.startLine);
|
|
86
|
+
const unassigned = [];
|
|
87
|
+
for (const binding of importBindings) {
|
|
88
|
+
if (typeof binding?.local !== 'string' || binding.local === '') continue;
|
|
89
|
+
const owner = Number.isSafeInteger(binding.line)
|
|
90
|
+
? statements.find(({ startLine, endLine }) => binding.line >= startLine && binding.line <= endLine)
|
|
91
|
+
: undefined;
|
|
92
|
+
if (owner === undefined) { unassigned.push(binding.local); continue; }
|
|
93
|
+
if (!owner.bindings.includes(binding.local)) owner.bindings.push(binding.local);
|
|
94
|
+
}
|
|
95
|
+
for (const statement of statements) statement.bindings.sort(compareText);
|
|
96
|
+
return { statements, unassigned: [...new Set(unassigned)].sort(compareText) };
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
/**
|
|
67
100
|
* 宣言の範囲を直前の連続コメント行まで広げる。
|
|
68
101
|
*
|
|
@@ -85,28 +118,44 @@ function exportedBlock(raw) {
|
|
|
85
118
|
return parts.join('\n');
|
|
86
119
|
}
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const declaration = raw.split('\n')
|
|
91
|
-
.find((line) => !COMMENT_LINE.test(line) && line.trim() !== '');
|
|
92
|
-
return declaration !== undefined && /^\s*export\s/u.test(declaration);
|
|
93
|
-
}
|
|
121
|
+
// 原pathでexport宣言だったかは、text走査でなくsensorのisExported(AST事実)で判定する
|
|
122
|
+
// (sc-013)。extentと同じくfile-nodes由来で、symbolExtentsの各entryが持って来る。
|
|
94
123
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
124
|
+
/**
|
|
125
|
+
* repo相対path同士から、ESMが解決できる相対specifierを作る。
|
|
126
|
+
*
|
|
127
|
+
* 以前は行き先がfromの配下でない時に`./<repo相対>`を返しており、親ディレクトリや兄弟
|
|
128
|
+
* ディレクトリへの移動で**解決不能なspecifier**を生成していた(`src/a/x.mjs`→`src/b/y.mjs`で
|
|
129
|
+
* `./src/b/y.mjs`)。segment単位で共通prefixを外し、残りを`../`で遡って組み立てる。
|
|
130
|
+
*/
|
|
131
|
+
export function relativeSpecifier(fromPath, toPath) {
|
|
132
|
+
const fromSegments = fromPath.split('/').slice(0, -1);
|
|
133
|
+
const toSegments = toPath.split('/');
|
|
134
|
+
let shared = 0;
|
|
135
|
+
while (shared < fromSegments.length && shared < toSegments.length - 1
|
|
136
|
+
&& fromSegments[shared] === toSegments[shared]) shared += 1;
|
|
137
|
+
const ascent = '../'.repeat(fromSegments.length - shared);
|
|
138
|
+
const descent = toSegments.slice(shared).join('/');
|
|
139
|
+
return ascent === '' ? `./${descent}` : `${ascent}${descent}`;
|
|
98
140
|
}
|
|
99
141
|
|
|
100
142
|
/**
|
|
101
143
|
* 三面の変換後textを作る。
|
|
102
144
|
*
|
|
145
|
+
* import面と各symbolのexport状態はsensorの観測を入力として受け取る(sc-013)。
|
|
146
|
+
* ここで正規表現によるimport再解析を行わない——言語理解はsensorが所有し、
|
|
147
|
+
* ここは決まった移動のtext組み立てだけを持つ。観測が無ければtyped理由で止める。
|
|
148
|
+
* 唯一残るtext走査は直前コメント行の巻き込み(extendUpward)で、sensorはcomment行の
|
|
149
|
+
* 範囲を記録しないため、これはtext組み立ての一部として保持する。
|
|
150
|
+
*
|
|
103
151
|
* @param {object} options
|
|
104
152
|
* @param {string} options.sourceText 原pathの現在の内容
|
|
105
153
|
* @param {object} options.candidate `lattice.bounded_seam_candidate.v2`
|
|
106
|
-
* @param {object} options.symbolExtents symbol名 -> `{startLine, endLine}`(1始まり・両端含む)
|
|
154
|
+
* @param {object} options.symbolExtents symbol名 -> `{startLine, endLine, isExported}`(1始まり・両端含む)
|
|
155
|
+
* @param {object} options.importSurface `joinImportSurface`の結果(`{statements, unassigned}`)
|
|
107
156
|
* @returns {{files: object|null, reasons: string[]}} pathごとの変換後text
|
|
108
157
|
*/
|
|
109
|
-
export function planSeamRewrite({ sourceText, candidate, symbolExtents } = {}) {
|
|
158
|
+
export function planSeamRewrite({ sourceText, candidate, symbolExtents, importSurface } = {}) {
|
|
110
159
|
if (typeof sourceText !== 'string' || sourceText.length === 0) return fail(['empty_source']);
|
|
111
160
|
const lines = sourceText.split('\n');
|
|
112
161
|
const surfaces = candidate?.surfaces ?? [];
|
|
@@ -124,8 +173,12 @@ export function planSeamRewrite({ sourceText, candidate, symbolExtents } = {}) {
|
|
|
124
173
|
|| extent.endLine < extent.startLine) {
|
|
125
174
|
return fail([`symbol_extent_missing:${symbol}`]);
|
|
126
175
|
}
|
|
176
|
+
// export状態はAST事実として要求する。無ければ推測せず止める(確実の門)。
|
|
177
|
+
if (typeof extent.isExported !== 'boolean') {
|
|
178
|
+
return fail([`symbol_export_status_missing:${symbol}`]);
|
|
179
|
+
}
|
|
127
180
|
blocks.push({
|
|
128
|
-
symbol, path: surface.path, end: extent.endLine,
|
|
181
|
+
symbol, path: surface.path, end: extent.endLine, exported: extent.isExported,
|
|
129
182
|
start: extendUpward(lines, extent.startLine),
|
|
130
183
|
});
|
|
131
184
|
}
|
|
@@ -138,7 +191,40 @@ export function planSeamRewrite({ sourceText, candidate, symbolExtents } = {}) {
|
|
|
138
191
|
}
|
|
139
192
|
}
|
|
140
193
|
|
|
141
|
-
|
|
194
|
+
// import面はsensorの観測から。観測が無い・束縛の帰属が決まらない・importが先頭block
|
|
195
|
+
// の外にある——いずれも「たぶん大丈夫」で進まず、typed理由でAIへ渡す。
|
|
196
|
+
if (importSurface === null || typeof importSurface !== 'object'
|
|
197
|
+
|| !Array.isArray(importSurface.statements)) {
|
|
198
|
+
return fail(['import_surface_missing']);
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(importSurface.unassigned) && importSurface.unassigned.length > 0) {
|
|
201
|
+
return fail(importSurface.unassigned.map((name) => `import_binding_unassigned:${name}`));
|
|
202
|
+
}
|
|
203
|
+
const statements = [];
|
|
204
|
+
let cursor = 1;
|
|
205
|
+
for (const entry of [...importSurface.statements]
|
|
206
|
+
.sort((left, right) => left.startLine - right.startLine)) {
|
|
207
|
+
if (!Number.isSafeInteger(entry.startLine) || !Number.isSafeInteger(entry.endLine)
|
|
208
|
+
|| entry.startLine < 1 || entry.endLine > lines.length || entry.endLine < entry.startLine
|
|
209
|
+
|| !Array.isArray(entry.bindings)) {
|
|
210
|
+
return fail(['import_surface_missing']);
|
|
211
|
+
}
|
|
212
|
+
if (entry.startLine < cursor) return fail([`import_statement_ambiguous:${entry.startLine}`]);
|
|
213
|
+
for (let line = cursor; line < entry.startLine; line += 1) {
|
|
214
|
+
const text = lines[line - 1];
|
|
215
|
+
if (text.trim() !== '' && !COMMENT_LINE.test(text)) {
|
|
216
|
+
// 先頭block外のimport(ESMでは合法)は、残余headerの組み立てが機械では確実に
|
|
217
|
+
// できない。整形で解こうとせず止める。
|
|
218
|
+
return fail([`import_below_header:${entry.startLine}`]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
statements.push({
|
|
222
|
+
text: lines.slice(entry.startLine - 1, entry.endLine).join('\n'),
|
|
223
|
+
bindings: entry.bindings.filter((name) => typeof name === 'string' && name !== ''),
|
|
224
|
+
});
|
|
225
|
+
cursor = entry.endLine + 1;
|
|
226
|
+
}
|
|
227
|
+
const endIndex = cursor - 2;
|
|
142
228
|
if (blocks.some((block) => block.start <= endIndex + 1)) {
|
|
143
229
|
return fail(['symbol_inside_import_block']);
|
|
144
230
|
}
|
|
@@ -152,7 +238,7 @@ export function planSeamRewrite({ sourceText, candidate, symbolExtents } = {}) {
|
|
|
152
238
|
const raw = lines.slice(block.start - 1, block.end).join('\n');
|
|
153
239
|
if (!bodyByPath.has(block.path)) bodyByPath.set(block.path, []);
|
|
154
240
|
bodyByPath.get(block.path).push(exportedBlock(raw));
|
|
155
|
-
if (
|
|
241
|
+
if (block.exported) {
|
|
156
242
|
if (!reExportByPath.has(block.path)) reExportByPath.set(block.path, []);
|
|
157
243
|
reExportByPath.get(block.path).push(block.symbol);
|
|
158
244
|
}
|
|
@@ -23,6 +23,10 @@ const sortedUnique = (values) => [...new Set(values)].sort(compareText);
|
|
|
23
23
|
* 分割で外部の消費者が影響を受けるかは、原pathの公開面が変わったかで決まる。原pathが
|
|
24
24
|
* 同じ名前を同じだけ出し続けるなら、原pathをimportしている側は一行も変わらない
|
|
25
25
|
* (ADR 0137 Decision 3)。
|
|
26
|
+
*
|
|
27
|
+
* **ESM構文(`export function` / `export {}`)を正規表現で読むJS/TS限定の検査である。**
|
|
28
|
+
* 他言語のsourceに対しては公開名を1つも読めず、比較は実質空になる——「検証済み」を
|
|
29
|
+
* 主張しない(ADR 0145)。言語非依存の網は切断参照の計数(`severed`観測)が担う。
|
|
26
30
|
*/
|
|
27
31
|
export function readExportSurface(text) {
|
|
28
32
|
if (typeof text !== 'string') return [];
|
|
@@ -149,19 +153,38 @@ const CONDITIONS = Object.freeze([
|
|
|
149
153
|
* @param {{preserved: boolean, missing: string[]}} options.exportSurface 公開面の比較
|
|
150
154
|
* @param {boolean} options.focusedTestsPassed 変換後worktreeでのfocused test結果
|
|
151
155
|
* @param {boolean} options.sensorFresh 変換後の再indexが新pathを収載したか
|
|
156
|
+
* @param {{observed: boolean, entries: Array<{file: string, name: string}>}|null} options.severed
|
|
157
|
+
* 切断参照の観測(ADR 0145)。`null`はfresh indexへ到達しなかった時で、その場合は
|
|
158
|
+
* sensor_freshが先に落ちる。`observed: false`は観測の失敗であり「切断なし」へ丸めない。
|
|
152
159
|
* @param {{targetResolved: boolean, before: number, after: number}} options.conflictPairs 競合対の増減
|
|
153
160
|
* @param {{before: number|null, after: number|null}} options.waves 実行段階数の増減
|
|
161
|
+
*
|
|
162
|
+
* behavior_equivalentは2つの観測の合成である。(1) export面の比較——**ESM構文を正規表現で
|
|
163
|
+
* 読むのでJS/TS限定**。他言語のsourceに対しては常にpreservedを返す実質空の検査であり、
|
|
164
|
+
* 「検証済み」を主張しない。(2) 切断参照の計数——fresh indexのsymbol一覧に立つので、
|
|
165
|
+
* sensorが抽出できる言語なら効く。網の言語非依存部はこちらが担う。
|
|
154
166
|
*/
|
|
155
167
|
export function evaluateSeamVerification(options = {}) {
|
|
156
168
|
const {
|
|
157
|
-
exportSurface, focusedTestsPassed, sensorFresh, conflictPairs, waves,
|
|
169
|
+
exportSurface, focusedTestsPassed, sensorFresh, conflictPairs, waves, severed = null,
|
|
158
170
|
} = options;
|
|
159
171
|
const detail = {};
|
|
160
172
|
const failures = [];
|
|
161
173
|
|
|
162
|
-
|
|
174
|
+
const surfacePreserved = exportSurface?.preserved === true;
|
|
175
|
+
// severedがnullのままここへ来るのはsensor_freshが落ちる経路だけ。fresh indexに到達したのに
|
|
176
|
+
// 観測が組めなかった(observed: false)は、切断なしではなく観測の欠落として落とす。
|
|
177
|
+
const severedObserved = severed === null || severed.observed === true;
|
|
178
|
+
const severedEntries = severed?.entries ?? [];
|
|
179
|
+
detail.behavior_equivalent = surfacePreserved && severedObserved && severedEntries.length === 0;
|
|
163
180
|
if (!detail.behavior_equivalent) {
|
|
164
|
-
|
|
181
|
+
if (!surfacePreserved) {
|
|
182
|
+
failures.push(`behavior_equivalent:${(exportSurface?.missing ?? []).join(',') || 'unknown'}`);
|
|
183
|
+
}
|
|
184
|
+
if (!severedObserved) failures.push('behavior_equivalent:severed_observation_missing');
|
|
185
|
+
for (const entry of severedEntries) {
|
|
186
|
+
failures.push(`behavior_equivalent:severed_reference:${entry.file}:${entry.name}`);
|
|
187
|
+
}
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
detail.focused_tests_passed = focusedTestsPassed === true;
|
package/src/sensor-adapter.mjs
CHANGED
|
@@ -333,9 +333,14 @@ async function collectOne({ cwd, query, execute, inspectAffectedPath }) {
|
|
|
333
333
|
operation,
|
|
334
334
|
target,
|
|
335
335
|
cwd,
|
|
336
|
+
// callers/calleesはCLI既定の20件で黙って切られる。fanoutの大きい関数では
|
|
337
|
+
// module値への参照(valueRef)が窓の外へ落ち、閉包が「完結した」と誤認する
|
|
338
|
+
// ——観測の打ち切りを「無い」へ丸めない。明示limitで引く。
|
|
336
339
|
args: operation === 'status'
|
|
337
340
|
? ['status', '.', '--json']
|
|
338
|
-
: [
|
|
341
|
+
: ['callers', 'callees'].includes(operation)
|
|
342
|
+
? [operation, target, '--path', '.', '--limit', '200', '--json']
|
|
343
|
+
: [operation, target, '--path', '.', '--json'],
|
|
339
344
|
};
|
|
340
345
|
const result = await executeCommand(execute, command);
|
|
341
346
|
if (result.code !== 0) {
|
package/src/todo-cli.mjs
CHANGED
|
@@ -861,6 +861,56 @@ async function independence({ repoRoot, requestedPlanKey }) {
|
|
|
861
861
|
return result;
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
/**
|
|
865
|
+
* 切断コストの内訳を投影する(read-only、docs/plan_seam-cost.md)。
|
|
866
|
+
*
|
|
867
|
+
* 係争fileについて「何を共有しているから単純に切れないのか」を数えられる事実として出す。
|
|
868
|
+
* task別のsymbol帰属はwitness setのconcern_anchorsから取る——係争資源に対する宣言は
|
|
869
|
+
* そこに既に在り、ここで発明しない。閾値も可否判定も返さない。
|
|
870
|
+
*
|
|
871
|
+
* 投影であって記録ではない。sensorが進めば変わる値なので、artifactへ焼き込まず
|
|
872
|
+
* stdoutへ返すだけにする(ADR 0127のindependence記録と同じ線)。
|
|
873
|
+
*/
|
|
874
|
+
async function seamProfile({ repoRoot, planKey, filePath }) {
|
|
875
|
+
const witnessSet = await readTodoWitnessSet({ repoRoot, planKey });
|
|
876
|
+
if (witnessSet === null) {
|
|
877
|
+
throw new TodoStoreError('SEAM_PROFILE_UNAVAILABLE', 'witness_set_absent', undefined, {
|
|
878
|
+
plan_key: planKey, next_action: 'declare_witness_set',
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const ownedSymbolsByTask = {};
|
|
882
|
+
for (const [taskId, witness] of Object.entries(witnessSet.manual_witness ?? {})) {
|
|
883
|
+
const symbols = (witness.concern_anchors ?? [])
|
|
884
|
+
.filter((anchor) => anchor.within?.target === filePath)
|
|
885
|
+
.flatMap((anchor) => anchor.symbols);
|
|
886
|
+
if (symbols.length > 0) ownedSymbolsByTask[taskId] = [...symbols].sort();
|
|
887
|
+
}
|
|
888
|
+
if (Object.keys(ownedSymbolsByTask).length < 2) {
|
|
889
|
+
// 帰属が2 task未満なら「切断のコスト」という問いが立たない。宣言が無いことを
|
|
890
|
+
// 空の内訳へ丸めず、何を書けば観測できるかを返す(ADR 0130の案内規律)。
|
|
891
|
+
throw new TodoStoreError('SEAM_PROFILE_UNAVAILABLE', 'concern_anchors_below_two_tasks', undefined, {
|
|
892
|
+
plan_key: planKey, file: filePath,
|
|
893
|
+
next_action: 'declare_concern_anchors_for_contested_file',
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
let sourceText;
|
|
897
|
+
try {
|
|
898
|
+
sourceText = await readFile(path.join(repoRoot, filePath), 'utf8');
|
|
899
|
+
} catch {
|
|
900
|
+
throw new TodoStoreError('SEAM_PROFILE_UNAVAILABLE', 'contested_file_unreadable', undefined, {
|
|
901
|
+
file: filePath,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
const { computeSeamCostProfile } = await import('./seam-cost.mjs');
|
|
905
|
+
const { profile, reasons } = await computeSeamCostProfile({
|
|
906
|
+
repoRoot, sourcePath: filePath, sourceText, ownedSymbolsByTask,
|
|
907
|
+
});
|
|
908
|
+
if (profile === null) {
|
|
909
|
+
throw new TodoStoreError('SEAM_PROFILE_UNAVAILABLE', reasons[0] ?? 'profile_unavailable');
|
|
910
|
+
}
|
|
911
|
+
return profile;
|
|
912
|
+
}
|
|
913
|
+
|
|
864
914
|
async function seamProposalCompile({ repoRoot, planKey }) {
|
|
865
915
|
requireCleanWorktree(repoRoot);
|
|
866
916
|
const currentBaseSha = currentHeadSha(repoRoot);
|
|
@@ -1811,6 +1861,11 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
1811
1861
|
pathNames: await readSeamPathNames(repoRoot, argv[5]),
|
|
1812
1862
|
land: true,
|
|
1813
1863
|
});
|
|
1864
|
+
} else if ((argv.length === 5 || argv.length === 6) && argv[0] === 'seam-profile'
|
|
1865
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
1866
|
+
&& argv[3] === '--file' && isTodoRef(argv[4])
|
|
1867
|
+
&& (argv.length === 5 || argv[5] === '--json')) {
|
|
1868
|
+
action = (repoRoot) => seamProfile({ repoRoot, planKey: argv[2], filePath: argv[4] });
|
|
1814
1869
|
} else if (argv.length === 4 && argv[0] === 'seam-proposal' && argv[1] === 'compile'
|
|
1815
1870
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])) {
|
|
1816
1871
|
action = (repoRoot) => seamProposalCompile({ repoRoot, planKey: argv[3] });
|