@hegemonart/get-design-done 1.52.0 → 1.53.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +49 -0
- package/README.md +2 -0
- package/agents/design-context-reviewer-gate.md +102 -0
- package/agents/design-context-reviewer.md +186 -0
- package/dist/claude-code/.claude/skills/discover/SKILL.md +7 -1
- package/dist/claude-code/.claude/skills/explore/SKILL.md +3 -1
- package/package.json +1 -1
- package/scripts/lib/explore-parallel-runner/index.ts +58 -0
- package/scripts/lib/explore-parallel-runner/types.ts +58 -0
- package/scripts/lib/manifest/skills.json +2 -2
- package/scripts/lib/mappers/compute-batches.mjs +625 -0
- package/scripts/lib/mappers/graph-adjacency.mjs +129 -0
- package/scripts/lib/mappers/incremental-discover.cjs +617 -0
- package/scripts/lib/mappers/incremental-discover.d.cts +133 -0
- package/scripts/lib/mappers/neighbor-map.mjs +0 -0
- package/sdk/cli/index.js +369 -2
- package/sdk/fingerprint/classify.cjs +406 -0
- package/sdk/fingerprint/index.ts +405 -0
- package/sdk/fingerprint/store.cjs +523 -0
- package/sdk/index.ts +1 -0
- package/skills/discover/SKILL.md +7 -1
- package/skills/explore/SKILL.md +3 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* sdk/fingerprint/classify.cjs — Phase 53 (Semantic Mapper Engine), FP-02.
|
|
4
|
+
*
|
|
5
|
+
* The incremental-update CLASSIFIER. Given a pre-computed array of per-node
|
|
6
|
+
* fingerprint-compare results (see `compareFingerprints` in
|
|
7
|
+
* `sdk/fingerprint/index.ts`, which yields NONE | COSMETIC | STRUCTURAL) and a
|
|
8
|
+
* project-shape snapshot, decide HOW MUCH of the design context must be re-mapped
|
|
9
|
+
* on this cycle:
|
|
10
|
+
*
|
|
11
|
+
* SKIP — nothing structural changed; reuse the prior context.
|
|
12
|
+
* PARTIAL_UPDATE — a few nodes changed; re-map only the affected batches.
|
|
13
|
+
* ARCHITECTURE_UPDATE — a modest, dir-reshaping change; re-map + re-batch.
|
|
14
|
+
* FULL_UPDATE — a large or restructuring change (or a bootstrap with no
|
|
15
|
+
* prior baseline); re-map everything.
|
|
16
|
+
*
|
|
17
|
+
* This module is the one `.cjs` exception the ROADMAP names so that a `.cjs` CLI
|
|
18
|
+
* or skill can `require()` it directly. It is DEP-FREE and PURE: it consumes a
|
|
19
|
+
* `compareResults` array (NOT fingerprint internals) and never calls the
|
|
20
|
+
* fingerprint engine, so it is fully independent of `sdk/fingerprint/index.ts`.
|
|
21
|
+
* It performs NO filesystem access — directory shape is derived from node-id
|
|
22
|
+
* provenance carried in `projectStats`, never from an FS re-walk.
|
|
23
|
+
*
|
|
24
|
+
* Determinism is a hard contract (D6): no `Math.random`, no `Date.now`, sorted
|
|
25
|
+
* accumulation of `affectedBatchHints`. Identical inputs → identical output on
|
|
26
|
+
* win32 / Linux / macOS.
|
|
27
|
+
*
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* INPUT / OUTPUT CONTRACT (for executor E's wiring at the discover/explore boundary)
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
*
|
|
32
|
+
* classify(compareResults, projectStats) → result
|
|
33
|
+
*
|
|
34
|
+
* compareResults: Array<{ id: string, type?: string, change: 'NONE'|'COSMETIC'|'STRUCTURAL' }>
|
|
35
|
+
* One entry per fingerprinted node that exists in BOTH the prior and the
|
|
36
|
+
* current context, plus add/remove which `compareFingerprints` already maps
|
|
37
|
+
* to STRUCTURAL. `change` is the authoritative signal; `type` (component |
|
|
38
|
+
* token | motion | ...) is carried through for downstream batching but does
|
|
39
|
+
* not affect the action decision. An EMPTY array means "no prior baseline
|
|
40
|
+
* signal" (bootstrap / first run) → FULL_UPDATE (documented below).
|
|
41
|
+
*
|
|
42
|
+
* projectStats: {
|
|
43
|
+
* totalFiles: number, // count of file-nodes in the CURRENT context
|
|
44
|
+
* prevDirShape?: DirShape|null, // null/absent ⇒ bootstrap (no prior baseline)
|
|
45
|
+
* currDirShape?: DirShape|null,
|
|
46
|
+
* thresholds?: Partial<Thresholds> // inline override (lower precedence than config)
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* DirShape = {
|
|
50
|
+
* dirs: string[], // top-level design dirs (derived from node-id provenance)
|
|
51
|
+
* counts: Record<string, number>, // file-node count per top-level dir
|
|
52
|
+
* layerHist: Record<string, number> // Atomic-layer histogram (atom/molecule/organism/…)
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* result: {
|
|
56
|
+
* action: 'SKIP'|'PARTIAL_UPDATE'|'ARCHITECTURE_UPDATE'|'FULL_UPDATE',
|
|
57
|
+
* structuralCount: number, // # of STRUCTURAL entries
|
|
58
|
+
* pct: number, // structuralCount / totalFiles (0 when totalFiles===0)
|
|
59
|
+
* dirChanged: boolean, // any top-level dir added/removed/renamed, or per-dir count delta
|
|
60
|
+
* majorRestructure: boolean, // see DECISION MATRIX below
|
|
61
|
+
* affectedBatchHints: string[], // sorted ids of STRUCTURAL-changed inputs
|
|
62
|
+
* reason: string, // human-readable trigger (advisory; stable text)
|
|
63
|
+
* thresholds: Thresholds // the effective thresholds actually used
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* ---------------------------------------------------------------------------
|
|
67
|
+
* DECISION MATRIX (top-down, FIRST match wins)
|
|
68
|
+
* ---------------------------------------------------------------------------
|
|
69
|
+
* (0) totalFiles === 0 → SKIP (guard: no divide-by-zero, nothing to map)
|
|
70
|
+
* (B) prevDirShape == null (no prior baseline) → FULL_UPDATE (bootstrap; empty compareResults lands here)
|
|
71
|
+
* (1) structuralCount === 0 → SKIP (cosmetic-only / no-op vs a KNOWN baseline)
|
|
72
|
+
* (2) structuralCount > fullFileCount
|
|
73
|
+
* || pct > fullPct
|
|
74
|
+
* || majorRestructure → FULL_UPDATE
|
|
75
|
+
* (3) 0 < pct <= archPctMax && dirChanged → ARCHITECTURE_UPDATE
|
|
76
|
+
* (4) else → PARTIAL_UPDATE
|
|
77
|
+
*
|
|
78
|
+
* BOOTSTRAP rule (no prior baseline): when `prevDirShape` is null/absent (a
|
|
79
|
+
* first run has no baseline to diff against) and there ARE files to map, we
|
|
80
|
+
* cannot trust `pct` or `dirChanged` to be meaningful, so we classify
|
|
81
|
+
* FULL_UPDATE. An EMPTY `compareResults` is the canonical bootstrap signal and
|
|
82
|
+
* likewise yields FULL_UPDATE (the caller treats the whole context as new). The
|
|
83
|
+
* bootstrap check (B) therefore sits ABOVE the structuralCount===0 SKIP (1) —
|
|
84
|
+
* otherwise an empty compare array would be misread as "nothing changed". Only
|
|
85
|
+
* the totalFiles===0 guard (0) wins over bootstrap: a project with zero
|
|
86
|
+
* file-nodes has nothing to map even on its first run.
|
|
87
|
+
*
|
|
88
|
+
* majorRestructure is TRUE when ANY of:
|
|
89
|
+
* (a) a top-level design dir was added, removed, or renamed
|
|
90
|
+
* (set difference of prevDirShape.dirs vs currDirShape.dirs is non-empty);
|
|
91
|
+
* (b) the Atomic-layer histogram shifts > 30% (relative) in ANY bucket;
|
|
92
|
+
* (c) > 25% of the file-nodes changed their owning top-level dir, approximated
|
|
93
|
+
* deterministically by the net per-dir count churn over total files.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Thresholds — defaults overridable from `.design/config.json#incremental`
|
|
98
|
+
// (config is read by the CALLER and threaded in via projectStats.thresholds,
|
|
99
|
+
// or merged here when a caller hands us the raw config object). We keep the
|
|
100
|
+
// merge logic here so every consumer agrees on precedence + clamping.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const DEFAULT_THRESHOLDS = Object.freeze({
|
|
104
|
+
fullFileCount: 30, // > this many STRUCTURAL file changes ⇒ FULL_UPDATE
|
|
105
|
+
fullPct: 0.5, // structural fraction > this ⇒ FULL_UPDATE
|
|
106
|
+
archPctMax: 0.3, // structural fraction at-or-below this (with dirChanged) ⇒ ARCHITECTURE
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Relative-shift fraction for an Atomic-layer bucket that counts as "major". */
|
|
110
|
+
const LAYER_SHIFT_MAJOR = 0.3;
|
|
111
|
+
/** Fraction of file-nodes that must change owning dir to count as "major". */
|
|
112
|
+
const DIR_REOWN_MAJOR = 0.25;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Coerce + clamp a thresholds object. Unknown keys are ignored; non-finite or
|
|
116
|
+
* out-of-range values fall back to the default for that key. `fullFileCount` is
|
|
117
|
+
* a non-negative integer; the two pcts are clamped to [0, 1].
|
|
118
|
+
*
|
|
119
|
+
* @param {Partial<typeof DEFAULT_THRESHOLDS>|null|undefined} raw
|
|
120
|
+
* @returns {typeof DEFAULT_THRESHOLDS}
|
|
121
|
+
*/
|
|
122
|
+
function normalizeThresholds(raw) {
|
|
123
|
+
const t = { ...DEFAULT_THRESHOLDS };
|
|
124
|
+
if (!raw || typeof raw !== 'object') return t;
|
|
125
|
+
|
|
126
|
+
if (Number.isFinite(raw.fullFileCount) && raw.fullFileCount >= 0) {
|
|
127
|
+
t.fullFileCount = Math.floor(raw.fullFileCount);
|
|
128
|
+
}
|
|
129
|
+
if (Number.isFinite(raw.fullPct) && raw.fullPct >= 0) {
|
|
130
|
+
t.fullPct = Math.min(1, raw.fullPct);
|
|
131
|
+
}
|
|
132
|
+
if (Number.isFinite(raw.archPctMax) && raw.archPctMax >= 0) {
|
|
133
|
+
t.archPctMax = Math.min(1, raw.archPctMax);
|
|
134
|
+
}
|
|
135
|
+
return t;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the effective thresholds. Precedence (highest first):
|
|
140
|
+
* 1. `projectStats.thresholds` — explicit inline override
|
|
141
|
+
* 2. `projectStats.config.incremental` — a raw `.design/config.json` object,
|
|
142
|
+
* if a caller hands the whole config through instead of pre-extracting it
|
|
143
|
+
* 3. DEFAULT_THRESHOLDS
|
|
144
|
+
*
|
|
145
|
+
* @param {object} projectStats
|
|
146
|
+
* @returns {typeof DEFAULT_THRESHOLDS}
|
|
147
|
+
*/
|
|
148
|
+
function resolveThresholds(projectStats) {
|
|
149
|
+
const ps = projectStats && typeof projectStats === 'object' ? projectStats : {};
|
|
150
|
+
if (ps.thresholds && typeof ps.thresholds === 'object') {
|
|
151
|
+
return normalizeThresholds(ps.thresholds);
|
|
152
|
+
}
|
|
153
|
+
if (ps.config && typeof ps.config === 'object' && ps.config.incremental) {
|
|
154
|
+
return normalizeThresholds(ps.config.incremental);
|
|
155
|
+
}
|
|
156
|
+
return { ...DEFAULT_THRESHOLDS };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Dir-shape diffing — all derived from node-id provenance carried in
|
|
161
|
+
// projectStats; NO filesystem walk. A "DirShape" is { dirs, counts, layerHist }.
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/** Safe array of strings from an arbitrary value. */
|
|
165
|
+
function strList(v) {
|
|
166
|
+
return Array.isArray(v) ? v.filter((s) => typeof s === 'string') : [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Safe Record<string, number> from an arbitrary value. */
|
|
170
|
+
function numMap(v) {
|
|
171
|
+
const out = {};
|
|
172
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
173
|
+
for (const k of Object.keys(v)) {
|
|
174
|
+
if (Number.isFinite(v[k])) out[k] = v[k];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* True when the set of top-level dirs differs between prev and curr (any add,
|
|
182
|
+
* remove, or rename surfaces as a set difference in at least one direction).
|
|
183
|
+
*
|
|
184
|
+
* @param {string[]} prevDirs
|
|
185
|
+
* @param {string[]} currDirs
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
function topLevelDirsChanged(prevDirs, currDirs) {
|
|
189
|
+
const a = new Set(prevDirs);
|
|
190
|
+
const b = new Set(currDirs);
|
|
191
|
+
if (a.size !== b.size) return true;
|
|
192
|
+
for (const d of a) if (!b.has(d)) return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* True when ANY per-dir file-count delta is non-zero. This catches files moving
|
|
198
|
+
* between existing dirs (which leaves `dirs` identical but shifts `counts`).
|
|
199
|
+
*
|
|
200
|
+
* @param {Record<string, number>} prevCounts
|
|
201
|
+
* @param {Record<string, number>} currCounts
|
|
202
|
+
* @returns {boolean}
|
|
203
|
+
*/
|
|
204
|
+
function perDirCountsChanged(prevCounts, currCounts) {
|
|
205
|
+
const keys = new Set([...Object.keys(prevCounts), ...Object.keys(currCounts)]);
|
|
206
|
+
for (const k of keys) {
|
|
207
|
+
if ((prevCounts[k] || 0) !== (currCounts[k] || 0)) return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* True when the Atomic-layer histogram shifts more than LAYER_SHIFT_MAJOR
|
|
214
|
+
* (relative) in ANY bucket. Relative shift for a bucket is
|
|
215
|
+
* |curr - prev| / max(prev, 1) so a bucket going 0→N is a full (>=100%) shift,
|
|
216
|
+
* and we also treat any bucket appearing/disappearing as a major shift.
|
|
217
|
+
*
|
|
218
|
+
* @param {Record<string, number>} prevHist
|
|
219
|
+
* @param {Record<string, number>} currHist
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
function layerHistMajorShift(prevHist, currHist) {
|
|
223
|
+
const keys = new Set([...Object.keys(prevHist), ...Object.keys(currHist)]);
|
|
224
|
+
for (const k of keys) {
|
|
225
|
+
const p = prevHist[k] || 0;
|
|
226
|
+
const c = currHist[k] || 0;
|
|
227
|
+
if (p === 0 && c === 0) continue;
|
|
228
|
+
const rel = Math.abs(c - p) / Math.max(p, 1);
|
|
229
|
+
if (rel > LAYER_SHIFT_MAJOR) return true;
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Deterministic approximation of "> DIR_REOWN_MAJOR of file-nodes changed their
|
|
236
|
+
* owning top-level dir". We do not have per-node before/after dir membership in
|
|
237
|
+
* `compareResults`, so we approximate the re-owning churn from the per-dir count
|
|
238
|
+
* deltas: half the summed absolute count delta is the minimum number of files
|
|
239
|
+
* that must have moved across the dir boundary (each move decrements one dir and
|
|
240
|
+
* increments another, so the summed |delta| double-counts a pure move). We
|
|
241
|
+
* divide by totalFiles to get a fraction. This is monotone in churn and
|
|
242
|
+
* deterministic.
|
|
243
|
+
*
|
|
244
|
+
* @param {Record<string, number>} prevCounts
|
|
245
|
+
* @param {Record<string, number>} currCounts
|
|
246
|
+
* @param {number} totalFiles
|
|
247
|
+
* @returns {boolean}
|
|
248
|
+
*/
|
|
249
|
+
function dirReownMajor(prevCounts, currCounts, totalFiles) {
|
|
250
|
+
if (!(totalFiles > 0)) return false;
|
|
251
|
+
const keys = new Set([...Object.keys(prevCounts), ...Object.keys(currCounts)]);
|
|
252
|
+
let absDelta = 0;
|
|
253
|
+
for (const k of keys) {
|
|
254
|
+
absDelta += Math.abs((currCounts[k] || 0) - (prevCounts[k] || 0));
|
|
255
|
+
}
|
|
256
|
+
const movedApprox = absDelta / 2;
|
|
257
|
+
return movedApprox / totalFiles > DIR_REOWN_MAJOR;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compute { dirChanged, majorRestructure } from the two dir shapes. When there
|
|
262
|
+
* is no prior shape (bootstrap), there is nothing to diff: dirChanged=false and
|
|
263
|
+
* majorRestructure=false (the bootstrap → FULL decision is made separately, on
|
|
264
|
+
* the absence of a baseline, not on a fabricated "everything changed").
|
|
265
|
+
*
|
|
266
|
+
* @param {object|null} prevDirShape
|
|
267
|
+
* @param {object|null} currDirShape
|
|
268
|
+
* @param {number} totalFiles
|
|
269
|
+
* @returns {{ dirChanged: boolean, majorRestructure: boolean }}
|
|
270
|
+
*/
|
|
271
|
+
function diffDirShapes(prevDirShape, currDirShape, totalFiles) {
|
|
272
|
+
if (!prevDirShape || typeof prevDirShape !== 'object') {
|
|
273
|
+
return { dirChanged: false, majorRestructure: false };
|
|
274
|
+
}
|
|
275
|
+
const prevDirs = strList(prevDirShape.dirs);
|
|
276
|
+
const currDirs = strList(currDirShape && currDirShape.dirs);
|
|
277
|
+
const prevCounts = numMap(prevDirShape.counts);
|
|
278
|
+
const currCounts = numMap(currDirShape && currDirShape.counts);
|
|
279
|
+
const prevHist = numMap(prevDirShape.layerHist);
|
|
280
|
+
const currHist = numMap(currDirShape && currDirShape.layerHist);
|
|
281
|
+
|
|
282
|
+
const dirsChanged = topLevelDirsChanged(prevDirs, currDirs);
|
|
283
|
+
const countsChanged = perDirCountsChanged(prevCounts, currCounts);
|
|
284
|
+
const dirChanged = dirsChanged || countsChanged;
|
|
285
|
+
|
|
286
|
+
const majorRestructure =
|
|
287
|
+
dirsChanged ||
|
|
288
|
+
layerHistMajorShift(prevHist, currHist) ||
|
|
289
|
+
dirReownMajor(prevCounts, currCounts, totalFiles);
|
|
290
|
+
|
|
291
|
+
return { dirChanged, majorRestructure };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// classify — the 4-action top-down matrix.
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Classify a cycle's change set into an update action. See the module header
|
|
300
|
+
* for the full input/output contract and decision matrix.
|
|
301
|
+
*
|
|
302
|
+
* @param {Array<{id: string, type?: string, change: string}>} compareResults
|
|
303
|
+
* @param {object} projectStats
|
|
304
|
+
* @returns {{
|
|
305
|
+
* action: string, structuralCount: number, pct: number,
|
|
306
|
+
* dirChanged: boolean, majorRestructure: boolean,
|
|
307
|
+
* affectedBatchHints: string[], reason: string,
|
|
308
|
+
* thresholds: typeof DEFAULT_THRESHOLDS
|
|
309
|
+
* }}
|
|
310
|
+
*/
|
|
311
|
+
function classify(compareResults, projectStats) {
|
|
312
|
+
const results = Array.isArray(compareResults) ? compareResults : [];
|
|
313
|
+
const ps = projectStats && typeof projectStats === 'object' ? projectStats : {};
|
|
314
|
+
const thresholds = resolveThresholds(ps);
|
|
315
|
+
|
|
316
|
+
const totalFiles = Number.isFinite(ps.totalFiles) && ps.totalFiles >= 0 ? ps.totalFiles : 0;
|
|
317
|
+
const hasPriorBaseline = ps.prevDirShape != null && typeof ps.prevDirShape === 'object';
|
|
318
|
+
|
|
319
|
+
// Gather STRUCTURAL ids deterministically (sorted, deduped). Only STRUCTURAL
|
|
320
|
+
// entries become batch hints; NONE/COSMETIC do not trigger a re-map.
|
|
321
|
+
const structuralIds = [];
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
for (const r of results) {
|
|
324
|
+
if (!r || typeof r !== 'object') continue;
|
|
325
|
+
if (r.change === 'STRUCTURAL' && typeof r.id === 'string' && r.id.length) {
|
|
326
|
+
if (!seen.has(r.id)) {
|
|
327
|
+
seen.add(r.id);
|
|
328
|
+
structuralIds.push(r.id);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
structuralIds.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
333
|
+
const structuralCount = structuralIds.length;
|
|
334
|
+
|
|
335
|
+
const pct = totalFiles > 0 ? structuralCount / totalFiles : 0;
|
|
336
|
+
|
|
337
|
+
const { dirChanged, majorRestructure } = diffDirShapes(
|
|
338
|
+
ps.prevDirShape || null,
|
|
339
|
+
ps.currDirShape || null,
|
|
340
|
+
totalFiles,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const base = {
|
|
344
|
+
structuralCount,
|
|
345
|
+
pct,
|
|
346
|
+
dirChanged,
|
|
347
|
+
majorRestructure,
|
|
348
|
+
affectedBatchHints: structuralIds,
|
|
349
|
+
thresholds,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// (0) Empty project guard — nothing to map, no divide-by-zero. This wins over
|
|
353
|
+
// everything: a project with zero file-nodes has nothing to re-map even on a
|
|
354
|
+
// first run, so a bootstrap with no files still SKIPs.
|
|
355
|
+
if (totalFiles === 0) {
|
|
356
|
+
return { ...base, action: 'SKIP', reason: 'no-files' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Bootstrap: there is no prior baseline to diff against (first run / no
|
|
360
|
+
// fingerprint store). We cannot trust `pct` or `dirChanged` to be meaningful,
|
|
361
|
+
// so the whole current context is treated as new ⇒ FULL_UPDATE. This is
|
|
362
|
+
// ALSO the canonical empty-`compareResults` path: a caller with files but no
|
|
363
|
+
// prior fingerprints passes `[]` + no `prevDirShape`, and we map everything.
|
|
364
|
+
// The bootstrap decision is made on the ABSENCE OF A BASELINE, not on the
|
|
365
|
+
// structural count — so it must be checked BEFORE the structuralCount===0 SKIP
|
|
366
|
+
// (an empty compare array would otherwise be misread as "nothing changed").
|
|
367
|
+
if (!hasPriorBaseline) {
|
|
368
|
+
return { ...base, action: 'FULL_UPDATE', reason: 'bootstrap-no-baseline' };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// (1) No structural change against a KNOWN baseline — skip the cycle
|
|
372
|
+
// (cosmetic-only edits or a genuine no-op).
|
|
373
|
+
if (structuralCount === 0) {
|
|
374
|
+
return { ...base, action: 'SKIP', reason: 'no-structural-change' };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// (2) Large or restructuring change ⇒ FULL.
|
|
378
|
+
if (structuralCount > thresholds.fullFileCount) {
|
|
379
|
+
return { ...base, action: 'FULL_UPDATE', reason: 'structural-count-over-threshold' };
|
|
380
|
+
}
|
|
381
|
+
if (pct > thresholds.fullPct) {
|
|
382
|
+
return { ...base, action: 'FULL_UPDATE', reason: 'structural-pct-over-threshold' };
|
|
383
|
+
}
|
|
384
|
+
if (majorRestructure) {
|
|
385
|
+
return { ...base, action: 'FULL_UPDATE', reason: 'major-restructure' };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// (3) Modest, dir-reshaping change ⇒ ARCHITECTURE.
|
|
389
|
+
if (pct > 0 && pct <= thresholds.archPctMax && dirChanged) {
|
|
390
|
+
return { ...base, action: 'ARCHITECTURE_UPDATE', reason: 'dir-reshape-modest' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// (4) Default — a handful of in-place changes ⇒ PARTIAL.
|
|
394
|
+
return { ...base, action: 'PARTIAL_UPDATE', reason: 'partial-in-place' };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
classify,
|
|
399
|
+
// exported for callers that want to pre-validate or display the effective
|
|
400
|
+
// thresholds, and for the test suite.
|
|
401
|
+
normalizeThresholds,
|
|
402
|
+
resolveThresholds,
|
|
403
|
+
DEFAULT_THRESHOLDS,
|
|
404
|
+
LAYER_SHIFT_MAJOR,
|
|
405
|
+
DIR_REOWN_MAJOR,
|
|
406
|
+
};
|