arkgate 3.0.2 → 3.0.4
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 +63 -1
- package/README.md +30 -2
- package/bin/ark-check.mjs +10 -0
- package/bin/ark-mcp.mjs +14 -7
- package/bin/lib/ai-velocity.mjs +293 -0
- package/bin/lib/ci-and-commands.mjs +8 -6
- package/bin/lib/design-smells.mjs +122 -58
- package/bin/lib/doctor-plan.mjs +104 -7
- package/bin/lib/golden-pattern.mjs +184 -0
- package/bin/lib/html-report-depth.mjs +282 -0
- package/bin/lib/html-report.mjs +214 -21
- package/bin/lib/pilot-loop.mjs +266 -0
- package/bin/lib/post-green-path.mjs +79 -0
- package/bin/lib/prepare-write.mjs +2 -0
- package/bin/lib/skill-install.mjs +2 -1
- package/bin/lib/write-path-detect.mjs +25 -14
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/agent-guide.md +62 -10
- package/docs/brownfield-adoption.md +22 -1
- package/docs/package-surface.md +5 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +7 -0
- package/templates/skills/ark-explain.md +34 -3
- package/templates/skills/ark-explore.md +15 -5
- package/templates/skills/ark-place.md +4 -1
|
@@ -21,6 +21,51 @@ export const DESIGN_SMELL_IDS = Object.freeze([
|
|
|
21
21
|
'soft-contract',
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Q02 — outcome-oriented human language per smell id (newbie / vibecoder).
|
|
26
|
+
* Stable `id`s never change; technical detail stays in `message`.
|
|
27
|
+
*/
|
|
28
|
+
export const DESIGN_SMELL_OUTCOMES = Object.freeze({
|
|
29
|
+
'io-under-application':
|
|
30
|
+
'Business/application code reaches the database or external APIs directly — the AI will keep pasting I/O into the wrong place. Put data access behind a port/adapter.',
|
|
31
|
+
'handler-in-persistence':
|
|
32
|
+
'HTTP handlers live under data/repository folders — names look like “storage” but they are routes. Move handlers to the API/UI layer so the AI stops mixing transport and storage.',
|
|
33
|
+
'god-module':
|
|
34
|
+
'A few huge files own too many responsibilities — the AI cannot safely edit one concern without breaking others. Split the pilot file by job (one export surface per concern).',
|
|
35
|
+
'domain-logic-in-ui':
|
|
36
|
+
'Business rules (can*/calculate*/policy) sit in UI components — the AI will duplicate them in pages. Move pure rules into Domain (or a pure domain module) and import from the UI.',
|
|
37
|
+
'facade-sql-in-routes':
|
|
38
|
+
'Routes/controllers import the ORM or SQL client — the AI will keep growing “smart controllers.” Keep queries in a repository/adapter; routes only call that port.',
|
|
39
|
+
'mixed-pattern-cluster':
|
|
40
|
+
'The repo mixes several layout styles (features vs services vs hex folders) — the AI does not know where new code goes. Pick one golden pattern and migrate one pilot cluster on touch.',
|
|
41
|
+
'soft-contract':
|
|
42
|
+
'Some layers have files but almost no deny rules — the gate looks green while peers can still import freely. Add real layer rules so the AI has hard walls, not soft suggestions.',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** @param {string} id */
|
|
46
|
+
export function outcomeForSmellId(id) {
|
|
47
|
+
return (
|
|
48
|
+
DESIGN_SMELL_OUTCOMES[id] ||
|
|
49
|
+
'This design residual confuses where the AI should put new code. Clarify one home for this kind of change (see evidence paths) without weakening the gate.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {{ id: string, severity?: string, message: string, evidence?: string[], fix?: string }} partial
|
|
55
|
+
*/
|
|
56
|
+
export function makeDesignSmell(partial) {
|
|
57
|
+
const id = partial.id;
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
severity: partial.severity ?? 'warn',
|
|
61
|
+
message: partial.message,
|
|
62
|
+
/** Plain-language outcome (Q02); prefer this for human doctor lines. */
|
|
63
|
+
outcome: outcomeForSmellId(id),
|
|
64
|
+
evidence: partial.evidence ?? [],
|
|
65
|
+
fix: partial.fix ?? '',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
const IO_IMPORT_RE =
|
|
25
70
|
/\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm|typeorm|knex|mongodb|pg|mysql2|better-sqlite3|ioredis|redis)['"]|require\(\s*['"](?:@?prisma\/client|pg|knex|typeorm)/;
|
|
26
71
|
const HANDLER_CONTENT_RE =
|
|
@@ -133,25 +178,29 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
|
|
|
133
178
|
? coverage.layersWithoutRules
|
|
134
179
|
: [];
|
|
135
180
|
if (withoutRules.length > 0) {
|
|
136
|
-
smells.push(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
smells.push(
|
|
182
|
+
makeDesignSmell({
|
|
183
|
+
id: 'soft-contract',
|
|
184
|
+
severity: 'warn',
|
|
185
|
+
message: `Layers classify files but have no deny/allow rule edges: ${withoutRules.join(', ')}. Soft green — peer leaks may go unchecked.`,
|
|
186
|
+
evidence: withoutRules.map((n) => `layer:${n}`),
|
|
187
|
+
fix: 'Add rules via /ark-contract (or a policy pack) so every populated layer participates in enforcement.',
|
|
188
|
+
})
|
|
189
|
+
);
|
|
143
190
|
}
|
|
144
191
|
|
|
145
192
|
// Classic false-green I/O under Application (reuse detector when coverage present)
|
|
146
193
|
const falseGreen = detectContractFalseGreenRisk(resolvedRoot, config, coverage ?? {});
|
|
147
194
|
if (falseGreen?.risk) {
|
|
148
|
-
smells.push(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
195
|
+
smells.push(
|
|
196
|
+
makeDesignSmell({
|
|
197
|
+
id: 'io-under-application',
|
|
198
|
+
severity: 'warn',
|
|
199
|
+
message: falseGreen.message,
|
|
200
|
+
evidence: (falseGreen.ioPaths || []).slice(0, 12),
|
|
201
|
+
fix: falseGreen.fix,
|
|
202
|
+
})
|
|
203
|
+
);
|
|
155
204
|
}
|
|
156
205
|
|
|
157
206
|
const godEvidence = [];
|
|
@@ -209,53 +258,63 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
|
|
|
209
258
|
}
|
|
210
259
|
|
|
211
260
|
if (!falseGreen?.risk && ioUnderAppFiles.length > 0) {
|
|
212
|
-
smells.push(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
261
|
+
smells.push(
|
|
262
|
+
makeDesignSmell({
|
|
263
|
+
id: 'io-under-application',
|
|
264
|
+
severity: 'warn',
|
|
265
|
+
message: `Application-layer files import database/client SDKs directly (${ioUnderAppFiles.length} file(s)). Prefer ports in Domain + adapters outside Application.`,
|
|
266
|
+
evidence: ioUnderAppFiles.slice(0, 12),
|
|
267
|
+
fix: 'Extract a port + adapter (extraction card); do not weaken ark.config to silence the smell.',
|
|
268
|
+
})
|
|
269
|
+
);
|
|
219
270
|
}
|
|
220
271
|
|
|
221
272
|
if (handlerInPersist.length > 0) {
|
|
222
|
-
smells.push(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
273
|
+
smells.push(
|
|
274
|
+
makeDesignSmell({
|
|
275
|
+
id: 'handler-in-persistence',
|
|
276
|
+
severity: 'warn',
|
|
277
|
+
message: `HTTP/route handler shape found under persistence/repository paths (${handlerInPersist.length} file(s)) — semantic false-green risk.`,
|
|
278
|
+
evidence: handlerInPersist.slice(0, 12),
|
|
279
|
+
fix: 'Move handlers to Presentation/API; keep Persistence as data access only (/ark-explore shape-focus).',
|
|
280
|
+
})
|
|
281
|
+
);
|
|
229
282
|
}
|
|
230
283
|
|
|
231
284
|
if (godEvidence.length > 0) {
|
|
232
|
-
smells.push(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
285
|
+
smells.push(
|
|
286
|
+
makeDesignSmell({
|
|
287
|
+
id: 'god-module',
|
|
288
|
+
severity: 'warn',
|
|
289
|
+
message: `God-module candidates: large files with wide export surfaces (${godEvidence.length} file(s), ≥${GOD_LOC} LOC and ≥${GOD_EXPORTS} exports).`,
|
|
290
|
+
evidence: godEvidence.slice(0, 12),
|
|
291
|
+
fix: 'Split by concern with a pilot cluster; keep gate rules; use dual-plan B extraction card.',
|
|
292
|
+
})
|
|
293
|
+
);
|
|
239
294
|
}
|
|
240
295
|
|
|
241
296
|
if (domainInUi.length > 0) {
|
|
242
|
-
smells.push(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
297
|
+
smells.push(
|
|
298
|
+
makeDesignSmell({
|
|
299
|
+
id: 'domain-logic-in-ui',
|
|
300
|
+
severity: 'warn',
|
|
301
|
+
message: `Business-style can*/calculate*/compute* helpers live under UI/presentation paths (${domainInUi.length} file(s)).`,
|
|
302
|
+
evidence: domainInUi.slice(0, 12),
|
|
303
|
+
fix: 'Move pure rules into Domain (or shared pure module under Domain globs) and import from UI.',
|
|
304
|
+
})
|
|
305
|
+
);
|
|
249
306
|
}
|
|
250
307
|
|
|
251
308
|
if (facadeSql.length > 0) {
|
|
252
|
-
smells.push(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
309
|
+
smells.push(
|
|
310
|
+
makeDesignSmell({
|
|
311
|
+
id: 'facade-sql-in-routes',
|
|
312
|
+
severity: 'warn',
|
|
313
|
+
message: `Route/controller files import ORM/SQL clients directly (${facadeSql.length} file(s)).`,
|
|
314
|
+
evidence: facadeSql.slice(0, 12),
|
|
315
|
+
fix: 'Relocate query bytes into a repository/adapter; routes call a port — extraction card; no schema rewrite.',
|
|
316
|
+
})
|
|
317
|
+
);
|
|
259
318
|
}
|
|
260
319
|
|
|
261
320
|
// mixed-pattern: vertical-slice features coexisting with flat services and/or hex folders
|
|
@@ -265,20 +324,25 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
|
|
|
265
324
|
if (hasFeaturesLayout) evidence.push('layout:features/*');
|
|
266
325
|
if (hasFlatServices) evidence.push('layout:services/*');
|
|
267
326
|
if (hasHexPorts) evidence.push('layout:hex-domain-application-infra');
|
|
268
|
-
smells.push(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
327
|
+
smells.push(
|
|
328
|
+
makeDesignSmell({
|
|
329
|
+
id: 'mixed-pattern-cluster',
|
|
330
|
+
severity: 'info',
|
|
331
|
+
message:
|
|
332
|
+
'Concurrent design patterns detected in the tree (slice features vs flat services vs hex folders). Pick a golden pattern and pilot migrate-on-touch.',
|
|
333
|
+
evidence,
|
|
334
|
+
fix: 'Run /ark-explore shape-focus; mark golden vs legacy; dual-plan B with pilot + kill-switch.',
|
|
335
|
+
})
|
|
336
|
+
);
|
|
276
337
|
}
|
|
277
338
|
|
|
278
339
|
// Stable order by id for snapshots
|
|
279
340
|
const order = new Map(DESIGN_SMELL_IDS.map((id, i) => [id, i]));
|
|
280
341
|
smells.sort((a, b) => (order.get(a.id) ?? 99) - (order.get(b.id) ?? 99));
|
|
281
|
-
|
|
342
|
+
// Ensure every smell carries outcome (defensive for partial callers).
|
|
343
|
+
return smells.map((s) =>
|
|
344
|
+
s.outcome ? s : makeDesignSmell({ id: s.id, severity: s.severity, message: s.message, evidence: s.evidence, fix: s.fix })
|
|
345
|
+
);
|
|
282
346
|
}
|
|
283
347
|
|
|
284
348
|
/**
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
summarizeDesignFitness,
|
|
32
32
|
isDesignWeak,
|
|
33
33
|
} from './design-smells.mjs';
|
|
34
|
+
import {
|
|
35
|
+
buildPostGreenNextAction,
|
|
36
|
+
mergePostGreenTopActions,
|
|
37
|
+
isDoctorHealthyNothingToDo,
|
|
38
|
+
} from './post-green-path.mjs';
|
|
39
|
+
import { loadGoldenPattern, summarizeGoldenPattern } from './golden-pattern.mjs';
|
|
40
|
+
import { summarizePilotLoop } from './pilot-loop.mjs';
|
|
34
41
|
|
|
35
42
|
const color = {
|
|
36
43
|
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
@@ -221,6 +228,12 @@ export function buildRemediationPlan(
|
|
|
221
228
|
governedPercent,
|
|
222
229
|
totalFiles,
|
|
223
230
|
});
|
|
231
|
+
// Q04 — single next pilot extraction card (one at a time → re-doctor).
|
|
232
|
+
const pilotLoop = summarizePilotLoop({
|
|
233
|
+
designWeak,
|
|
234
|
+
patternBets,
|
|
235
|
+
designSmells,
|
|
236
|
+
});
|
|
224
237
|
|
|
225
238
|
let statement =
|
|
226
239
|
activeViolations.length > 0
|
|
@@ -263,6 +276,8 @@ export function buildRemediationPlan(
|
|
|
263
276
|
// Additive: pattern evolution bets derived from design smells (never auto).
|
|
264
277
|
patternBets,
|
|
265
278
|
designSmells,
|
|
279
|
+
// Q04: one-pilot loop step (extraction card); never mechanical-safe.
|
|
280
|
+
pilotLoop,
|
|
266
281
|
};
|
|
267
282
|
}
|
|
268
283
|
|
|
@@ -318,6 +333,21 @@ export function runPlan(
|
|
|
318
333
|
console.log(color.dim(` success: ${bet.successSignal}`));
|
|
319
334
|
}
|
|
320
335
|
}
|
|
336
|
+
// Q04 — single next pilot (one at a time → re-doctor).
|
|
337
|
+
if (plan.pilotLoop?.active && plan.pilotLoop.nextPilot) {
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(color.bold('Next pilot (one at a time → re-doctor)'));
|
|
340
|
+
const np = plan.pilotLoop.nextPilot;
|
|
341
|
+
console.log(` Pilot: ${np.pilotTarget || np.pilot} [${np.smellId}]`);
|
|
342
|
+
console.log(color.dim(` Move: ${np.move}`));
|
|
343
|
+
console.log(color.dim(` Success: ${np.successSignal}`));
|
|
344
|
+
console.log(color.dim(` Kill-switch: ${np.killSwitch}`));
|
|
345
|
+
console.log(
|
|
346
|
+
color.dim(
|
|
347
|
+
' Apply this ONE pilot, then ark-check --doctor — never multi-pilot batch; never mechanical-safe.'
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
321
351
|
if (activeViolations.length === 0) return plan;
|
|
322
352
|
console.log('');
|
|
323
353
|
console.log(
|
|
@@ -383,6 +413,17 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
383
413
|
governedPercent: cov.governed.percent,
|
|
384
414
|
totalFiles: cov.governed.totalFiles,
|
|
385
415
|
});
|
|
416
|
+
// Q01 — single post-green door when design-weak (map → B; no skill shopping).
|
|
417
|
+
const postGreenPath = buildPostGreenNextAction(designFitness);
|
|
418
|
+
// Q03 — optional golden pattern for NEW code (advisory; never clears design-weak).
|
|
419
|
+
const goldenPattern = summarizeGoldenPattern(loadGoldenPattern(root));
|
|
420
|
+
// Q04 — one next pilot (extraction card) when design-weak.
|
|
421
|
+
const patternBetsForLoop = buildPatternBetsFromSmells(designSmells);
|
|
422
|
+
const pilotLoop = summarizePilotLoop({
|
|
423
|
+
designWeak: designFitness.designWeak,
|
|
424
|
+
patternBets: patternBetsForLoop,
|
|
425
|
+
designSmells,
|
|
426
|
+
});
|
|
386
427
|
|
|
387
428
|
if (asJson) {
|
|
388
429
|
console.log(
|
|
@@ -407,6 +448,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
407
448
|
// Path-correct ENFORCE can still be design-weak (P02).
|
|
408
449
|
designFitness,
|
|
409
450
|
designSmells,
|
|
451
|
+
// Q01: primary next action when Shape residual dominates (null if not design-weak).
|
|
452
|
+
postGreenPath,
|
|
453
|
+
...(postGreenPath
|
|
454
|
+
? {
|
|
455
|
+
primaryNextAction: postGreenPath.action,
|
|
456
|
+
healthyFinishedForbidden: true,
|
|
457
|
+
}
|
|
458
|
+
: {}),
|
|
459
|
+
// Q03: advisory golden for new-code placement (absent = no claim).
|
|
460
|
+
goldenPattern,
|
|
461
|
+
// Q04: one-pilot loop (extraction card → re-doctor).
|
|
462
|
+
pilotLoop,
|
|
410
463
|
governed: cov.governed,
|
|
411
464
|
emptyLayers: cov.emptyLayers,
|
|
412
465
|
layersWithoutRules: cov.layersWithoutRules,
|
|
@@ -523,7 +576,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
523
576
|
modeMark,
|
|
524
577
|
`${modeTitle} — ${
|
|
525
578
|
designFitness.designWeak
|
|
526
|
-
? 'Guard on edges is honest, but design smells remain (Shape residual). You do not pick this mode. Next: /ark-explore dual-plan B
|
|
579
|
+
? 'Guard on edges is honest, but design smells remain (Shape residual). You do not pick this mode. Next: single path — /ark-explore shape-focus → dual-plan B, then /ark-autopilot only to apply B with your OK. Never empty plan A = done.'
|
|
527
580
|
: modeHelp[mode]
|
|
528
581
|
}`
|
|
529
582
|
);
|
|
@@ -541,18 +594,52 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
541
594
|
} else {
|
|
542
595
|
line(designFitness.designWeak ? warn : warn, designFitness.label);
|
|
543
596
|
for (const smell of designSmells.slice(0, 5)) {
|
|
544
|
-
|
|
597
|
+
// Q02: outcome-first (plain language); technical message stays in JSON + dim detail.
|
|
598
|
+
const outcome = smell.outcome || smell.message;
|
|
599
|
+
line(' ', `[${smell.id}] ${outcome}`);
|
|
600
|
+
if (smell.outcome && smell.message && smell.message !== smell.outcome) {
|
|
601
|
+
line(' ', color.dim(`detail: ${smell.message}`));
|
|
602
|
+
}
|
|
545
603
|
if (smell.evidence?.length) {
|
|
546
604
|
line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`));
|
|
547
605
|
}
|
|
548
606
|
}
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
|
|
607
|
+
if (postGreenPath) {
|
|
608
|
+
// Rank first via mergePostGreenTopActions at the end (Q01 single door).
|
|
609
|
+
actions.push(postGreenPath.action);
|
|
610
|
+
}
|
|
611
|
+
// Q04 — surface one next pilot under design-weak.
|
|
612
|
+
if (pilotLoop?.active && pilotLoop.nextPilot) {
|
|
613
|
+
const np = pilotLoop.nextPilot;
|
|
614
|
+
line(
|
|
615
|
+
warn,
|
|
616
|
+
`Next pilot (one at a time): ${np.pilotTarget || np.pilot} [${np.smellId}] → re-doctor after change`
|
|
552
617
|
);
|
|
618
|
+
line(' ', color.dim(`success: ${np.successSignal}`));
|
|
619
|
+
line(' ', color.dim('never multi-pilot batch; patternBets never mechanical-safe'));
|
|
553
620
|
}
|
|
554
621
|
}
|
|
555
622
|
|
|
623
|
+
// Q03 — optional golden pattern note (advisory for new code only).
|
|
624
|
+
if (goldenPattern.present) {
|
|
625
|
+
console.log('');
|
|
626
|
+
console.log(color.bold('Golden pattern (new code)'));
|
|
627
|
+
line(
|
|
628
|
+
ok,
|
|
629
|
+
`"${goldenPattern.name}" — ${goldenPattern.norm}` +
|
|
630
|
+
(goldenPattern.newCodeHome ? ` Prefer: ${goldenPattern.newCodeHome}.` : '') +
|
|
631
|
+
' Advisory only — does not clear design-weak or replace the gate.'
|
|
632
|
+
);
|
|
633
|
+
} else if (goldenPattern.invalid) {
|
|
634
|
+
console.log('');
|
|
635
|
+
console.log(color.bold('Golden pattern (new code)'));
|
|
636
|
+
line(
|
|
637
|
+
warn,
|
|
638
|
+
`${goldenPattern.path} is present but invalid (${goldenPattern.error || 'invalid'}). ` +
|
|
639
|
+
'Fix or remove it — absence is fine; a bad file is not guidance.'
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
556
643
|
console.log('');
|
|
557
644
|
console.log(color.bold('Coverage'));
|
|
558
645
|
const govMark =
|
|
@@ -776,11 +863,21 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
776
863
|
}
|
|
777
864
|
|
|
778
865
|
console.log('');
|
|
779
|
-
|
|
866
|
+
const uniqueActions = mergePostGreenTopActions(actions, postGreenPath);
|
|
867
|
+
if (isDoctorHealthyNothingToDo(designFitness, uniqueActions)) {
|
|
780
868
|
console.log(color.green('✔ Healthy — nothing to do.'));
|
|
781
869
|
} else {
|
|
782
|
-
|
|
870
|
+
if (designFitness.designWeak && uniqueActions.length === 0 && postGreenPath) {
|
|
871
|
+
uniqueActions.push(postGreenPath.action);
|
|
872
|
+
}
|
|
783
873
|
console.log(color.bold(`Top actions (${uniqueActions.length}):`));
|
|
784
874
|
uniqueActions.forEach((action, index) => console.log(` ${index + 1}. ${action}`));
|
|
875
|
+
if (postGreenPath) {
|
|
876
|
+
console.log(
|
|
877
|
+
color.dim(
|
|
878
|
+
' (post-green path is primary when ENFORCE · design-weak — do not skill-shop explore vs coverage vs think)'
|
|
879
|
+
)
|
|
880
|
+
);
|
|
881
|
+
}
|
|
785
882
|
}
|
|
786
883
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q03 — optional golden pattern artifact for *new* code guidance.
|
|
3
|
+
*
|
|
4
|
+
* Path: `.ark/golden-pattern.json` (side-car under existing local-state convention).
|
|
5
|
+
* Absent is normal. Presence is advisory only — never ENFORCE, never clears design-weak.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Relative path from project root (stable contract). */
|
|
11
|
+
export const GOLDEN_PATTERN_REL = '.ark/golden-pattern.json';
|
|
12
|
+
|
|
13
|
+
export const GOLDEN_PATTERN_SCHEMA_VERSION = '1';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* schemaVersion?: string,
|
|
18
|
+
* name: string,
|
|
19
|
+
* norm: string,
|
|
20
|
+
* newCodeHome?: string,
|
|
21
|
+
* examplePath?: string,
|
|
22
|
+
* }} GoldenPattern
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* ok: boolean,
|
|
28
|
+
* present: boolean,
|
|
29
|
+
* path: string,
|
|
30
|
+
* golden?: GoldenPattern,
|
|
31
|
+
* invalid?: boolean,
|
|
32
|
+
* error?: string,
|
|
33
|
+
* }} GoldenPatternLoadResult
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load optional golden pattern from the consumer tree.
|
|
38
|
+
* Never throws. Malformed → invalid, not present, ok:false.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} root
|
|
41
|
+
* @returns {GoldenPatternLoadResult}
|
|
42
|
+
*/
|
|
43
|
+
export function loadGoldenPattern(root) {
|
|
44
|
+
const rel = GOLDEN_PATTERN_REL;
|
|
45
|
+
if (typeof root !== 'string' || !root) {
|
|
46
|
+
return { ok: true, present: false, path: rel };
|
|
47
|
+
}
|
|
48
|
+
const abs = path.join(root, '.ark', 'golden-pattern.json');
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
52
|
+
return { ok: true, present: false, path: rel };
|
|
53
|
+
}
|
|
54
|
+
raw = fs.readFileSync(abs, 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
present: false,
|
|
59
|
+
invalid: true,
|
|
60
|
+
error: 'unreadable',
|
|
61
|
+
path: rel,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let data;
|
|
66
|
+
try {
|
|
67
|
+
data = JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
present: false,
|
|
72
|
+
invalid: true,
|
|
73
|
+
error: 'invalid-json',
|
|
74
|
+
path: rel,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
present: false,
|
|
82
|
+
invalid: true,
|
|
83
|
+
error: 'not-object',
|
|
84
|
+
path: rel,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const name = typeof data.name === 'string' ? data.name.trim() : '';
|
|
89
|
+
const norm = typeof data.norm === 'string' ? data.norm.trim() : '';
|
|
90
|
+
if (!name || !norm) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
present: false,
|
|
94
|
+
invalid: true,
|
|
95
|
+
error: 'missing-name-or-norm',
|
|
96
|
+
path: rel,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @type {GoldenPattern} */
|
|
101
|
+
const golden = {
|
|
102
|
+
schemaVersion:
|
|
103
|
+
typeof data.schemaVersion === 'string' && data.schemaVersion.trim()
|
|
104
|
+
? data.schemaVersion.trim()
|
|
105
|
+
: GOLDEN_PATTERN_SCHEMA_VERSION,
|
|
106
|
+
name,
|
|
107
|
+
norm,
|
|
108
|
+
};
|
|
109
|
+
if (typeof data.newCodeHome === 'string' && data.newCodeHome.trim()) {
|
|
110
|
+
golden.newCodeHome = data.newCodeHome.trim();
|
|
111
|
+
}
|
|
112
|
+
if (typeof data.examplePath === 'string' && data.examplePath.trim()) {
|
|
113
|
+
golden.examplePath = data.examplePath.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { ok: true, present: true, path: rel, golden };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One-line guidance for agents / placement notes.
|
|
121
|
+
* @param {GoldenPatternLoadResult} result
|
|
122
|
+
* @returns {string | null}
|
|
123
|
+
*/
|
|
124
|
+
export function formatGoldenPatternNote(result) {
|
|
125
|
+
if (!result?.present || !result.golden) return null;
|
|
126
|
+
const g = result.golden;
|
|
127
|
+
let s = `Golden pattern (advisory for NEW code only): "${g.name}" — ${g.norm}`;
|
|
128
|
+
if (g.newCodeHome) s += ` Prefer new files under ${g.newCodeHome}.`;
|
|
129
|
+
if (g.examplePath) s += ` Example: ${g.examplePath}.`;
|
|
130
|
+
s += ' Does not clear design-weak or replace the gate.';
|
|
131
|
+
return s;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compact JSON-safe summary for doctor / prepare_write.
|
|
136
|
+
* @param {GoldenPatternLoadResult} result
|
|
137
|
+
*/
|
|
138
|
+
export function summarizeGoldenPattern(result) {
|
|
139
|
+
if (!result) {
|
|
140
|
+
return { present: false, path: GOLDEN_PATTERN_REL };
|
|
141
|
+
}
|
|
142
|
+
if (result.invalid) {
|
|
143
|
+
return {
|
|
144
|
+
present: false,
|
|
145
|
+
path: result.path || GOLDEN_PATTERN_REL,
|
|
146
|
+
invalid: true,
|
|
147
|
+
error: result.error || 'invalid',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (!result.present || !result.golden) {
|
|
151
|
+
return { present: false, path: result.path || GOLDEN_PATTERN_REL };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
present: true,
|
|
155
|
+
path: result.path || GOLDEN_PATTERN_REL,
|
|
156
|
+
name: result.golden.name,
|
|
157
|
+
norm: result.golden.norm,
|
|
158
|
+
...(result.golden.newCodeHome ? { newCodeHome: result.golden.newCodeHome } : {}),
|
|
159
|
+
...(result.golden.examplePath ? { examplePath: result.golden.examplePath } : {}),
|
|
160
|
+
advisoryOnly: true,
|
|
161
|
+
doesNotClearDesignWeak: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Attach golden guidance to a placement object (ark_place / prepare_write).
|
|
167
|
+
* @param {object} placement
|
|
168
|
+
* @param {GoldenPatternLoadResult} goldenResult
|
|
169
|
+
*/
|
|
170
|
+
export function attachGoldenToPlacement(placement, goldenResult) {
|
|
171
|
+
if (!placement || typeof placement !== 'object') return placement;
|
|
172
|
+
if (placement.error) return placement;
|
|
173
|
+
|
|
174
|
+
const summary = summarizeGoldenPattern(goldenResult);
|
|
175
|
+
const note = formatGoldenPatternNote(goldenResult);
|
|
176
|
+
const next = {
|
|
177
|
+
...placement,
|
|
178
|
+
goldenPattern: summary,
|
|
179
|
+
};
|
|
180
|
+
if (note) {
|
|
181
|
+
next.note = placement.note ? `${placement.note} ${note}` : note;
|
|
182
|
+
}
|
|
183
|
+
return next;
|
|
184
|
+
}
|