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.
@@ -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
- id: 'soft-contract',
138
- severity: 'warn',
139
- message: `Layers classify files but have no deny/allow rule edges: ${withoutRules.join(', ')}. Soft green — peer leaks may go unchecked.`,
140
- evidence: withoutRules.map((n) => `layer:${n}`),
141
- fix: 'Add rules via /ark-contract (or a policy pack) so every populated layer participates in enforcement.',
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
- id: 'io-under-application',
150
- severity: 'warn',
151
- message: falseGreen.message,
152
- evidence: (falseGreen.ioPaths || []).slice(0, 12),
153
- fix: falseGreen.fix,
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
- id: 'io-under-application',
214
- severity: 'warn',
215
- message: `Application-layer files import database/client SDKs directly (${ioUnderAppFiles.length} file(s)). Prefer ports in Domain + adapters outside Application.`,
216
- evidence: ioUnderAppFiles.slice(0, 12),
217
- fix: 'Extract a port + adapter (extraction card); do not weaken ark.config to silence the smell.',
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
- id: 'handler-in-persistence',
224
- severity: 'warn',
225
- message: `HTTP/route handler shape found under persistence/repository paths (${handlerInPersist.length} file(s)) — semantic false-green risk.`,
226
- evidence: handlerInPersist.slice(0, 12),
227
- fix: 'Move handlers to Presentation/API; keep Persistence as data access only (/ark-explore shape-focus).',
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
- id: 'god-module',
234
- severity: 'warn',
235
- message: `God-module candidates: large files with wide export surfaces (${godEvidence.length} file(s), ≥${GOD_LOC} LOC and ≥${GOD_EXPORTS} exports).`,
236
- evidence: godEvidence.slice(0, 12),
237
- fix: 'Split by concern with a pilot cluster; keep gate rules; use dual-plan B extraction card.',
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
- id: 'domain-logic-in-ui',
244
- severity: 'warn',
245
- message: `Business-style can*/calculate*/compute* helpers live under UI/presentation paths (${domainInUi.length} file(s)).`,
246
- evidence: domainInUi.slice(0, 12),
247
- fix: 'Move pure rules into Domain (or shared pure module under Domain globs) and import from UI.',
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
- id: 'facade-sql-in-routes',
254
- severity: 'warn',
255
- message: `Route/controller files import ORM/SQL clients directly (${facadeSql.length} file(s)).`,
256
- evidence: facadeSql.slice(0, 12),
257
- fix: 'Relocate query bytes into a repository/adapter; routes call a port — extraction card; no schema rewrite.',
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
- id: 'mixed-pattern-cluster',
270
- severity: 'info',
271
- message:
272
- 'Concurrent design patterns detected in the tree (slice features vs flat services vs hex folders). Pick a golden pattern and pilot migrate-on-touch.',
273
- evidence,
274
- fix: 'Run /ark-explore shape-focus; mark golden vs legacy; dual-plan B with pilot + kill-switch.',
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
- return smells;
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
  /**
@@ -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 or /ark-autopilot for pattern bets never treat empty plan A as healthy finished.'
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
- line(' ', color.dim(`[${smell.id}] ${smell.message}`));
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 (designFitness.designWeak) {
550
- actions.push(
551
- 'shape residual: /ark-explore (shape-focus) or /ark-autopilot dual-plan B — pattern bets are never mechanical-safe'
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
- if (actions.length === 0) {
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
- const uniqueActions = [...new Set(actions.filter(Boolean))];
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
+ }