@ryuenn3123/agentic-senior-core 3.0.15 → 3.0.16

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.
@@ -7,109 +7,14 @@ import { fileURLToPath } from 'node:url';
7
7
  import { BLUEPRINT_RECOMMENDATIONS } from './constants.mjs';
8
8
  import { ensureDirectory, pathExists, toTitleCase } from './utils.mjs';
9
9
 
10
- export const ARCHITECT_DEFAULT_TOKEN_BUDGET = 900;
11
- export const ARCHITECT_DEFAULT_TIMEOUT_MS = 1500;
12
- export const ARCHITECT_MIN_TOKEN_BUDGET = 200;
13
- export const ARCHITECT_MAX_TOKEN_BUDGET = 4000;
14
- export const ARCHITECT_MIN_TIMEOUT_MS = 200;
15
- export const ARCHITECT_MAX_TIMEOUT_MS = 10000;
16
- export const ARCHITECT_DEFAULT_RESEARCH_MODE = 'snapshot';
17
-
18
- const ARCHITECT_ALLOWED_RESEARCH_MODES = new Set(['snapshot', 'realtime']);
19
- const ARCHITECT_DEFAULT_REALTIME_SIGNAL_ENV_KEY = 'AGENTIC_ARCHITECT_REALTIME_SIGNAL_JSON';
20
- const ARCHITECT_DEFAULT_REALTIME_SIGNAL_PATH_ENV_KEY = 'AGENTIC_ARCHITECT_REALTIME_SIGNAL_PATH';
21
- const ARCHITECT_ALLOWED_PALETTE_ROLES = ['base', 'surface', 'accent', 'success', 'danger'];
22
- const ARCHITECT_ALLOWED_TYPOGRAPHY_SCALES = ['compact', 'balanced', 'expressive'];
23
- const ARCHITECT_ALLOWED_SPACING_PATTERNS = ['compact-grid', 'balanced-grid', 'airy-grid'];
24
- const ARCHITECT_ALLOWED_MOTION_CHARACTERISTICS = [
25
- 'subtle-enter',
26
- 'staggered-reveal',
27
- 'state-feedback',
28
- 'conversion-focus',
29
- 'calm-transition',
30
- ];
31
-
32
10
  const ARCHITECT_MODULE_FILE_PATH = fileURLToPath(import.meta.url);
33
11
  const ARCHITECT_PACKAGE_ROOT = path.resolve(path.dirname(ARCHITECT_MODULE_FILE_PATH), '..', '..');
34
- const ARCHITECT_RESEARCH_SNAPSHOT_FILE_PATH = path.join(
35
- ARCHITECT_PACKAGE_ROOT,
36
- '.agent-context',
37
- 'state',
38
- 'stack-research-snapshot.json'
39
- );
40
-
41
- const FALLBACK_STACK_RESEARCH_SNAPSHOT = {
42
- schemaVersion: '1.0.0',
43
- snapshotId: 'ecosystem-signals-fallback',
44
- generatedAt: '2026-04-18T00:00:00.000Z',
45
- deterministic: true,
46
- sourceName: 'Agentic-Senior-Core internal fallback snapshot',
47
- sourceUrl: 'state://stack-research-snapshot/fallback',
48
- trustedRealtimeSources: [
49
- {
50
- sourceId: 'awwwards-trend-feed',
51
- sourceName: 'Awwwards Trend Feed',
52
- sourceUrl: 'https://www.awwwards.com',
53
- },
54
- ],
55
- stackSignals: [
56
- {
57
- stackFileName: 'typescript.md',
58
- measuredAt: '2026-04-18T00:00:00.000Z',
59
- metrics: { ecosystemMaturity: 0.91, talentAvailability: 0.9, deliveryVelocity: 0.89 },
60
- },
61
- {
62
- stackFileName: 'python.md',
63
- measuredAt: '2026-04-18T00:00:00.000Z',
64
- metrics: { ecosystemMaturity: 0.92, talentAvailability: 0.88, deliveryVelocity: 0.9 },
65
- },
66
- {
67
- stackFileName: 'java.md',
68
- measuredAt: '2026-04-18T00:00:00.000Z',
69
- metrics: { ecosystemMaturity: 0.89, talentAvailability: 0.83, deliveryVelocity: 0.8 },
70
- },
71
- {
72
- stackFileName: 'php.md',
73
- measuredAt: '2026-04-18T00:00:00.000Z',
74
- metrics: { ecosystemMaturity: 0.79, talentAvailability: 0.75, deliveryVelocity: 0.84 },
75
- },
76
- {
77
- stackFileName: 'go.md',
78
- measuredAt: '2026-04-18T00:00:00.000Z',
79
- metrics: { ecosystemMaturity: 0.84, talentAvailability: 0.78, deliveryVelocity: 0.82 },
80
- },
81
- {
82
- stackFileName: 'csharp.md',
83
- measuredAt: '2026-04-18T00:00:00.000Z',
84
- metrics: { ecosystemMaturity: 0.86, talentAvailability: 0.8, deliveryVelocity: 0.79 },
85
- },
86
- {
87
- stackFileName: 'rust.md',
88
- measuredAt: '2026-04-18T00:00:00.000Z',
89
- metrics: { ecosystemMaturity: 0.74, talentAvailability: 0.63, deliveryVelocity: 0.67 },
90
- },
91
- {
92
- stackFileName: 'ruby.md',
93
- measuredAt: '2026-04-18T00:00:00.000Z',
94
- metrics: { ecosystemMaturity: 0.7, talentAvailability: 0.62, deliveryVelocity: 0.72 },
95
- },
96
- {
97
- stackFileName: 'react-native.md',
98
- measuredAt: '2026-04-18T00:00:00.000Z',
99
- metrics: { ecosystemMaturity: 0.72, talentAvailability: 0.67, deliveryVelocity: 0.74 },
100
- },
101
- {
102
- stackFileName: 'flutter.md',
103
- measuredAt: '2026-04-18T00:00:00.000Z',
104
- metrics: { ecosystemMaturity: 0.75, talentAvailability: 0.69, deliveryVelocity: 0.76 },
105
- },
106
- ],
107
- };
108
12
 
109
13
  const ARCHITECT_PREFERENCE_FILE_PATH = process.env.AGENTIC_ARCHITECT_PREF_FILE
110
14
  ? path.resolve(process.env.AGENTIC_ARCHITECT_PREF_FILE)
111
15
  : path.join(os.homedir(), '.agentic-senior-core', 'architect-preferences.json');
112
16
 
17
+ // Keyword hints — low-confidence bias only, not authoritative research.
113
18
  const STACK_SIGNAL_WEIGHTS = {
114
19
  'typescript.md': [
115
20
  { term: 'typescript', weight: 0.9 },
@@ -205,14 +110,6 @@ const STACK_TRADEOFF_SUMMARIES = {
205
110
  'flutter.md': 'consistent UI across mobile platforms, but ecosystem fit should be checked per package.',
206
111
  };
207
112
 
208
- function clampNumericValue(value, minimumValue, maximumValue) {
209
- return Math.min(Math.max(value, minimumValue), maximumValue);
210
- }
211
-
212
- function estimateTokenUsage(textValue) {
213
- return Math.ceil(String(textValue || '').length / 4);
214
- }
215
-
216
113
  function resolveConfidenceLabel(confidenceScore) {
217
114
  if (confidenceScore >= 0.85) {
218
115
  return 'high';
@@ -234,481 +131,23 @@ function resolveRecommendedBlueprintFileName(stackFileName, blueprintFileNames)
234
131
  return blueprintFileNames[0] || null;
235
132
  }
236
133
 
237
- function normalizeArchitectResearchMode(rawMode) {
238
- const normalizedMode = String(rawMode || ARCHITECT_DEFAULT_RESEARCH_MODE).trim().toLowerCase();
239
- if (ARCHITECT_ALLOWED_RESEARCH_MODES.has(normalizedMode)) {
240
- return normalizedMode;
241
- }
242
-
243
- return ARCHITECT_DEFAULT_RESEARCH_MODE;
244
- }
245
-
246
- function isValidIsoTimestamp(value) {
247
- if (typeof value !== 'string' || value.trim().length === 0) {
248
- return false;
249
- }
250
-
251
- return !Number.isNaN(Date.parse(value));
252
- }
253
-
254
- function toIsoTimestamp(value, fallbackValue = new Date().toISOString()) {
255
- return isValidIsoTimestamp(value) ? new Date(value).toISOString() : fallbackValue;
256
- }
257
-
258
- function loadStackResearchSnapshot() {
259
- try {
260
- if (!existsSync(ARCHITECT_RESEARCH_SNAPSHOT_FILE_PATH)) {
261
- return {
262
- snapshot: FALLBACK_STACK_RESEARCH_SNAPSHOT,
263
- sourcePath: 'fallback://embedded-snapshot',
264
- };
265
- }
266
-
267
- const snapshotContent = readFileSync(ARCHITECT_RESEARCH_SNAPSHOT_FILE_PATH, 'utf8');
268
- const parsedSnapshot = JSON.parse(snapshotContent);
269
-
270
- if (!Array.isArray(parsedSnapshot?.stackSignals) || parsedSnapshot.stackSignals.length === 0) {
271
- return {
272
- snapshot: FALLBACK_STACK_RESEARCH_SNAPSHOT,
273
- sourcePath: 'fallback://embedded-snapshot',
274
- };
275
- }
276
-
277
- return {
278
- snapshot: parsedSnapshot,
279
- sourcePath: ARCHITECT_RESEARCH_SNAPSHOT_FILE_PATH,
280
- };
281
- } catch {
282
- return {
283
- snapshot: FALLBACK_STACK_RESEARCH_SNAPSHOT,
284
- sourcePath: 'fallback://embedded-snapshot',
285
- };
286
- }
287
- }
288
-
289
- function normalizeMetricValue(value, fallbackValue = 0) {
290
- const parsedValue = Number(value);
291
- if (!Number.isFinite(parsedValue)) {
292
- return fallbackValue;
293
- }
294
-
295
- return clampNumericValue(parsedValue, 0, 1);
296
- }
297
-
298
- function calculateSnapshotSignalScore(snapshotSignalEntry) {
299
- const metrics = snapshotSignalEntry?.metrics || {};
300
- const ecosystemMaturity = normalizeMetricValue(metrics.ecosystemMaturity, 0.65);
301
- const talentAvailability = normalizeMetricValue(metrics.talentAvailability, 0.65);
302
- const deliveryVelocity = normalizeMetricValue(metrics.deliveryVelocity, 0.65);
303
- const aggregateScore = (ecosystemMaturity + talentAvailability + deliveryVelocity) / 3;
304
-
305
- return {
306
- aggregateScore,
307
- metrics: {
308
- ecosystemMaturity,
309
- talentAvailability,
310
- deliveryVelocity,
311
- },
312
- };
313
- }
314
-
315
- function parseRealtimeSignalPayloadFromJson(rawJsonContent) {
316
- try {
317
- const parsedPayload = JSON.parse(rawJsonContent);
318
- if (!Array.isArray(parsedPayload?.stackSignals)) {
319
- return null;
320
- }
321
-
322
- return parsedPayload;
323
- } catch {
324
- return null;
325
- }
326
- }
327
-
328
- function loadRealtimeSignalPayload(realtimeSignalFilePath) {
329
- const normalizedRealtimeSignalFilePath = String(realtimeSignalFilePath || '').trim();
330
- if (normalizedRealtimeSignalFilePath) {
331
- try {
332
- const absoluteRealtimeSignalPath = path.resolve(normalizedRealtimeSignalFilePath);
333
- if (existsSync(absoluteRealtimeSignalPath)) {
334
- const filePayload = parseRealtimeSignalPayloadFromJson(readFileSync(absoluteRealtimeSignalPath, 'utf8'));
335
- if (filePayload) {
336
- return {
337
- payload: filePayload,
338
- sourcePath: absoluteRealtimeSignalPath,
339
- };
340
- }
341
- }
342
- } catch {
343
- // Ignore file loading errors and continue to environment fallback.
344
- }
345
- }
346
-
347
- const signalPathFromEnvironment = String(process.env[ARCHITECT_DEFAULT_REALTIME_SIGNAL_PATH_ENV_KEY] || '').trim();
348
- if (signalPathFromEnvironment) {
349
- try {
350
- const absoluteRealtimeSignalPath = path.resolve(signalPathFromEnvironment);
351
- if (existsSync(absoluteRealtimeSignalPath)) {
352
- const filePayload = parseRealtimeSignalPayloadFromJson(readFileSync(absoluteRealtimeSignalPath, 'utf8'));
353
- if (filePayload) {
354
- return {
355
- payload: filePayload,
356
- sourcePath: absoluteRealtimeSignalPath,
357
- };
358
- }
359
- }
360
- } catch {
361
- // Ignore file loading errors and continue to environment JSON fallback.
362
- }
363
- }
364
-
365
- const signalJsonFromEnvironment = String(process.env[ARCHITECT_DEFAULT_REALTIME_SIGNAL_ENV_KEY] || '').trim();
366
- if (signalJsonFromEnvironment) {
367
- const payloadFromEnvironment = parseRealtimeSignalPayloadFromJson(signalJsonFromEnvironment);
368
- if (payloadFromEnvironment) {
369
- return {
370
- payload: payloadFromEnvironment,
371
- sourcePath: `env://${ARCHITECT_DEFAULT_REALTIME_SIGNAL_ENV_KEY}`,
372
- };
373
- }
374
- }
375
-
376
- return {
377
- payload: null,
378
- sourcePath: null,
379
- };
380
- }
381
-
382
- function normalizeStackSignalBoost(value) {
383
- const normalizedSignal = normalizeMetricValue(value, 0);
384
- return Number((normalizedSignal * 0.35).toFixed(4));
385
- }
386
-
387
- function normalizePaletteRoles(rawPaletteRoles) {
388
- if (!Array.isArray(rawPaletteRoles) || rawPaletteRoles.length === 0) {
389
- return ['base', 'surface', 'accent'];
390
- }
391
-
392
- const sanitizedRoles = rawPaletteRoles
393
- .map((paletteRole) => String(paletteRole || '').trim().toLowerCase())
394
- .filter((paletteRole) => ARCHITECT_ALLOWED_PALETTE_ROLES.includes(paletteRole));
395
-
396
- if (sanitizedRoles.length === 0) {
397
- return ['base', 'surface', 'accent'];
398
- }
399
-
400
- return Array.from(new Set(sanitizedRoles));
401
- }
402
-
403
- function normalizeTypographyScale(rawTypographyScale) {
404
- const normalizedScale = String(rawTypographyScale || '').trim().toLowerCase();
405
- if (ARCHITECT_ALLOWED_TYPOGRAPHY_SCALES.includes(normalizedScale)) {
406
- return normalizedScale;
407
- }
408
-
409
- return 'balanced';
410
- }
411
-
412
- function normalizeSpacingPattern(rawSpacingPattern) {
413
- const normalizedPattern = String(rawSpacingPattern || '').trim().toLowerCase();
414
- if (ARCHITECT_ALLOWED_SPACING_PATTERNS.includes(normalizedPattern)) {
415
- return normalizedPattern;
416
- }
417
-
418
- return 'balanced-grid';
419
- }
420
-
421
- function normalizeMotionCharacteristics(rawMotionCharacteristics) {
422
- if (!Array.isArray(rawMotionCharacteristics) || rawMotionCharacteristics.length === 0) {
423
- return ['subtle-enter', 'state-feedback'];
424
- }
425
-
426
- const sanitizedCharacteristics = rawMotionCharacteristics
427
- .map((motionCharacteristic) => String(motionCharacteristic || '').trim().toLowerCase())
428
- .filter((motionCharacteristic) => ARCHITECT_ALLOWED_MOTION_CHARACTERISTICS.includes(motionCharacteristic));
429
-
430
- if (sanitizedCharacteristics.length === 0) {
431
- return ['subtle-enter', 'state-feedback'];
432
- }
433
-
434
- return Array.from(new Set(sanitizedCharacteristics));
435
- }
436
-
437
- function synthesizeDesignSignals({
438
- normalizedProjectDescription,
439
- recommendedStackFileName,
440
- realtimePayload,
441
- snapshotGeneratedAt,
442
- }) {
443
- const descriptionText = String(normalizedProjectDescription || '');
444
- const looksLikeFinancialSurface = /fraud|bank|payment|regulated|security/.test(descriptionText);
445
- const looksLikeGrowthSurface = /marketing|landing|conversion|sales|campaign/.test(descriptionText);
446
- const looksLikeDataProduct = /analytics|dashboard|report|insight|data/.test(descriptionText);
447
-
448
- let paletteRoles = ['base', 'surface', 'accent'];
449
- let typographyScale = 'balanced';
450
- let spacingPattern = 'balanced-grid';
451
- let motionCharacteristics = ['subtle-enter', 'state-feedback'];
452
-
453
- if (looksLikeFinancialSurface) {
454
- paletteRoles = ['base', 'surface', 'accent', 'danger', 'success'];
455
- motionCharacteristics = ['calm-transition', 'state-feedback'];
456
- } else if (looksLikeGrowthSurface) {
457
- paletteRoles = ['base', 'surface', 'accent', 'success'];
458
- typographyScale = 'expressive';
459
- motionCharacteristics = ['staggered-reveal', 'conversion-focus', 'state-feedback'];
460
- }
461
-
462
- if (looksLikeDataProduct || recommendedStackFileName === 'python.md') {
463
- spacingPattern = 'compact-grid';
464
- }
465
-
466
- const realtimeDesignSignals = realtimePayload?.designSignals || null;
467
- if (realtimeDesignSignals) {
468
- paletteRoles = normalizePaletteRoles(realtimeDesignSignals.paletteRoles || paletteRoles);
469
- typographyScale = normalizeTypographyScale(realtimeDesignSignals.typographyScale || typographyScale);
470
- spacingPattern = normalizeSpacingPattern(realtimeDesignSignals.spacingPattern || spacingPattern);
471
- motionCharacteristics = normalizeMotionCharacteristics(
472
- realtimeDesignSignals.motionCharacteristics || motionCharacteristics
473
- );
474
- }
475
-
476
- return {
477
- generatedAt: snapshotGeneratedAt,
478
- normalizedSignals: {
479
- paletteRoles,
480
- typographyScale,
481
- spacingPattern,
482
- motionCharacteristics,
483
- },
484
- sourcePolicy: {
485
- normalizedSignalsOnly: true,
486
- copiedExternalProse: false,
487
- blockedInputPacks: ['DESIGN.md'],
488
- },
489
- };
490
- }
491
-
492
- function buildEvidenceCitation({
493
- citationId,
494
- sourceType,
495
- sourceName,
496
- sourceUrl,
497
- measuredAt,
498
- stackFileName,
499
- metrics,
500
- note,
501
- }) {
502
- return {
503
- citationId,
504
- sourceType,
505
- sourceName,
506
- sourceUrl,
507
- measuredAt: toIsoTimestamp(measuredAt),
508
- stackFileName,
509
- metrics,
510
- note,
511
- };
512
- }
513
-
514
- function buildFallbackRecommendation({
515
- stackFileNames,
516
- blueprintFileNames,
517
- tokenBudget,
518
- timeoutMs,
519
- usedTokens,
520
- elapsedMs,
521
- timeoutTriggered,
522
- requestedResearchMode,
523
- effectiveResearchMode,
524
- snapshot,
525
- snapshotSourcePath,
526
- realtimeGateEnabled,
527
- realtimeSignalsLoaded,
528
- realtimeSignalSourcePath,
529
- normalizedProjectDescription,
530
- }) {
531
- const fallbackStackFileName = stackFileNames.includes('typescript.md')
532
- ? 'typescript.md'
533
- : stackFileNames[0] || 'typescript.md';
534
- const fallbackBlueprintFileName = resolveRecommendedBlueprintFileName(fallbackStackFileName, blueprintFileNames);
535
- const fallbackSnapshotSignal = Array.isArray(snapshot?.stackSignals)
536
- ? snapshot.stackSignals.find((stackSignal) => stackSignal.stackFileName === fallbackStackFileName) || null
537
- : null;
538
- const fallbackSnapshotMetrics = calculateSnapshotSignalScore(fallbackSnapshotSignal);
539
- const fallbackEvidenceCitations = [
540
- buildEvidenceCitation({
541
- citationId: `snapshot:${snapshot?.snapshotId || 'fallback'}:${fallbackStackFileName}`,
542
- sourceType: 'snapshot',
543
- sourceName: snapshot?.sourceName || 'Stack research snapshot',
544
- sourceUrl: snapshot?.sourceUrl || snapshotSourcePath,
545
- measuredAt: fallbackSnapshotSignal?.measuredAt || snapshot?.generatedAt,
546
- stackFileName: fallbackStackFileName,
547
- metrics: fallbackSnapshotMetrics.metrics,
548
- note: 'Deterministic snapshot baseline used for fallback recommendation.',
549
- }),
550
- ];
551
-
552
- const fallbackDesignGuidance = synthesizeDesignSignals({
553
- normalizedProjectDescription,
554
- recommendedStackFileName: fallbackStackFileName,
555
- realtimePayload: null,
556
- snapshotGeneratedAt: toIsoTimestamp(snapshot?.generatedAt),
557
- });
558
-
559
- return {
560
- projectDescription: String(normalizedProjectDescription || '').trim(),
561
- recommendedStackFileName: fallbackStackFileName,
562
- recommendedBlueprintFileName: fallbackBlueprintFileName,
563
- confidenceLabel: 'low',
564
- confidenceScore: 0.45,
565
- rationaleSentences: [
566
- `I recommend ${toTitleCase(fallbackStackFileName)} with ${toTitleCase(fallbackBlueprintFileName)} as a safe fallback path.`,
567
- 'The architecture recommendation budget was constrained before a stronger stack signal could be computed.',
568
- `Main trade-off: ${STACK_TRADEOFF_SUMMARIES[fallbackStackFileName] || 'validate ecosystem fit before production rollout.'}`,
569
- ],
570
- alternatives: [],
571
- uncertaintyNotes: [
572
- timeoutTriggered
573
- ? 'Timeout guardrail triggered before recommendation analysis completed.'
574
- : 'Input signals were not strong enough for a high-confidence architecture recommendation.',
575
- ],
576
- signalSummary: 'fallback mode',
577
- failureModes: {
578
- lowConfidence: true,
579
- dataConflict: false,
580
- repeatedOverride: false,
581
- realtimeGated: requestedResearchMode === 'realtime' && !realtimeGateEnabled,
582
- realtimeUnavailable: requestedResearchMode === 'realtime' && realtimeGateEnabled && !realtimeSignalsLoaded,
583
- },
584
- research: {
585
- requestedMode: requestedResearchMode,
586
- effectiveMode: effectiveResearchMode,
587
- deterministic: effectiveResearchMode === 'snapshot',
588
- snapshotId: snapshot?.snapshotId || 'fallback',
589
- snapshotGeneratedAt: toIsoTimestamp(snapshot?.generatedAt),
590
- snapshotSourcePath,
591
- realtimeGateEnabled,
592
- realtimeSignalsLoaded,
593
- realtimeSignalSourcePath,
594
- trustedRealtimeSources: Array.isArray(snapshot?.trustedRealtimeSources)
595
- ? snapshot.trustedRealtimeSources
596
- : [],
597
- },
598
- evidenceCitations: fallbackEvidenceCitations,
599
- designGuidance: fallbackDesignGuidance,
600
- researchBudget: {
601
- tokenBudget,
602
- timeoutMs,
603
- usedTokens: Math.min(usedTokens, tokenBudget),
604
- elapsedMs: Math.min(elapsedMs, timeoutMs),
605
- tokenBudgetCapped: usedTokens >= tokenBudget,
606
- timeoutTriggered,
607
- },
608
- };
609
- }
610
-
134
+ /**
135
+ * Generates a repo-grounded architecture brief based on project description
136
+ * keywords and repository marker detection. This is an offline brief —
137
+ * for ecosystem-level research, the consuming agent should perform live
138
+ * web research rather than relying on stale local snapshots.
139
+ */
611
140
  export function recommendArchitecture({
612
141
  projectDescription,
613
142
  projectDetection,
614
143
  stackFileNames,
615
144
  blueprintFileNames,
616
- tokenBudget = ARCHITECT_DEFAULT_TOKEN_BUDGET,
617
- timeoutMs = ARCHITECT_DEFAULT_TIMEOUT_MS,
618
- researchMode = ARCHITECT_DEFAULT_RESEARCH_MODE,
619
- enableRealtimeResearch = false,
620
- realtimeSignalFilePath = null,
621
145
  }) {
622
- const startedAt = Date.now();
623
- const boundedTokenBudget = clampNumericValue(tokenBudget, ARCHITECT_MIN_TOKEN_BUDGET, ARCHITECT_MAX_TOKEN_BUDGET);
624
- const boundedTimeoutMs = clampNumericValue(timeoutMs, ARCHITECT_MIN_TIMEOUT_MS, ARCHITECT_MAX_TIMEOUT_MS);
625
- const requestedResearchMode = normalizeArchitectResearchMode(researchMode);
626
- const { snapshot: stackResearchSnapshot, sourcePath: snapshotSourcePath } = loadStackResearchSnapshot();
627
- const snapshotGeneratedAt = toIsoTimestamp(stackResearchSnapshot.generatedAt);
628
- const realtimeGateEnabled = requestedResearchMode === 'realtime' && enableRealtimeResearch === true;
629
- const realtimeSignalPayloadResult = realtimeGateEnabled
630
- ? loadRealtimeSignalPayload(realtimeSignalFilePath)
631
- : { payload: null, sourcePath: null };
632
- const realtimeSignalPayload = realtimeSignalPayloadResult.payload;
633
- const realtimeSignalsLoaded = Array.isArray(realtimeSignalPayload?.stackSignals)
634
- && realtimeSignalPayload.stackSignals.length > 0;
635
- const effectiveResearchMode = realtimeGateEnabled && realtimeSignalsLoaded
636
- ? 'realtime'
637
- : 'snapshot';
638
146
  const normalizedDescription = String(projectDescription || '').trim().toLowerCase();
639
- const effectiveDescriptionSeed = normalizedDescription || 'general software project';
640
- let effectiveDescription = effectiveDescriptionSeed;
641
- let usedTokens = estimateTokenUsage(effectiveDescription) + 120;
147
+ const effectiveDescription = normalizedDescription || 'general software project';
642
148
  const uncertaintyNotes = [];
643
149
 
644
- if (requestedResearchMode === 'realtime' && !realtimeGateEnabled) {
645
- uncertaintyNotes.push('Realtime research mode requested but gate is off. Using deterministic snapshot baseline.');
646
- }
647
-
648
- if (requestedResearchMode === 'realtime' && realtimeGateEnabled && !realtimeSignalsLoaded) {
649
- uncertaintyNotes.push('Realtime research gate is on, but no trusted realtime payload was available. Using deterministic snapshot baseline.');
650
- }
651
-
652
- if (usedTokens > boundedTokenBudget) {
653
- effectiveDescription = effectiveDescription.slice(0, Math.max(120, boundedTokenBudget * 4));
654
- usedTokens = boundedTokenBudget;
655
- uncertaintyNotes.push('Token budget guardrail trimmed input context before recommendation.');
656
- }
657
-
658
- if ((Date.now() - startedAt) > boundedTimeoutMs) {
659
- return buildFallbackRecommendation({
660
- stackFileNames,
661
- blueprintFileNames,
662
- tokenBudget: boundedTokenBudget,
663
- timeoutMs: boundedTimeoutMs,
664
- usedTokens,
665
- elapsedMs: Date.now() - startedAt,
666
- timeoutTriggered: true,
667
- requestedResearchMode,
668
- effectiveResearchMode,
669
- snapshot: stackResearchSnapshot,
670
- snapshotSourcePath,
671
- realtimeGateEnabled,
672
- realtimeSignalsLoaded,
673
- realtimeSignalSourcePath: realtimeSignalPayloadResult.sourcePath,
674
- normalizedProjectDescription: normalizedDescription,
675
- });
676
- }
677
-
678
- const snapshotSignalByStackFileName = new Map();
679
- for (const snapshotSignalEntry of stackResearchSnapshot?.stackSignals || []) {
680
- if (!snapshotSignalEntry?.stackFileName) {
681
- continue;
682
- }
683
-
684
- snapshotSignalByStackFileName.set(snapshotSignalEntry.stackFileName, snapshotSignalEntry);
685
- }
686
-
687
- const realtimeSignalByStackFileName = new Map();
688
- if (realtimeSignalsLoaded) {
689
- for (const realtimeSignalEntry of realtimeSignalPayload.stackSignals) {
690
- const stackFileName = realtimeSignalEntry?.stackFileName;
691
- if (!stackFileName || !stackFileNames.includes(stackFileName)) {
692
- continue;
693
- }
694
-
695
- const rawRealtimeSignalStrength = realtimeSignalEntry.signalStrength
696
- ?? realtimeSignalEntry.metrics?.signalStrength
697
- ?? 0;
698
- const normalizedRealtimeSignalStrength = normalizeMetricValue(rawRealtimeSignalStrength, 0);
699
- realtimeSignalByStackFileName.set(stackFileName, {
700
- signalBoost: normalizeStackSignalBoost(normalizedRealtimeSignalStrength),
701
- measuredAt: realtimeSignalEntry.measuredAt || realtimeSignalPayload.generatedAt || snapshotGeneratedAt,
702
- sourceName: realtimeSignalEntry.sourceName || realtimeSignalPayload.sourceName || 'Trusted realtime source',
703
- sourceUrl: realtimeSignalEntry.sourceUrl || realtimeSignalPayload.sourceUrl || realtimeSignalPayloadResult.sourcePath,
704
- metrics: {
705
- signalStrength: normalizedRealtimeSignalStrength,
706
- freshnessHours: Number(realtimeSignalEntry.metrics?.freshnessHours) || 0,
707
- },
708
- });
709
- }
710
- }
711
-
150
+ // Repo marker detection scoring (grounded in actual project files).
712
151
  const detectionScoreByStackFileName = new Map();
713
152
  for (const rankedCandidate of projectDetection?.rankedCandidates || []) {
714
153
  const confidenceScore = Number(rankedCandidate.confidenceScore) || 0;
@@ -722,63 +161,64 @@ export function recommendArchitecture({
722
161
  );
723
162
  }
724
163
 
164
+ // Keyword hint scoring (low-confidence bias, not research).
725
165
  const scoredStackCandidates = stackFileNames.map((stackFileName) => {
726
166
  const configuredSignals = STACK_SIGNAL_WEIGHTS[stackFileName] || [];
727
- const matchedSignals = [];
728
- let keywordSignalScore = 0;
167
+ const matchedKeywords = [];
168
+ let keywordScore = 0;
729
169
 
730
170
  for (const configuredSignal of configuredSignals) {
731
171
  if (!effectiveDescription.includes(configuredSignal.term)) {
732
172
  continue;
733
173
  }
734
174
 
735
- keywordSignalScore += configuredSignal.weight;
736
- matchedSignals.push(configuredSignal.term);
175
+ keywordScore += configuredSignal.weight;
176
+ matchedKeywords.push(configuredSignal.term);
737
177
  }
738
178
 
739
- const detectionSignalScore = detectionScoreByStackFileName.get(stackFileName) || 0;
740
- const snapshotSignalEntry = snapshotSignalByStackFileName.get(stackFileName) || null;
741
- const snapshotSignalScorePayload = calculateSnapshotSignalScore(snapshotSignalEntry);
742
- const snapshotSignalScore = normalizeStackSignalBoost(snapshotSignalScorePayload.aggregateScore);
743
- const realtimeSignalPayloadEntry = realtimeSignalByStackFileName.get(stackFileName) || null;
744
- const realtimeSignalScore = realtimeSignalPayloadEntry?.signalBoost || 0;
745
- const totalScore = 0.2 + keywordSignalScore + detectionSignalScore + snapshotSignalScore + realtimeSignalScore;
179
+ const detectionScore = detectionScoreByStackFileName.get(stackFileName) || 0;
180
+ const totalScore = 0.2 + keywordScore + detectionScore;
746
181
 
747
182
  return {
748
183
  stackFileName,
749
184
  totalScore,
750
- keywordSignalScore,
751
- detectionSignalScore,
752
- snapshotSignalScore,
753
- snapshotMetrics: snapshotSignalScorePayload.metrics,
754
- realtimeSignalScore,
755
- realtimeMetrics: realtimeSignalPayloadEntry?.metrics || null,
756
- snapshotMeasuredAt: snapshotSignalEntry?.measuredAt || stackResearchSnapshot.generatedAt,
757
- realtimeMeasuredAt: realtimeSignalPayloadEntry?.measuredAt || null,
758
- realtimeSourceName: realtimeSignalPayloadEntry?.sourceName || null,
759
- realtimeSourceUrl: realtimeSignalPayloadEntry?.sourceUrl || null,
760
- matchedSignals,
185
+ keywordScore,
186
+ detectionScore,
187
+ matchedKeywords,
761
188
  };
762
189
  }).sort((leftCandidate, rightCandidate) => rightCandidate.totalScore - leftCandidate.totalScore);
763
190
 
191
+ // Fallback when no candidates can be scored.
764
192
  if (scoredStackCandidates.length === 0) {
765
- return buildFallbackRecommendation({
766
- stackFileNames,
767
- blueprintFileNames,
768
- tokenBudget: boundedTokenBudget,
769
- timeoutMs: boundedTimeoutMs,
770
- usedTokens,
771
- elapsedMs: Date.now() - startedAt,
772
- timeoutTriggered: false,
773
- requestedResearchMode,
774
- effectiveResearchMode,
775
- snapshot: stackResearchSnapshot,
776
- snapshotSourcePath,
777
- realtimeGateEnabled,
778
- realtimeSignalsLoaded,
779
- realtimeSignalSourcePath: realtimeSignalPayloadResult.sourcePath,
780
- normalizedProjectDescription: normalizedDescription,
781
- });
193
+ const fallbackStackFileName = stackFileNames.includes('typescript.md')
194
+ ? 'typescript.md'
195
+ : stackFileNames[0] || 'typescript.md';
196
+ const fallbackBlueprintFileName = resolveRecommendedBlueprintFileName(fallbackStackFileName, blueprintFileNames);
197
+
198
+ return {
199
+ briefType: 'offline',
200
+ projectDescription: String(projectDescription || '').trim(),
201
+ recommendedStackFileName: fallbackStackFileName,
202
+ recommendedBlueprintFileName: fallbackBlueprintFileName,
203
+ confidenceLabel: 'low',
204
+ confidenceScore: 0.35,
205
+ rationaleSentences: [
206
+ `Defaulting to ${toTitleCase(fallbackStackFileName)} with ${toTitleCase(fallbackBlueprintFileName)} as a safe fallback.`,
207
+ 'No keyword or detection signals were strong enough for a grounded recommendation.',
208
+ 'For ecosystem-level validation, perform live web research before committing to this stack.',
209
+ ],
210
+ alternatives: [],
211
+ uncertaintyNotes: [
212
+ 'This is an offline brief based on keyword hints and repo markers only.',
213
+ 'Ecosystem fitness was not verified — the agent should research before finalizing.',
214
+ ],
215
+ signalSummary: 'fallback (no strong signals)',
216
+ failureModes: {
217
+ lowConfidence: true,
218
+ dataConflict: false,
219
+ repeatedOverride: false,
220
+ },
221
+ };
782
222
  }
783
223
 
784
224
  const strongestCandidate = scoredStackCandidates[0];
@@ -791,7 +231,7 @@ export function recommendArchitecture({
791
231
  + Math.min(strongestCandidate.totalScore / 8, 0.25)
792
232
  + Math.min(Math.max(scoreGap, 0) / 3, 0.18);
793
233
 
794
- if (strongestCandidate.matchedSignals.length === 0) {
234
+ if (strongestCandidate.matchedKeywords.length === 0) {
795
235
  confidenceScore -= 0.2;
796
236
  }
797
237
 
@@ -799,7 +239,7 @@ export function recommendArchitecture({
799
239
  confidenceScore -= 0.1;
800
240
  }
801
241
 
802
- confidenceScore = clampNumericValue(confidenceScore, 0.35, 0.97);
242
+ confidenceScore = Math.min(Math.max(confidenceScore, 0.35), 0.97);
803
243
 
804
244
  const confidenceLabel = resolveConfidenceLabel(confidenceScore);
805
245
  const lowConfidence = confidenceScore < 0.7;
@@ -813,114 +253,36 @@ export function recommendArchitecture({
813
253
  uncertaintyNotes.push('Data conflict: top stack candidates are close, so trade-offs need manual confirmation.');
814
254
  }
815
255
 
256
+ // Offline briefs should always be transparent about their grounding source.
257
+ uncertaintyNotes.push(
258
+ 'This brief is grounded in repo markers and keyword hints only. '
259
+ + 'For ecosystem-level validation, the agent should perform live web research.'
260
+ );
261
+
816
262
  const recommendedStackFileName = strongestCandidate.stackFileName;
817
263
  const recommendedBlueprintFileName = resolveRecommendedBlueprintFileName(recommendedStackFileName, blueprintFileNames);
264
+
818
265
  const signalSummaryParts = [];
819
- if (strongestCandidate.matchedSignals.length > 0) {
820
- signalSummaryParts.push(strongestCandidate.matchedSignals.slice(0, 4).join(', '));
266
+ if (strongestCandidate.matchedKeywords.length > 0) {
267
+ signalSummaryParts.push(`keywords: ${strongestCandidate.matchedKeywords.slice(0, 4).join(', ')}`);
821
268
  }
822
- signalSummaryParts.push(`snapshot ${strongestCandidate.snapshotSignalScore.toFixed(2)}`);
823
- if (strongestCandidate.realtimeSignalScore > 0) {
824
- signalSummaryParts.push(`realtime ${strongestCandidate.realtimeSignalScore.toFixed(2)}`);
269
+ if (strongestCandidate.detectionScore > 0) {
270
+ signalSummaryParts.push(`repo detection: ${strongestCandidate.detectionScore.toFixed(2)}`);
825
271
  }
826
- const signalSummary = signalSummaryParts.join(', ');
272
+ const signalSummary = signalSummaryParts.length > 0
273
+ ? signalSummaryParts.join('; ')
274
+ : 'weak signals only';
827
275
 
828
276
  const rationaleSentences = [
829
- `I recommend ${toTitleCase(recommendedStackFileName)} with ${toTitleCase(recommendedBlueprintFileName)} for this project.`,
830
- `The strongest evidence in your description is ${signalSummary}, combined with available repository signals.`,
277
+ `Stack brief: ${toTitleCase(recommendedStackFileName)} with ${toTitleCase(recommendedBlueprintFileName)}.`,
278
+ `Grounding signals: ${signalSummary}.`,
831
279
  `Main trade-off: ${STACK_TRADEOFF_SUMMARIES[recommendedStackFileName] || 'validate ecosystem fit before production rollout.'}`,
832
280
  ];
833
281
 
834
282
  if (lowConfidence) {
835
- rationaleSentences.push('Confidence is low, so review alternatives before finalizing architecture.');
283
+ rationaleSentences.push('Confidence is low review alternatives and perform live research before finalizing.');
836
284
  }
837
285
 
838
- const evidenceCitations = [
839
- buildEvidenceCitation({
840
- citationId: `snapshot:${stackResearchSnapshot.snapshotId || 'snapshot'}:${recommendedStackFileName}`,
841
- sourceType: 'snapshot',
842
- sourceName: stackResearchSnapshot.sourceName || 'Stack research snapshot',
843
- sourceUrl: stackResearchSnapshot.sourceUrl || snapshotSourcePath,
844
- measuredAt: strongestCandidate.snapshotMeasuredAt || snapshotGeneratedAt,
845
- stackFileName: recommendedStackFileName,
846
- metrics: strongestCandidate.snapshotMetrics,
847
- note: 'Deterministic stack research snapshot signal.',
848
- }),
849
- ];
850
-
851
- if (projectDetection?.recommendedStackFileName) {
852
- evidenceCitations.push(
853
- buildEvidenceCitation({
854
- citationId: `detector:${projectDetection.recommendedStackFileName}`,
855
- sourceType: 'detection',
856
- sourceName: 'Existing project marker detector',
857
- sourceUrl: 'state://project-detection',
858
- measuredAt: snapshotGeneratedAt,
859
- stackFileName: projectDetection.recommendedStackFileName,
860
- metrics: {
861
- confidenceScore: Number(projectDetection.confidenceScore || 0),
862
- confidenceGap: Number(projectDetection.confidenceGap || 0),
863
- },
864
- note: 'Repository marker detection signal used for stack recommendation bias.',
865
- })
866
- );
867
- }
868
-
869
- if (effectiveResearchMode === 'realtime') {
870
- const realtimeCitationCandidate = strongestCandidate.realtimeSignalScore > 0
871
- ? strongestCandidate
872
- : scoredStackCandidates.find((stackCandidate) => stackCandidate.realtimeSignalScore > 0) || null;
873
-
874
- if (realtimeCitationCandidate) {
875
- const realtimeCitationOnRecommendedStack = realtimeCitationCandidate.stackFileName === recommendedStackFileName;
876
- evidenceCitations.push(
877
- buildEvidenceCitation({
878
- citationId: `realtime:${realtimeCitationCandidate.stackFileName}`,
879
- sourceType: 'realtime',
880
- sourceName: realtimeCitationCandidate.realtimeSourceName || 'Trusted realtime source',
881
- sourceUrl: realtimeCitationCandidate.realtimeSourceUrl || realtimeSignalPayloadResult.sourcePath,
882
- measuredAt: realtimeCitationCandidate.realtimeMeasuredAt || snapshotGeneratedAt,
883
- stackFileName: realtimeCitationCandidate.stackFileName,
884
- metrics: realtimeCitationCandidate.realtimeMetrics
885
- || { signalStrength: realtimeCitationCandidate.realtimeSignalScore },
886
- note: realtimeCitationOnRecommendedStack
887
- ? 'Optional realtime signal, loaded only when realtime research is explicitly gated on.'
888
- : 'Optional realtime signal loaded for an alternative stack signal; used as supporting evidence in realtime mode.',
889
- })
890
- );
891
- } else if (realtimeSignalsLoaded && Array.isArray(realtimeSignalPayload?.stackSignals) && realtimeSignalPayload.stackSignals.length > 0) {
892
- const fallbackRealtimeSignal = realtimeSignalPayload.stackSignals[0];
893
- const fallbackRealtimeSignalStrength = normalizeMetricValue(
894
- fallbackRealtimeSignal?.signalStrength
895
- ?? fallbackRealtimeSignal?.metrics?.signalStrength
896
- ?? 0,
897
- 0
898
- );
899
-
900
- evidenceCitations.push(
901
- buildEvidenceCitation({
902
- citationId: 'realtime:payload-fallback',
903
- sourceType: 'realtime',
904
- sourceName: fallbackRealtimeSignal?.sourceName || realtimeSignalPayload.sourceName || 'Trusted realtime source',
905
- sourceUrl: fallbackRealtimeSignal?.sourceUrl || realtimeSignalPayload.sourceUrl || realtimeSignalPayloadResult.sourcePath,
906
- measuredAt: fallbackRealtimeSignal?.measuredAt || realtimeSignalPayload.generatedAt || snapshotGeneratedAt,
907
- stackFileName: recommendedStackFileName,
908
- metrics: {
909
- signalStrength: fallbackRealtimeSignalStrength,
910
- },
911
- note: 'Optional realtime payload loaded, but stack mapping did not match current candidate set. Stored as trace citation.',
912
- })
913
- );
914
- }
915
- }
916
-
917
- const designGuidance = synthesizeDesignSignals({
918
- normalizedProjectDescription: normalizedDescription,
919
- recommendedStackFileName,
920
- realtimePayload: effectiveResearchMode === 'realtime' ? realtimeSignalPayload : null,
921
- snapshotGeneratedAt,
922
- });
923
-
924
286
  const alternatives = scoredStackCandidates
925
287
  .slice(1, 3)
926
288
  .map((stackCandidate) => {
@@ -930,32 +292,12 @@ export function recommendArchitecture({
930
292
  blueprintFileName: alternativeBlueprintFileName,
931
293
  oneLineTradeoff: STACK_TRADEOFF_SUMMARIES[stackCandidate.stackFileName]
932
294
  || 'validate fit with your runtime and team constraints.',
933
- evidenceSummary: `keyword=${stackCandidate.keywordSignalScore.toFixed(2)}, detection=${stackCandidate.detectionSignalScore.toFixed(2)}, snapshot=${stackCandidate.snapshotSignalScore.toFixed(2)}, realtime=${stackCandidate.realtimeSignalScore.toFixed(2)}`,
295
+ evidenceSummary: `keyword=${stackCandidate.keywordScore.toFixed(2)}, detection=${stackCandidate.detectionScore.toFixed(2)}`,
934
296
  };
935
297
  });
936
298
 
937
- const elapsedMs = Date.now() - startedAt;
938
- if (elapsedMs > boundedTimeoutMs) {
939
- return buildFallbackRecommendation({
940
- stackFileNames,
941
- blueprintFileNames,
942
- tokenBudget: boundedTokenBudget,
943
- timeoutMs: boundedTimeoutMs,
944
- usedTokens,
945
- elapsedMs,
946
- timeoutTriggered: true,
947
- requestedResearchMode,
948
- effectiveResearchMode,
949
- snapshot: stackResearchSnapshot,
950
- snapshotSourcePath,
951
- realtimeGateEnabled,
952
- realtimeSignalsLoaded,
953
- realtimeSignalSourcePath: realtimeSignalPayloadResult.sourcePath,
954
- normalizedProjectDescription: normalizedDescription,
955
- });
956
- }
957
-
958
299
  return {
300
+ briefType: 'offline',
959
301
  projectDescription: String(projectDescription || '').trim(),
960
302
  recommendedStackFileName,
961
303
  recommendedBlueprintFileName,
@@ -969,46 +311,16 @@ export function recommendArchitecture({
969
311
  lowConfidence,
970
312
  dataConflict,
971
313
  repeatedOverride: false,
972
- realtimeGated: requestedResearchMode === 'realtime' && !realtimeGateEnabled,
973
- realtimeUnavailable: requestedResearchMode === 'realtime' && realtimeGateEnabled && !realtimeSignalsLoaded,
974
- },
975
- research: {
976
- requestedMode: requestedResearchMode,
977
- effectiveMode: effectiveResearchMode,
978
- deterministic: effectiveResearchMode === 'snapshot',
979
- snapshotId: stackResearchSnapshot.snapshotId || 'snapshot',
980
- snapshotGeneratedAt,
981
- snapshotSourcePath,
982
- realtimeGateEnabled,
983
- realtimeSignalsLoaded,
984
- realtimeSignalSourcePath: realtimeSignalPayloadResult.sourcePath,
985
- trustedRealtimeSources: Array.isArray(stackResearchSnapshot.trustedRealtimeSources)
986
- ? stackResearchSnapshot.trustedRealtimeSources
987
- : [],
988
- },
989
- evidenceCitations,
990
- designGuidance,
991
- researchBudget: {
992
- tokenBudget: boundedTokenBudget,
993
- timeoutMs: boundedTimeoutMs,
994
- usedTokens: Math.min(usedTokens, boundedTokenBudget),
995
- elapsedMs,
996
- tokenBudgetCapped: usedTokens >= boundedTokenBudget,
997
- timeoutTriggered: false,
998
314
  },
999
315
  };
1000
316
  }
1001
317
 
1002
318
  export function formatArchitectureRecommendation(architectureRecommendation) {
1003
- const researchModeSummary = architectureRecommendation.research
1004
- ? `${architectureRecommendation.research.requestedMode} -> ${architectureRecommendation.research.effectiveMode}`
1005
- : 'snapshot';
1006
319
  const outputLines = [
1007
- '\nArchitecture recommendation (project-description-first):',
320
+ '\nArchitecture brief (offline, repo-grounded):',
1008
321
  `- Stack: ${toTitleCase(architectureRecommendation.recommendedStackFileName)}`,
1009
322
  `- Blueprint: ${toTitleCase(architectureRecommendation.recommendedBlueprintFileName)}`,
1010
323
  `- Confidence: ${architectureRecommendation.confidenceLabel} (${architectureRecommendation.confidenceScore})`,
1011
- `- Research mode: ${researchModeSummary}`,
1012
324
  '- Rationale:',
1013
325
  ...architectureRecommendation.rationaleSentences.map((sentence, sentenceIndex) => ` ${sentenceIndex + 1}. ${sentence}`),
1014
326
  '- Alternatives:',
@@ -1048,31 +360,6 @@ export function formatArchitectureRecommendation(architectureRecommendation) {
1048
360
  outputLines.push(`- Caution labels: ${cautionLabels.join(', ')}`);
1049
361
  }
1050
362
 
1051
- if (Array.isArray(architectureRecommendation.evidenceCitations) && architectureRecommendation.evidenceCitations.length > 0) {
1052
- outputLines.push('- Evidence citations (measurable source + timestamp):');
1053
- outputLines.push(
1054
- ...architectureRecommendation.evidenceCitations.map((citationEntry, citationIndex) => {
1055
- const metricsSummary = Object.entries(citationEntry.metrics || {})
1056
- .map(([metricKey, metricValue]) => `${metricKey}=${metricValue}`)
1057
- .join(', ');
1058
- return ` ${citationIndex + 1}. [${citationEntry.sourceType}] ${citationEntry.sourceName} @ ${citationEntry.measuredAt} (${metricsSummary || 'no metrics'})`;
1059
- })
1060
- );
1061
- }
1062
-
1063
- if (architectureRecommendation.designGuidance?.normalizedSignals) {
1064
- const normalizedSignals = architectureRecommendation.designGuidance.normalizedSignals;
1065
- outputLines.push('- Design signal synthesis (normalized, no copied external prose):');
1066
- outputLines.push(` 1. Palette roles: ${normalizedSignals.paletteRoles.join(', ')}`);
1067
- outputLines.push(` 2. Typography scale: ${normalizedSignals.typographyScale}`);
1068
- outputLines.push(` 3. Spacing pattern: ${normalizedSignals.spacingPattern}`);
1069
- outputLines.push(` 4. Motion characteristics: ${normalizedSignals.motionCharacteristics.join(', ')}`);
1070
- }
1071
-
1072
- outputLines.push(
1073
- `- Research guardrails: ${architectureRecommendation.researchBudget.usedTokens}/${architectureRecommendation.researchBudget.tokenBudget} tokens, ${architectureRecommendation.researchBudget.elapsedMs}ms/${architectureRecommendation.researchBudget.timeoutMs}ms`
1074
- );
1075
-
1076
363
  return outputLines.join('\n');
1077
364
  }
1078
365