arkgate 4.8.19 → 4.8.21
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/CHANGELOG.md +220 -0
- package/README.md +107 -117
- package/bin/ark-check-runtime.mjs +26 -4
- package/bin/ark-layer-match.mjs +236 -12
- package/bin/ark.mjs +6 -2
- package/bin/lib/analysis-engine.mjs +13 -6
- package/bin/lib/config-contract.mjs +6 -0
- package/bin/lib/diagnostic-catalog.mjs +1 -0
- package/bin/lib/doctor-advisories.mjs +22 -1
- package/bin/lib/doctor-human.mjs +11 -9
- package/bin/lib/doctor-plan.mjs +17 -14
- package/bin/lib/enforcement-honesty.mjs +30 -2
- package/bin/lib/first-run-help.mjs +7 -1
- package/bin/lib/html-report-advisories.mjs +8 -0
- package/bin/lib/html-report-depth.mjs +12 -7
- package/bin/lib/invariant-coverage.mjs +406 -54
- package/bin/lib/managed-upgrade.mjs +59 -14
- package/bin/lib/package-manager.mjs +5 -2
- package/bin/lib/physical-cohesion.mjs +68 -51
- package/bin/lib/pilot-loop.mjs +183 -37
- package/bin/lib/reshape-decisions.mjs +2 -2
- package/bin/lib/rules-under-contract.mjs +40 -4
- package/bin/lib/start-install-recovery.mjs +156 -0
- package/bin/lib/start-preview.mjs +0 -54
- package/bin/lib/upgrade-behavior-changes.mjs +83 -0
- package/bin/lib/upgrade-command.mjs +69 -31
- package/bin/lib/upgrade-outcome.mjs +359 -0
- package/bin/lib/violations.mjs +109 -0
- package/dist/{configTypes-VD0qcubY.d.ts → configTypes-CTELo87-.d.ts} +9 -0
- package/dist/{diagnosticCatalog-D2v0ASN2.d.ts → diagnosticCatalog-B7BXlet6.d.ts} +79 -5
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +35 -4
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +25 -18
- package/dist/index.d.ts +13 -4
- package/dist/index.js +27 -20
- package/dist/nestjs/index.cjs +9 -9
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +9 -9
- package/dist/runtime/index.cjs +18 -11
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +18 -11
- package/dist/{types-D5GT5ZT8.d.ts → types-fQ9-KrBj.d.ts} +1 -1
- package/dist/{types-BSzRy2X1.d.ts → types-yDNEQ603.d.ts} +1 -1
- package/docs/README.md +4 -3
- package/docs/agent-guide.md +15 -7
- package/docs/brownfield-adoption.md +4 -3
- package/docs/configuration.md +46 -8
- package/docs/develop.md +1 -1
- package/docs/diagnostics.md +12 -2
- package/docs/package-surface.md +4 -3
- package/docs/product-voice.md +11 -3
- package/docs/use.md +1 -1
- package/package.json +3 -3
- package/schemas/ark.config.schema.json +14 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +18 -1
- package/templates/agent-skills/ark-architect/SKILL.md +2 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -2
- package/templates/agent-skills/ark-contract/SKILL.md +2 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +3 -1
- package/templates/agent-skills/ark-explain/SKILL.md +3 -1
- package/templates/agent-skills/ark-explore/SKILL.md +15 -1
- package/templates/agent-skills/ark-fix/SKILL.md +2 -0
- package/templates/agent-skills/ark-loop/SKILL.md +2 -0
- package/templates/agent-skills/ark-order/SKILL.md +2 -0
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +2 -0
- package/templates/agent-skills/ark-think/SKILL.md +2 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +25 -10
- package/templates/skills/ark-adopt.md +18 -1
- package/templates/skills/ark-architect.md +2 -0
- package/templates/skills/ark-autopilot.md +6 -2
- package/templates/skills/ark-contract.md +2 -0
- package/templates/skills/ark-coverage.md +3 -1
- package/templates/skills/ark-explain.md +3 -1
- package/templates/skills/ark-explore.md +15 -1
- package/templates/skills/ark-fix.md +2 -0
- package/templates/skills/ark-loop.md +2 -0
- package/templates/skills/ark-order.md +2 -0
- package/templates/skills/ark-place.md +2 -0
- package/templates/skills/ark-runtime.md +2 -0
- package/templates/skills/ark-think.md +2 -0
- package/templates/skills/ark-upgrade.md +25 -10
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* as W01 layer roles: a miss costs a warning line, never a verdict.
|
|
16
16
|
*/
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { layerForRelativePath, sliceIdForPath } from '../ark-layer-match.mjs';
|
|
18
19
|
|
|
19
20
|
/** ADR 0010 D3 — corpus-calibrated, fixed. */
|
|
20
21
|
const CLUSTER_MIN = 40;
|
|
@@ -126,76 +127,92 @@ export function computePhysicalCohesion(root, files) {
|
|
|
126
127
|
};
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
function governedDestinationDir(anchor, concept) {
|
|
131
|
+
return !anchor || anchor === '.' ? concept : `${anchor}/${concept}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function anchorIsConsolidationSubtree(anchor, concept) {
|
|
135
|
+
return String(anchor).split('/').some((part) => part === concept);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function declaredSliceFolders(rules) {
|
|
139
|
+
const out = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
142
|
+
const folders = Array.isArray(rule?.sliceFolders) ? rule.sliceFolders : [];
|
|
143
|
+
for (const folder of folders) {
|
|
144
|
+
if (typeof folder !== 'string' || folder.length === 0 || seen.has(folder.toLowerCase())) continue;
|
|
145
|
+
seen.add(folder.toLowerCase());
|
|
146
|
+
out.push(folder);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function destinationKeepsLayerAndSlice(fromRel, toRel, layers, sliceFolders) {
|
|
153
|
+
return layerForRelativePath(fromRel, layers) === layerForRelativePath(toRel, layers)
|
|
154
|
+
&& sliceIdForPath(fromRel, sliceFolders) === sliceIdForPath(toRel, sliceFolders);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function withheldPilot(concept, note) {
|
|
158
|
+
return { proposed: true, applied: false, neverMechanicalSafe: true, concept, note, nextPilot: null };
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
/**
|
|
130
|
-
* R2 —
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* governed write path (T02 preflight validates every real move there).
|
|
162
|
+
* R2 — proposed reshape pilot (ADR 0010 D4–D7). `to` is `concept/` under the
|
|
163
|
+
* source anchor and is kept only when the generated layer and slice matchers
|
|
164
|
+
* agree. Tooling does not import Kernel preflight. Withheld when every
|
|
165
|
+
* candidate fails.
|
|
135
166
|
*/
|
|
136
|
-
export function computeReshapePilot(cohesion, files, root) {
|
|
167
|
+
export function computeReshapePilot(cohesion, files, root, contract) {
|
|
137
168
|
const top = cohesion?.findings?.[0];
|
|
138
169
|
if (!top) return null;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
// movable anchor sat below the display floor (cross-model review finding).
|
|
170
|
+
const layers = contract?.layers;
|
|
171
|
+
const sliceFolders = declaredSliceFolders(contract?.rules);
|
|
172
|
+
// Full anchor map: the finding's anchors are display-filtered (>= MIRROR_MIN).
|
|
143
173
|
const byAnchor = new Map();
|
|
144
174
|
const relOf = (abs) => path.relative(root, abs).split(path.sep).join('/');
|
|
145
|
-
|
|
175
|
+
const fileList = Array.isArray(files) ? files : [];
|
|
176
|
+
for (const abs of fileList) {
|
|
146
177
|
const rel = relOf(abs);
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
149
|
-
byAnchor.set(
|
|
178
|
+
const ranked = classifyPhysical(rel);
|
|
179
|
+
if (!ranked || ranked.concept !== top.concept) continue;
|
|
180
|
+
byAnchor.set(ranked.anchor, (byAnchor.get(ranked.anchor) ?? 0) + 1);
|
|
150
181
|
}
|
|
151
|
-
const targetDir = `src/features/${top.concept}`;
|
|
152
182
|
const movable = [...byAnchor.entries()]
|
|
153
|
-
.filter(
|
|
154
|
-
([anchor]) =>
|
|
155
|
-
!CONVENTION_ANCHOR_RE.test(`${anchor}/`) &&
|
|
156
|
-
// The consolidation target subtree is DONE, not a source — without
|
|
157
|
-
// this the loop re-proposes the files it just moved, forever
|
|
158
|
-
// (end-to-end pilot-loop finding).
|
|
159
|
-
anchor !== targetDir &&
|
|
160
|
-
!anchor.startsWith(`${targetDir}/`)
|
|
161
|
-
)
|
|
183
|
+
.filter(([anchor]) => !CONVENTION_ANCHOR_RE.test(`${anchor}/`) && !anchorIsConsolidationSubtree(anchor, top.concept))
|
|
162
184
|
.map(([anchor, count]) => ({ path: anchor, files: count }))
|
|
163
185
|
.sort((a, b) => a.files - b.files || (a.path < b.path ? -1 : 1));
|
|
164
186
|
if (movable.length === 0) {
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
return withheldPilot(top.concept, 'Every remaining anchor for this concept is fixed by framework convention or already consolidated — nothing to move; consider the merge-card review instead.');
|
|
188
|
+
}
|
|
189
|
+
const legalAnchors = [];
|
|
190
|
+
for (const anchor of movable) {
|
|
191
|
+
const targetDir = governedDestinationDir(anchor.path, top.concept);
|
|
192
|
+
const legal = fileList.map(relOf).filter((rel) => {
|
|
193
|
+
const ranked = classifyPhysical(rel);
|
|
194
|
+
if (!ranked || ranked.concept !== top.concept || ranked.anchor !== anchor.path) return false;
|
|
195
|
+
const to = `${targetDir}/${rel.split('/').at(-1)}`;
|
|
196
|
+
return destinationKeepsLayerAndSlice(rel, to, layers, sliceFolders);
|
|
197
|
+
}).sort().map((rel) => ({ from: rel, to: `${targetDir}/${rel.split('/').at(-1)}` }));
|
|
198
|
+
if (legal.length > 0) legalAnchors.push({ ...anchor, targetDir, legal });
|
|
199
|
+
}
|
|
200
|
+
if (legalAnchors.length === 0) {
|
|
201
|
+
return withheldPilot(top.concept, 'No governed destination keeps the layer and the slice.');
|
|
173
202
|
}
|
|
174
|
-
// Smallest
|
|
175
|
-
|
|
176
|
-
const pilotAnchor = movable.find((a) => a.files >= 10) ?? movable[movable.length - 1];
|
|
177
|
-
const rels = (Array.isArray(files) ? files : [])
|
|
178
|
-
.map(relOf)
|
|
179
|
-
.filter((rel) => {
|
|
180
|
-
const r = classifyPhysical(rel);
|
|
181
|
-
return r && r.concept === top.concept && r.anchor === pilotAnchor.path;
|
|
182
|
-
})
|
|
183
|
-
.sort();
|
|
184
|
-
const moves = rels.slice(0, MAX_MOVE_SAMPLE).map((rel) => ({
|
|
185
|
-
from: rel,
|
|
186
|
-
to: `${targetDir}/${rel.split('/').at(-1)}`,
|
|
187
|
-
}));
|
|
203
|
+
// Smallest cluster of at least 10 files; otherwise the largest movable anchor.
|
|
204
|
+
const pilotAnchor = legalAnchors.find((a) => a.files >= 10) ?? legalAnchors.at(-1);
|
|
188
205
|
return {
|
|
189
206
|
proposed: true,
|
|
190
207
|
applied: false,
|
|
191
208
|
neverMechanicalSafe: true,
|
|
192
209
|
concept: top.concept,
|
|
193
210
|
nextPilot: {
|
|
194
|
-
pilotTarget: `${top.concept} @ ${pilotAnchor.path} (${pilotAnchor.
|
|
195
|
-
move: `Consolidate the ${top.concept} cluster from ${pilotAnchor.path} under ${targetDir}/ — one anchor only, moves proposed as an architecture change map and validated by the atomic preflight before any write.`,
|
|
196
|
-
moveSample:
|
|
197
|
-
movesTotal:
|
|
198
|
-
successSignal: `re-run doctor: the ${top.concept} cluster count drops and
|
|
211
|
+
pilotTarget: `${top.concept} @ ${pilotAnchor.path} (${pilotAnchor.legal.length} file(s))`,
|
|
212
|
+
move: `Consolidate the ${top.concept} cluster from ${pilotAnchor.path} under ${pilotAnchor.targetDir}/ — one anchor only, same layer and slice, moves proposed as an architecture change map and validated by the atomic preflight before any write.`,
|
|
213
|
+
moveSample: pilotAnchor.legal.slice(0, MAX_MOVE_SAMPLE),
|
|
214
|
+
movesTotal: pilotAnchor.legal.length,
|
|
215
|
+
successSignal: `re-run doctor: the ${top.concept} cluster count drops, layer and slice id unchanged, card still proposed`,
|
|
199
216
|
killSwitch: 'revert this move set; nothing else was touched',
|
|
200
217
|
doNot: [
|
|
201
218
|
'never move files under app/ or pages/ — fixed by framework convention',
|
package/bin/lib/pilot-loop.mjs
CHANGED
|
@@ -120,35 +120,39 @@ export function extractionCardFromBet(bet, preferredFiles) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
123
|
+
* Viable pattern bets, best pilot first (same rank as the historical selector).
|
|
124
|
+
* Mechanical-safe claims and non-production god-module paths are dropped.
|
|
124
125
|
* @param {object[] | null | undefined} patternBets
|
|
125
|
-
* @param {
|
|
126
|
-
* @returns {null | ReturnType<typeof extractionCardFromBet>}
|
|
126
|
+
* @param {object[] | null | undefined} [designSmells]
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
function rankedPatternBets(patternBets, designSmells) {
|
|
129
129
|
let bets = Array.isArray(patternBets) ? [...patternBets] : [];
|
|
130
|
-
if (bets.length === 0 && Array.isArray(
|
|
131
|
-
bets = buildPatternBetsFromSmells(
|
|
130
|
+
if (bets.length === 0 && Array.isArray(designSmells) && designSmells.length) {
|
|
131
|
+
bets = buildPatternBetsFromSmells(designSmells);
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
let best = null;
|
|
136
|
-
let bestScore = -Infinity;
|
|
133
|
+
const ranked = [];
|
|
137
134
|
for (let i = 0; i < bets.length; i++) {
|
|
138
135
|
const bet = bets[i];
|
|
139
136
|
if (!bet || bet.neverMechanicalSafe === false) continue;
|
|
140
|
-
// Skip anything that claims mechanical-safe (honesty).
|
|
141
137
|
if (bet.class === 'mechanical-safe') continue;
|
|
142
138
|
const files = pilotFilesForBet(bet);
|
|
143
139
|
if (files === null) continue;
|
|
144
|
-
|
|
145
|
-
if (sc > bestScore) {
|
|
146
|
-
bestScore = sc;
|
|
147
|
-
best = { bet, files };
|
|
148
|
-
}
|
|
140
|
+
ranked.push({ bet, files, score: scoreBet(bet, i, files) });
|
|
149
141
|
}
|
|
150
|
-
|
|
151
|
-
return
|
|
142
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
143
|
+
return ranked;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Select **one** next pilot from pattern bets (or build bets from smells).
|
|
148
|
+
* @param {object[] | null | undefined} patternBets
|
|
149
|
+
* @param {{ designSmells?: object[] }} [options]
|
|
150
|
+
* @returns {null | ReturnType<typeof extractionCardFromBet>}
|
|
151
|
+
*/
|
|
152
|
+
export function selectNextPilot(patternBets, options = {}) {
|
|
153
|
+
const ranked = rankedPatternBets(patternBets, options.designSmells);
|
|
154
|
+
if (ranked.length === 0) return null;
|
|
155
|
+
return extractionCardFromBet(ranked[0].bet, ranked[0].files);
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
/**
|
|
@@ -174,15 +178,159 @@ export function formatExtractionCard(card) {
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
/**
|
|
177
|
-
*
|
|
181
|
+
* One proposed pilot. The loop does not know designWeak or which sensor
|
|
182
|
+
* produced the card — only this list.
|
|
183
|
+
*
|
|
184
|
+
* @typedef {'pattern-bet' | 'reshape'} PilotSource
|
|
185
|
+
* @typedef {{
|
|
186
|
+
* source: PilotSource,
|
|
187
|
+
* target: string,
|
|
188
|
+
* move: string,
|
|
189
|
+
* moveSample: Array<string | { from: string, to: string }>,
|
|
190
|
+
* successSignal: string,
|
|
191
|
+
* killSwitch: string,
|
|
192
|
+
* bet?: object,
|
|
193
|
+
* files?: string[],
|
|
194
|
+
* doNot?: string[],
|
|
195
|
+
* }} PilotCandidate
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {object} bet
|
|
200
|
+
* @param {string[]} files
|
|
201
|
+
* @returns {PilotCandidate}
|
|
202
|
+
*/
|
|
203
|
+
function candidateFromBet(bet, files) {
|
|
204
|
+
const target =
|
|
205
|
+
(files && files[0]) ||
|
|
206
|
+
(typeof bet.pilot === 'string' ? bet.pilot : null) ||
|
|
207
|
+
'src/**';
|
|
208
|
+
return {
|
|
209
|
+
source: 'pattern-bet',
|
|
210
|
+
target,
|
|
211
|
+
move:
|
|
212
|
+
typeof bet.fix === 'string' && bet.fix.trim()
|
|
213
|
+
? bet.fix.trim()
|
|
214
|
+
: 'Apply one bounded extraction for this smell on pilot paths only',
|
|
215
|
+
moveSample: files || [],
|
|
216
|
+
successSignal:
|
|
217
|
+
typeof bet.successSignal === 'string'
|
|
218
|
+
? bet.successSignal
|
|
219
|
+
: 'Smell evidence paths cleared on pilot without weakening the contract',
|
|
220
|
+
killSwitch:
|
|
221
|
+
typeof bet.killSwitch === 'string'
|
|
222
|
+
? bet.killSwitch
|
|
223
|
+
: 'If pilot increases edge violations without design clarity, stop and re-map with /ark-explore',
|
|
224
|
+
bet,
|
|
225
|
+
files,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {PilotCandidate} candidate
|
|
231
|
+
* @returns {null | ReturnType<typeof extractionCardFromBet>}
|
|
232
|
+
*/
|
|
233
|
+
function extractionCardFromCandidate(candidate) {
|
|
234
|
+
if (!candidate || typeof candidate.target !== 'string' || candidate.target.length === 0) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
if (candidate.source === 'pattern-bet' && candidate.bet) {
|
|
238
|
+
return extractionCardFromBet(candidate.bet, candidate.files);
|
|
239
|
+
}
|
|
240
|
+
const sample = Array.isArray(candidate.moveSample) ? candidate.moveSample : [];
|
|
241
|
+
const evidence = sample
|
|
242
|
+
.map((entry) => (typeof entry === 'string' ? entry : entry?.from))
|
|
243
|
+
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
244
|
+
.slice(0, 8);
|
|
245
|
+
const reshape = candidate.source === 'reshape';
|
|
246
|
+
return {
|
|
247
|
+
id: PILOT_LOOP_ID,
|
|
248
|
+
patternBetId: reshape ? `reshape:${candidate.target}` : `pattern-b:${candidate.target}`,
|
|
249
|
+
smellId: reshape ? 'physical-cohesion' : 'unknown',
|
|
250
|
+
pilot: candidate.target,
|
|
251
|
+
pilotTarget: candidate.target,
|
|
252
|
+
evidence,
|
|
253
|
+
move: candidate.move,
|
|
254
|
+
doNot:
|
|
255
|
+
Array.isArray(candidate.doNot) && candidate.doNot.length
|
|
256
|
+
? [...candidate.doNot]
|
|
257
|
+
: [...DEFAULT_DO_NOT],
|
|
258
|
+
successSignal: candidate.successSignal,
|
|
259
|
+
killSwitch: candidate.killSwitch,
|
|
260
|
+
neverMechanicalSafe: true,
|
|
261
|
+
class: 'judgment',
|
|
262
|
+
loopStep: 'one-pilot',
|
|
263
|
+
reDoctor: 'ark-check --doctor --json',
|
|
264
|
+
rePlan: 'ark-check --plan --json',
|
|
265
|
+
next: reshape
|
|
266
|
+
? '/ark-loop (one reshape pilot) | re-doctor after pilot'
|
|
267
|
+
: '/ark-autopilot (one cluster / one B pilot) | re-doctor after pilot',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Normalize design-weak pattern bets and a proposed reshape pilot into one list.
|
|
273
|
+
* Pattern bets stay in selector rank (best first) and are omitted unless
|
|
274
|
+
* design fitness is design-weak, so that path keeps today's card. A reshape
|
|
275
|
+
* card is appended when physical cohesion proposes one. Doctor builds this
|
|
276
|
+
* list after advisories, so the loop never depends on call order.
|
|
277
|
+
*
|
|
178
278
|
* @param {{
|
|
179
279
|
* designWeak?: boolean,
|
|
180
280
|
* patternBets?: object[],
|
|
181
281
|
* designSmells?: object[],
|
|
182
|
-
* }}
|
|
282
|
+
* }} [designFitness]
|
|
283
|
+
* @param {{
|
|
284
|
+
* physicalCohesion?: {
|
|
285
|
+
* reshapePilot?: { proposed?: boolean, nextPilot?: object | null },
|
|
286
|
+
* },
|
|
287
|
+
* }} [advisories]
|
|
288
|
+
* @returns {PilotCandidate[]}
|
|
183
289
|
*/
|
|
184
|
-
export function
|
|
185
|
-
|
|
290
|
+
export function collectPilotCandidates(designFitness = {}, advisories = {}) {
|
|
291
|
+
/** @type {PilotCandidate[]} */
|
|
292
|
+
const candidates = [];
|
|
293
|
+
if (designFitness?.designWeak === true) {
|
|
294
|
+
for (const ranked of rankedPatternBets(designFitness.patternBets, designFitness.designSmells)) {
|
|
295
|
+
candidates.push(candidateFromBet(ranked.bet, ranked.files));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const reshape = advisories?.physicalCohesion?.reshapePilot;
|
|
299
|
+
const card = reshape?.proposed === true ? reshape.nextPilot : null;
|
|
300
|
+
if (card && typeof card.pilotTarget === 'string' && card.pilotTarget.length > 0) {
|
|
301
|
+
candidates.push({
|
|
302
|
+
source: 'reshape',
|
|
303
|
+
target: card.pilotTarget,
|
|
304
|
+
move:
|
|
305
|
+
typeof card.move === 'string' && card.move.trim()
|
|
306
|
+
? card.move.trim()
|
|
307
|
+
: 'Consolidate one anchor only, then re-doctor before the next card',
|
|
308
|
+
moveSample: Array.isArray(card.moveSample) ? card.moveSample : [],
|
|
309
|
+
successSignal:
|
|
310
|
+
typeof card.successSignal === 'string'
|
|
311
|
+
? card.successSignal
|
|
312
|
+
: 're-run doctor: the cluster count drops, layer and slice id unchanged, card still proposed',
|
|
313
|
+
killSwitch:
|
|
314
|
+
typeof card.killSwitch === 'string'
|
|
315
|
+
? card.killSwitch
|
|
316
|
+
: 'revert this move set; nothing else was touched',
|
|
317
|
+
...(Array.isArray(card.doNot) ? { doNot: card.doNot } : {}),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return candidates;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Doctor/plan JSON summary. Active iff `candidates.length > 0`.
|
|
325
|
+
* Activates exactly one candidate (ADR 0010 one-pilot rule). The rest stay
|
|
326
|
+
* queued. No designWeak parameter.
|
|
327
|
+
*
|
|
328
|
+
* @param {PilotCandidate[]} [candidates]
|
|
329
|
+
*/
|
|
330
|
+
export function summarizePilotLoop(candidates = []) {
|
|
331
|
+
const list = (Array.isArray(candidates) ? candidates : []).filter(
|
|
332
|
+
(candidate) => candidate && typeof candidate.target === 'string' && candidate.target.length > 0
|
|
333
|
+
);
|
|
186
334
|
// Same forbid bits as DESIGN_WEAK_HONESTY_FLAGS (both auto-apply aliases).
|
|
187
335
|
const forbid = {
|
|
188
336
|
multiPilotBatchForbidden: true,
|
|
@@ -190,55 +338,53 @@ export function summarizePilotLoop(opts = {}) {
|
|
|
190
338
|
autoApplyPlanBForbidden: true,
|
|
191
339
|
};
|
|
192
340
|
|
|
193
|
-
if (
|
|
341
|
+
if (list.length === 0) {
|
|
194
342
|
return {
|
|
195
343
|
active: false,
|
|
196
344
|
id: PILOT_LOOP_ID,
|
|
197
|
-
reason: '
|
|
345
|
+
reason: 'no-pilot-candidates',
|
|
198
346
|
oneAtATime: true,
|
|
199
347
|
neverMechanicalSafe: true,
|
|
200
348
|
...forbid,
|
|
201
349
|
};
|
|
202
350
|
}
|
|
203
351
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (!nextPilot) {
|
|
352
|
+
// One pilot, not the list. Later candidates wait for a re-doctor.
|
|
353
|
+
const extractionCard = extractionCardFromCandidate(list[0]);
|
|
354
|
+
if (!extractionCard) {
|
|
208
355
|
return {
|
|
209
356
|
active: false,
|
|
210
357
|
id: PILOT_LOOP_ID,
|
|
211
|
-
reason: 'no-
|
|
358
|
+
reason: 'no-pilot-candidates',
|
|
212
359
|
oneAtATime: true,
|
|
213
360
|
neverMechanicalSafe: true,
|
|
214
361
|
...forbid,
|
|
215
362
|
};
|
|
216
363
|
}
|
|
217
364
|
|
|
218
|
-
const
|
|
219
|
-
const queued = Math.max(0, remaining - 1);
|
|
220
|
-
|
|
365
|
+
const queued = list.length - 1;
|
|
221
366
|
return {
|
|
222
367
|
active: true,
|
|
223
368
|
id: PILOT_LOOP_ID,
|
|
224
369
|
oneAtATime: true,
|
|
225
370
|
neverMechanicalSafe: true,
|
|
226
371
|
...forbid,
|
|
227
|
-
|
|
228
|
-
|
|
372
|
+
source: list[0].source,
|
|
373
|
+
remainingBets: list.length,
|
|
229
374
|
queuedBets: queued,
|
|
230
375
|
...(queued > 0
|
|
231
376
|
? {
|
|
232
|
-
queueNote: `${queued} additional
|
|
377
|
+
queueNote: `${queued} additional pilot candidate(s) stay queued — run the single nextPilot only, then re-doctor before selecting another.`,
|
|
233
378
|
}
|
|
234
379
|
: {}),
|
|
235
|
-
nextPilot,
|
|
380
|
+
nextPilot: extractionCard,
|
|
381
|
+
extractionCard,
|
|
236
382
|
instruction:
|
|
237
383
|
'Apply ONE pilot from nextPilot (extraction card), then re-doctor. ' +
|
|
238
384
|
'Do not multi-pilot batch. patternBets never mechanical-safe. ' +
|
|
239
385
|
'Never silent auto-apply of plan B. ' +
|
|
240
386
|
'Success = reduced smell evidence on pilot paths; residual outside pilot may remain.',
|
|
241
|
-
cardText: formatExtractionCard(
|
|
387
|
+
cardText: formatExtractionCard(extractionCard),
|
|
242
388
|
};
|
|
243
389
|
}
|
|
244
390
|
|
|
@@ -214,7 +214,7 @@ export function computeReshapeDecisionMemory(root, files, today = new Date().toI
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/** Select one actionable finding while respecting explicit current verdicts. */
|
|
217
|
-
export function computeDecisionAwareReshapePilot(cohesion, files, root, analysis) {
|
|
217
|
+
export function computeDecisionAwareReshapePilot(cohesion, files, root, analysis, contract) {
|
|
218
218
|
const findings = Array.isArray(cohesion?.findings) ? cohesion.findings : [];
|
|
219
219
|
if (findings.length === 0) return null;
|
|
220
220
|
const currentByTarget = new Map(
|
|
@@ -224,7 +224,7 @@ export function computeDecisionAwareReshapePilot(cohesion, files, root, analysis
|
|
|
224
224
|
const anchors = analysis.anchorSets.get(finding.concept) ?? [];
|
|
225
225
|
const decision = currentByTarget.get(targetKey(finding.concept, anchors));
|
|
226
226
|
if (decision?.suppressesPilot) continue;
|
|
227
|
-
const pilot = computeReshapePilot({ ...cohesion, findings: [finding] }, files, root);
|
|
227
|
+
const pilot = computeReshapePilot({ ...cohesion, findings: [finding] }, files, root, contract);
|
|
228
228
|
if (!pilot?.nextPilot) return pilot;
|
|
229
229
|
return {
|
|
230
230
|
...pilot,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
9
|
-
import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
|
|
9
|
+
import { evaluateInvariantCoverage, formatCoverageDiscards } from './invariant-coverage.mjs';
|
|
10
10
|
import {
|
|
11
11
|
coverageOptionsFromConfig,
|
|
12
12
|
invariantIdsFromCatalog,
|
|
@@ -138,9 +138,9 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
138
138
|
fileContents: coverageInputs.fileContents,
|
|
139
139
|
testFiles: coverageInputs.testFiles,
|
|
140
140
|
testGlobsMissing: coverageInputs.testGlobsMissing,
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
141
|
+
// Stats shape the green doctor line (discard counts), not only a failing
|
|
142
|
+
// sentence. Roots stay out: this caller drops violations.
|
|
143
|
+
...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
|
|
144
144
|
coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
|
|
145
145
|
});
|
|
146
146
|
const covById = new Map(
|
|
@@ -189,6 +189,9 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
189
189
|
const uncoveredTruncated = Math.max(0, uncoveredAll.length - UNCOVERED_CATALOG_MAX);
|
|
190
190
|
const uncovered = uncoveredAll.slice(0, UNCOVERED_CATALOG_MAX);
|
|
191
191
|
|
|
192
|
+
const symbolEvidence = (coverage.coverage ?? [])
|
|
193
|
+
.filter((row) => typeof row.symbolEvidenceFile === 'string' && row.symbolEvidenceFile.length > 0)
|
|
194
|
+
.map((row) => ({ id: row.invariantId, file: row.symbolEvidenceFile }));
|
|
192
195
|
const coveredAll = (coverage.coverage ?? [])
|
|
193
196
|
.filter((row) => row.covered)
|
|
194
197
|
.map((row) => ({
|
|
@@ -196,6 +199,9 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
196
199
|
layer: row.layer ?? null,
|
|
197
200
|
mode: row.mode ?? null,
|
|
198
201
|
description: row.description ?? null,
|
|
202
|
+
...(typeof row.symbolEvidenceFile === 'string' && row.symbolEvidenceFile.length > 0
|
|
203
|
+
? { symbolEvidenceFile: row.symbolEvidenceFile }
|
|
204
|
+
: {}),
|
|
199
205
|
}));
|
|
200
206
|
const coveredTruncated = Math.max(0, coveredAll.length - COVERED_SAMPLE_MAX);
|
|
201
207
|
const coveredSample = coveredAll.slice(0, COVERED_SAMPLE_MAX);
|
|
@@ -259,6 +265,8 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
259
265
|
uncoveredTruncated,
|
|
260
266
|
coveredSample,
|
|
261
267
|
coveredTruncated,
|
|
268
|
+
symbolEvidence,
|
|
269
|
+
...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
|
|
262
270
|
mergePlanes,
|
|
263
271
|
notAScore: true,
|
|
264
272
|
note: 'ArkRules plane (intra-layer) — counts and catalog, never a score. Green with uncovered residual must say so. Structure sensors are heuristics; invariants are catalog+coverage evidence, not a business runtime.',
|
|
@@ -280,6 +288,27 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Symbol witness paths and discard counts. Printed on green runs too —
|
|
293
|
+
* a passing check used to hide both. Depth and byte caps stay internal;
|
|
294
|
+
* only the counts already in the failure sentence are shown.
|
|
295
|
+
* @param {ReturnType<typeof summarizeRulesUnderContract>|null|undefined} section
|
|
296
|
+
* @returns {string[]}
|
|
297
|
+
*/
|
|
298
|
+
export function formatArkRulesEvidenceLines(section) {
|
|
299
|
+
if (!section || typeof section !== 'object') return [];
|
|
300
|
+
const lines = [];
|
|
301
|
+
const evidence = Array.isArray(section.symbolEvidence) ? section.symbolEvidence : [];
|
|
302
|
+
for (const row of evidence) {
|
|
303
|
+
if (typeof row?.file !== 'string' || row.file.length === 0) continue;
|
|
304
|
+
const id = typeof row.id === 'string' && row.id.length > 0 ? row.id : 'invariant';
|
|
305
|
+
lines.push(`ArkRules: ${id} symbol ${row.file}`);
|
|
306
|
+
}
|
|
307
|
+
const discard = formatCoverageDiscards(section.coverageStats);
|
|
308
|
+
if (discard) lines.push(`ArkRules:${discard}`);
|
|
309
|
+
return lines;
|
|
310
|
+
}
|
|
311
|
+
|
|
283
312
|
/**
|
|
284
313
|
* Compact / details doctor lines. Empty when the map is off — absence is silent.
|
|
285
314
|
* @param {ReturnType<typeof summarizeRulesUnderContract>|null|undefined} section
|
|
@@ -332,6 +361,7 @@ export function formatArkRulesDoctorLines(section) {
|
|
|
332
361
|
else if (typeof section.note === 'string' && /failed/i.test(section.note)) {
|
|
333
362
|
lines.push(`ArkRules: ${section.note}`);
|
|
334
363
|
}
|
|
364
|
+
lines.push(...formatArkRulesEvidenceLines(section));
|
|
335
365
|
return lines;
|
|
336
366
|
}
|
|
337
367
|
|
|
@@ -475,6 +505,7 @@ export function formatRulesUnderContractHtml(section, esc) {
|
|
|
475
505
|
<code>${escape(c.id)}</code>
|
|
476
506
|
<span class="tag">covered</span>
|
|
477
507
|
<span class="dim">· ${escape(c.layer || '?')}</span>
|
|
508
|
+
${c.symbolEvidenceFile ? `<span class="dim">· ${escape(c.symbolEvidenceFile)}</span>` : ''}
|
|
478
509
|
${c.description ? `<div class="msg">${escape(c.description)}</div>` : ''}
|
|
479
510
|
</li>`
|
|
480
511
|
)
|
|
@@ -528,6 +559,11 @@ export function formatRulesUnderContractHtml(section, esc) {
|
|
|
528
559
|
}
|
|
529
560
|
${uncoveredBlock}
|
|
530
561
|
${coveredBlock}
|
|
562
|
+
${
|
|
563
|
+
formatArkRulesEvidenceLines(section)
|
|
564
|
+
.map((line) => `<p class="muted" style="margin-top:.35rem;font-size:.84rem">${escape(line)}</p>`)
|
|
565
|
+
.join('')
|
|
566
|
+
}
|
|
531
567
|
${
|
|
532
568
|
coveredSample.length || uncovered.length
|
|
533
569
|
? `<p class="muted" style="margin-top:.65rem;font-size:.84rem">Covered = catalog evidence found (symbol and/or test title). Not a claim that business semantics are fully proven end-to-end.</p>`
|