vibepro 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,1732 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildDerivedDesignSystem,
|
|
6
|
+
buildDesignSystemGate,
|
|
7
|
+
buildProductSemanticModel,
|
|
8
|
+
collectScreens,
|
|
9
|
+
resolveDesignRoutes,
|
|
10
|
+
normalizeDesignSystemBundle
|
|
11
|
+
} from './design-modernize.js';
|
|
12
|
+
import { importGraphifyArtifacts } from './graphify-adapter.js';
|
|
13
|
+
import { localizedText } from './language.js';
|
|
14
|
+
|
|
15
|
+
const STYLE_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
16
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
|
17
|
+
const IGNORED_DIRS = new Set(['.git', '.next', '.vibepro', 'coverage', 'dist', 'node_modules']);
|
|
18
|
+
|
|
19
|
+
export async function deriveNativeDesignSystem(repoRoot, options = {}) {
|
|
20
|
+
const root = path.resolve(repoRoot);
|
|
21
|
+
const product = options.product ?? inferProductName(root);
|
|
22
|
+
const designSystemId = sanitizeId(options.designSystemId ?? options.id ?? product);
|
|
23
|
+
const routes = await resolveDesignRoutes(root, options.routes);
|
|
24
|
+
const visualFoundations = await readVisualFoundations(root, {
|
|
25
|
+
designSystemId,
|
|
26
|
+
product,
|
|
27
|
+
briefFile: options.briefFile
|
|
28
|
+
});
|
|
29
|
+
const graphify = await collectGraphifyEvidence(root, options);
|
|
30
|
+
const screens = await collectScreens(root, routes, {
|
|
31
|
+
product,
|
|
32
|
+
designSystem: { status: 'not_provided', title: product },
|
|
33
|
+
baseUrl: options.baseUrl
|
|
34
|
+
});
|
|
35
|
+
const productSemantics = buildProductSemanticModel({
|
|
36
|
+
product,
|
|
37
|
+
brief: options.brief,
|
|
38
|
+
routes,
|
|
39
|
+
screens
|
|
40
|
+
});
|
|
41
|
+
const derivedDesignSystem = buildDerivedDesignSystem({
|
|
42
|
+
product,
|
|
43
|
+
semanticModel: productSemantics,
|
|
44
|
+
screens,
|
|
45
|
+
referenceDesignSystem: { status: 'not_provided', title: product }
|
|
46
|
+
});
|
|
47
|
+
const styleEvidence = await collectStyleEvidence(root);
|
|
48
|
+
const sourceEvidence = await collectSourceEvidence(root);
|
|
49
|
+
const routePatterns = buildRoutePatterns({ screens, graphify });
|
|
50
|
+
const implementationMapping = buildImplementationMapping({ screens, sourceEvidence, graphify });
|
|
51
|
+
const semanticTokens = buildSemanticTokens({ derivedDesignSystem, styleEvidence });
|
|
52
|
+
const stateSemantics = buildStateSemantics({ derivedDesignSystem, screens });
|
|
53
|
+
const ctaPolicy = buildCtaPolicy({ derivedDesignSystem, screens });
|
|
54
|
+
const densityPolicy = buildDensityPolicy({ derivedDesignSystem, styleEvidence });
|
|
55
|
+
const navigationPolicy = buildNavigationPolicy({ derivedDesignSystem, screens });
|
|
56
|
+
const antiPatterns = buildAntiPatterns(derivedDesignSystem);
|
|
57
|
+
const evidenceCoverage = buildEvidenceCoverage({
|
|
58
|
+
screens,
|
|
59
|
+
styleEvidence,
|
|
60
|
+
sourceEvidence,
|
|
61
|
+
graphify,
|
|
62
|
+
semanticTokens,
|
|
63
|
+
implementationMapping
|
|
64
|
+
});
|
|
65
|
+
const dsGate = buildDesignSystemGate({
|
|
66
|
+
storyId: designSystemId,
|
|
67
|
+
derivedDesignSystem
|
|
68
|
+
});
|
|
69
|
+
const designSystem = {
|
|
70
|
+
schema_version: '0.1.0',
|
|
71
|
+
workflow: 'native-design-system-derivation',
|
|
72
|
+
design_system_id: designSystemId,
|
|
73
|
+
product,
|
|
74
|
+
generated_at: new Date().toISOString(),
|
|
75
|
+
output: { language: options.language ?? 'ja' },
|
|
76
|
+
authority: 'vibepro_native_design_system',
|
|
77
|
+
external_generator_required: false,
|
|
78
|
+
source_evidence: {
|
|
79
|
+
routes,
|
|
80
|
+
graphify,
|
|
81
|
+
current_ui_code: summarizeScreenEvidence(screens),
|
|
82
|
+
style_files: styleEvidence.files.map((file) => file.path),
|
|
83
|
+
visual_foundations: visualFoundations ? {
|
|
84
|
+
source: visualFoundations.source,
|
|
85
|
+
artifact: `.vibepro/design-system/${designSystemId}/visual-foundations.json`,
|
|
86
|
+
authority: visualFoundations.authority
|
|
87
|
+
} : null
|
|
88
|
+
},
|
|
89
|
+
visual_foundations: visualFoundations,
|
|
90
|
+
product_semantics: productSemantics,
|
|
91
|
+
theme_tokens: styleEvidence.theme_tokens,
|
|
92
|
+
semantic_tokens: semanticTokens,
|
|
93
|
+
component_roles: derivedDesignSystem.component_role_map,
|
|
94
|
+
component_states: stateSemantics,
|
|
95
|
+
screen_patterns: routePatterns,
|
|
96
|
+
cta_policy: ctaPolicy,
|
|
97
|
+
density_policy: densityPolicy,
|
|
98
|
+
navigation_policy: navigationPolicy,
|
|
99
|
+
anti_patterns: antiPatterns,
|
|
100
|
+
implementation_mapping: implementationMapping,
|
|
101
|
+
evidence_coverage: evidenceCoverage,
|
|
102
|
+
ds_gate: mergeVisualFoundationsGate(dsGate, visualFoundations)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const outDir = path.join(root, '.vibepro', 'design-system', designSystemId);
|
|
106
|
+
await mkdir(outDir, { recursive: true });
|
|
107
|
+
await writeDesignSystemArtifacts(outDir, designSystem);
|
|
108
|
+
return { outDir, result: designSystem };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function initDesignSystem(repoRoot, options = {}) {
|
|
112
|
+
const root = path.resolve(repoRoot);
|
|
113
|
+
if (!options.designSystemId && !options.id) {
|
|
114
|
+
throw new Error('design-system init requires --id <ds-id>');
|
|
115
|
+
}
|
|
116
|
+
const designSystemId = sanitizeId(options.designSystemId ?? options.id);
|
|
117
|
+
const product = options.product ?? designSystemId;
|
|
118
|
+
const designSystem = createEmptyDesignSystem({
|
|
119
|
+
designSystemId,
|
|
120
|
+
product,
|
|
121
|
+
language: options.language ?? 'ja'
|
|
122
|
+
});
|
|
123
|
+
const outDir = path.join(root, '.vibepro', 'design-system', designSystemId);
|
|
124
|
+
await mkdir(outDir, { recursive: true });
|
|
125
|
+
await writeDesignSystemArtifacts(outDir, designSystem);
|
|
126
|
+
return { outDir, result: designSystem };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function exportDesignSystem(repoRoot, options = {}) {
|
|
130
|
+
const root = path.resolve(repoRoot);
|
|
131
|
+
if (!options.designSystemId && !options.id) {
|
|
132
|
+
throw new Error('design-system export requires --id <ds-id>');
|
|
133
|
+
}
|
|
134
|
+
const designSystemId = sanitizeId(options.designSystemId ?? options.id);
|
|
135
|
+
const format = String(options.format ?? 'json').toLowerCase();
|
|
136
|
+
const outDir = path.join(root, '.vibepro', 'design-system', designSystemId);
|
|
137
|
+
const designSystemPath = path.join(outDir, 'design-system.json');
|
|
138
|
+
const designSystem = await readJsonIfExists(designSystemPath);
|
|
139
|
+
if (!designSystem) {
|
|
140
|
+
throw new Error(`Design System not found: ${path.relative(root, designSystemPath).split(path.sep).join('/')}. Run design-system init or derive first.`);
|
|
141
|
+
}
|
|
142
|
+
if (format === 'json') {
|
|
143
|
+
return {
|
|
144
|
+
outDir,
|
|
145
|
+
result: {
|
|
146
|
+
schema_version: '0.1.0',
|
|
147
|
+
workflow: 'design-system-export',
|
|
148
|
+
design_system_id: designSystemId,
|
|
149
|
+
format,
|
|
150
|
+
status: 'pass',
|
|
151
|
+
content_type: 'application/json',
|
|
152
|
+
content: `${JSON.stringify(designSystem, null, 2)}\n`
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (format === 'markdown') {
|
|
157
|
+
return {
|
|
158
|
+
outDir,
|
|
159
|
+
result: {
|
|
160
|
+
schema_version: '0.1.0',
|
|
161
|
+
workflow: 'design-system-export',
|
|
162
|
+
design_system_id: designSystemId,
|
|
163
|
+
format,
|
|
164
|
+
status: 'pass',
|
|
165
|
+
content_type: 'text/markdown',
|
|
166
|
+
content: renderNativeDesignSystemSummary(designSystem, options.language ?? designSystem.output?.language ?? 'ja')
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (format === 'css') {
|
|
171
|
+
return {
|
|
172
|
+
outDir,
|
|
173
|
+
result: buildCssExport(designSystem, designSystemId)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
throw new Error('design-system export requires --format json|markdown|css');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function ingestVisualDesignBrief(repoRoot, options = {}) {
|
|
180
|
+
const root = path.resolve(repoRoot);
|
|
181
|
+
if (!options.designSystemId && !options.id) {
|
|
182
|
+
throw new Error('design-system ingest-brief requires --id <ds-id>');
|
|
183
|
+
}
|
|
184
|
+
const designSystemId = sanitizeId(options.designSystemId ?? options.id);
|
|
185
|
+
if (!options.briefFile) {
|
|
186
|
+
throw new Error('design-system ingest-brief requires --brief-file <path>');
|
|
187
|
+
}
|
|
188
|
+
const outDir = path.join(root, '.vibepro', 'design-system', designSystemId);
|
|
189
|
+
const designSystemPath = path.join(outDir, 'design-system.json');
|
|
190
|
+
let designSystem;
|
|
191
|
+
try {
|
|
192
|
+
designSystem = JSON.parse(await readFile(designSystemPath, 'utf8'));
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error(`Design System not found: ${path.relative(root, designSystemPath).split(path.sep).join('/')}. Run design-system derive first.`);
|
|
195
|
+
}
|
|
196
|
+
const visualFoundations = await readVisualFoundations(root, {
|
|
197
|
+
designSystemId,
|
|
198
|
+
product: designSystem.product ?? options.product ?? designSystemId,
|
|
199
|
+
briefFile: options.briefFile
|
|
200
|
+
});
|
|
201
|
+
const nextDesignSystem = {
|
|
202
|
+
...designSystem,
|
|
203
|
+
output: { language: options.language ?? designSystem.output?.language ?? 'ja' },
|
|
204
|
+
visual_foundations: visualFoundations,
|
|
205
|
+
source_evidence: {
|
|
206
|
+
...(designSystem.source_evidence ?? {}),
|
|
207
|
+
visual_foundations: {
|
|
208
|
+
source: visualFoundations.source,
|
|
209
|
+
artifact: `.vibepro/design-system/${designSystemId}/visual-foundations.json`,
|
|
210
|
+
authority: visualFoundations.authority
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
ds_gate: mergeVisualFoundationsGate(designSystem.ds_gate, visualFoundations)
|
|
214
|
+
};
|
|
215
|
+
await writeDesignSystemArtifacts(outDir, nextDesignSystem);
|
|
216
|
+
return { outDir, result: nextDesignSystem };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function ingestExternalDesignSystemBundle(repoRoot, options = {}) {
|
|
220
|
+
const root = path.resolve(repoRoot);
|
|
221
|
+
if (!options.designSystemId && !options.id) {
|
|
222
|
+
throw new Error('design-system ingest requires --id <ds-id>');
|
|
223
|
+
}
|
|
224
|
+
if (!options.bundleFile) {
|
|
225
|
+
throw new Error('design-system ingest requires --bundle <file>');
|
|
226
|
+
}
|
|
227
|
+
const designSystemId = sanitizeId(options.designSystemId ?? options.id);
|
|
228
|
+
const bundlePath = path.isAbsolute(options.bundleFile) ? options.bundleFile : path.join(root, options.bundleFile);
|
|
229
|
+
const bundleText = await readFile(bundlePath, 'utf8');
|
|
230
|
+
const parsedBundle = JSON.parse(bundleText);
|
|
231
|
+
const sanitized = sanitizeExternalBundle(parsedBundle);
|
|
232
|
+
const bundleSummary = normalizeDesignSystemBundle(sanitized.value, {
|
|
233
|
+
designSystemId,
|
|
234
|
+
title: options.product ?? designSystemId
|
|
235
|
+
});
|
|
236
|
+
const externalBundle = buildExternalBundleReference({
|
|
237
|
+
root,
|
|
238
|
+
bundlePath,
|
|
239
|
+
designSystemId,
|
|
240
|
+
sanitized,
|
|
241
|
+
bundleSummary
|
|
242
|
+
});
|
|
243
|
+
const outDir = path.join(root, '.vibepro', 'design-system', designSystemId);
|
|
244
|
+
const designSystemPath = path.join(outDir, 'design-system.json');
|
|
245
|
+
const existingDesignSystem = await readJsonIfExists(designSystemPath);
|
|
246
|
+
const product = options.product
|
|
247
|
+
?? existingDesignSystem?.product
|
|
248
|
+
?? parsedBundle.product
|
|
249
|
+
?? parsedBundle.title
|
|
250
|
+
?? parsedBundle.name
|
|
251
|
+
?? designSystemId;
|
|
252
|
+
const base = existingDesignSystem ?? createBundleIngestBaseDesignSystem({ designSystemId, product });
|
|
253
|
+
const normalizedPayload = extractBundlePayload(sanitized.value);
|
|
254
|
+
const tokenEvidence = collectBundleTokenEvidence(normalizedPayload.tokens);
|
|
255
|
+
const componentEvidence = collectBundleComponentEvidence(normalizedPayload.components);
|
|
256
|
+
const guidelineEvidence = collectBundleGuidelineEvidence(normalizedPayload.guidelines);
|
|
257
|
+
const nextDesignSystem = {
|
|
258
|
+
...base,
|
|
259
|
+
workflow: base.workflow ?? 'native-design-system-derivation',
|
|
260
|
+
design_system_id: designSystemId,
|
|
261
|
+
product,
|
|
262
|
+
generated_at: new Date().toISOString(),
|
|
263
|
+
output: { language: options.language ?? base.output?.language ?? 'ja' },
|
|
264
|
+
authority: 'vibepro_native_design_system',
|
|
265
|
+
external_generator_required: false,
|
|
266
|
+
source_evidence: {
|
|
267
|
+
...(base.source_evidence ?? {}),
|
|
268
|
+
routes: base.source_evidence?.routes ?? [],
|
|
269
|
+
graphify: base.source_evidence?.graphify ?? emptyGraphifyEvidence(),
|
|
270
|
+
current_ui_code: base.source_evidence?.current_ui_code ?? [],
|
|
271
|
+
style_files: base.source_evidence?.style_files ?? [],
|
|
272
|
+
external_bundle: {
|
|
273
|
+
source: externalBundle.source,
|
|
274
|
+
artifact: `.vibepro/design-system/${designSystemId}/external-bundle.json`,
|
|
275
|
+
authority: externalBundle.authority,
|
|
276
|
+
redacted_value_count: externalBundle.redacted_value_count
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
external_bundle: externalBundle,
|
|
280
|
+
theme_tokens: mergeThemeTokens(base.theme_tokens, tokenEvidence),
|
|
281
|
+
semantic_tokens: mergeSemanticTokens(base.semantic_tokens, tokenEvidence, guidelineEvidence),
|
|
282
|
+
component_roles: mergeComponentRoles(base.component_roles, componentEvidence),
|
|
283
|
+
component_states: mergeComponentStates(base.component_states, guidelineEvidence),
|
|
284
|
+
cta_policy: mergeCtaPolicy(base.cta_policy, guidelineEvidence, componentEvidence),
|
|
285
|
+
density_policy: mergeDensityPolicy(base.density_policy, guidelineEvidence, tokenEvidence),
|
|
286
|
+
navigation_policy: mergeNavigationPolicy(base.navigation_policy, guidelineEvidence),
|
|
287
|
+
anti_patterns: mergeAntiPatterns(base.anti_patterns, guidelineEvidence),
|
|
288
|
+
evidence_coverage: mergeBundleEvidenceCoverage(base.evidence_coverage, { tokenEvidence, componentEvidence, guidelineEvidence }),
|
|
289
|
+
ds_gate: mergeExternalBundleGate(base.ds_gate, externalBundle)
|
|
290
|
+
};
|
|
291
|
+
await mkdir(outDir, { recursive: true });
|
|
292
|
+
await writeDesignSystemArtifacts(outDir, nextDesignSystem);
|
|
293
|
+
await writeFile(path.join(outDir, 'external-bundle.json'), `${JSON.stringify(externalBundle, null, 2)}\n`);
|
|
294
|
+
return { outDir, result: nextDesignSystem };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function validateDesignSystem(repoRoot, options = {}) {
|
|
298
|
+
const root = path.resolve(repoRoot);
|
|
299
|
+
if (!options.designSystemId && !options.id) {
|
|
300
|
+
throw new Error('design-system validate requires --id <ds-id>');
|
|
301
|
+
}
|
|
302
|
+
if (!options.storyId) {
|
|
303
|
+
throw new Error('design-system validate requires --story-id <story-id>');
|
|
304
|
+
}
|
|
305
|
+
const designSystemId = sanitizeId(options.designSystemId ?? options.id);
|
|
306
|
+
const storyId = sanitizeStoryId(options.storyId);
|
|
307
|
+
const outDir = path.join(root, '.vibepro', 'design-system', designSystemId);
|
|
308
|
+
const designSystemPath = path.join(outDir, 'design-system.json');
|
|
309
|
+
let designSystem;
|
|
310
|
+
try {
|
|
311
|
+
designSystem = JSON.parse(await readFile(designSystemPath, 'utf8'));
|
|
312
|
+
} catch {
|
|
313
|
+
throw new Error(`Design System not found: ${path.relative(root, designSystemPath).split(path.sep).join('/')}. Run design-system derive first.`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const storyContext = await collectDesignValidationStoryContext(root, storyId);
|
|
317
|
+
const artifactTexts = await readDesignSystemArtifactTexts(outDir);
|
|
318
|
+
const findings = [
|
|
319
|
+
...validateDesignSystemShape(designSystem),
|
|
320
|
+
...validateDesignSystemStoryDrift({ designSystem, storyContext }),
|
|
321
|
+
...validateSecretLeakage(artifactTexts)
|
|
322
|
+
];
|
|
323
|
+
const result = {
|
|
324
|
+
schema_version: '0.1.0',
|
|
325
|
+
workflow: 'design-system-validation',
|
|
326
|
+
design_system_id: designSystemId,
|
|
327
|
+
story_id: storyId,
|
|
328
|
+
generated_at: new Date().toISOString(),
|
|
329
|
+
output: { language: options.language ?? designSystem.output?.language ?? 'ja' },
|
|
330
|
+
authority: {
|
|
331
|
+
design_system: designSystem.authority ?? 'unknown',
|
|
332
|
+
implementation: 'current code, Story, Spec, Architecture, and VibePro gates remain authoritative',
|
|
333
|
+
generated_visuals: 'reference_only'
|
|
334
|
+
},
|
|
335
|
+
story_context: storyContext,
|
|
336
|
+
summary: summarizeValidationStatus(findings),
|
|
337
|
+
findings
|
|
338
|
+
};
|
|
339
|
+
const validationDir = path.join(outDir, 'validation');
|
|
340
|
+
await mkdir(validationDir, { recursive: true });
|
|
341
|
+
await writeFile(path.join(validationDir, `${storyId}.json`), `${JSON.stringify(result, null, 2)}\n`);
|
|
342
|
+
await writeFile(path.join(validationDir, `${storyId}.md`), renderDesignSystemValidationSummary(result, result.output.language));
|
|
343
|
+
return { outDir: validationDir, result };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function renderNativeDesignSystemSummary(result, language = result.output?.language ?? 'ja') {
|
|
347
|
+
return [
|
|
348
|
+
localizedText(language, { ja: `# Design System: ${result.product}`, en: `# Design System: ${result.product}` }),
|
|
349
|
+
'',
|
|
350
|
+
`- id: ${result.design_system_id}`,
|
|
351
|
+
`- workflow: ${result.workflow}`,
|
|
352
|
+
`- authority: ${result.authority}`,
|
|
353
|
+
`- routes: ${result.source_evidence.routes.join(', ')}`,
|
|
354
|
+
`- graphify: ${result.source_evidence.graphify.status}`,
|
|
355
|
+
`- style files: ${result.source_evidence.style_files.length}`,
|
|
356
|
+
`- component roles: ${result.component_roles.roles.length}`,
|
|
357
|
+
`- screen patterns: ${result.screen_patterns.patterns.length}`,
|
|
358
|
+
`- visual foundations: ${result.visual_foundations ? result.visual_foundations.source : 'not_provided'}`,
|
|
359
|
+
`- gate fallback allowed: ${result.ds_gate.fallback_allowed}`,
|
|
360
|
+
'',
|
|
361
|
+
localizedText(language, { ja: '## プロダクト意味論', en: '## Product Semantics' }),
|
|
362
|
+
'',
|
|
363
|
+
`- domain: ${result.product_semantics.primary_domain}`,
|
|
364
|
+
`- language: ${result.product_semantics.language_policy}`,
|
|
365
|
+
`- interaction: ${result.product_semantics.interaction_model}`,
|
|
366
|
+
`- concepts: ${result.product_semantics.domain_concepts.join(', ') || '-'}`,
|
|
367
|
+
'',
|
|
368
|
+
localizedText(language, { ja: '## 証跡カバレッジ', en: '## Evidence Coverage' }),
|
|
369
|
+
'',
|
|
370
|
+
`- status: ${result.evidence_coverage.status}`,
|
|
371
|
+
...result.evidence_coverage.findings.map((finding) => `- ${finding.status}: ${finding.id} - ${finding.summary}`),
|
|
372
|
+
''
|
|
373
|
+
].join('\n');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function renderDesignSystemValidationSummary(result, language = result.output?.language ?? 'ja') {
|
|
377
|
+
return `${localizedText(language, { ja: `# Design System検証: ${result.design_system_id}`, en: `# Design System Validation: ${result.design_system_id}` })}
|
|
378
|
+
|
|
379
|
+
- story: ${result.story_id}
|
|
380
|
+
- workflow: ${result.workflow}
|
|
381
|
+
- status: ${result.summary.status}
|
|
382
|
+
- pass: ${result.summary.pass}
|
|
383
|
+
- needs_review: ${result.summary.needs_review}
|
|
384
|
+
- needs_evidence: ${result.summary.needs_evidence}
|
|
385
|
+
- block: ${result.summary.block}
|
|
386
|
+
|
|
387
|
+
## ${localizedText(language, { ja: '正本境界', en: 'Authority' })}
|
|
388
|
+
|
|
389
|
+
- design_system: ${result.authority.design_system}
|
|
390
|
+
- implementation: ${result.authority.implementation}
|
|
391
|
+
- generated_visuals: ${result.authority.generated_visuals}
|
|
392
|
+
|
|
393
|
+
## ${localizedText(language, { ja: 'Story文脈', en: 'Story Context' })}
|
|
394
|
+
|
|
395
|
+
- sources: ${result.story_context.sources.map((source) => source.path).join(', ') || 'not_found'}
|
|
396
|
+
- ui_signal: ${result.story_context.ui_signal ? 'yes' : 'no'}
|
|
397
|
+
- ds_signal: ${result.story_context.design_system_signal ? 'yes' : 'no'}
|
|
398
|
+
|
|
399
|
+
## ${localizedText(language, { ja: '検出事項', en: 'Findings' })}
|
|
400
|
+
|
|
401
|
+
${result.findings.map((finding) => `- ${finding.status}: ${finding.id} - ${finding.summary}`).join('\n')}
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function createEmptyDesignSystem({ designSystemId, product, language }) {
|
|
406
|
+
const generatedAt = new Date().toISOString();
|
|
407
|
+
return {
|
|
408
|
+
schema_version: '0.1.0',
|
|
409
|
+
workflow: 'native-design-system-init',
|
|
410
|
+
design_system_id: designSystemId,
|
|
411
|
+
product_id: designSystemId,
|
|
412
|
+
product,
|
|
413
|
+
generated_at: generatedAt,
|
|
414
|
+
output: { language },
|
|
415
|
+
authority: 'vibepro_native_design_system',
|
|
416
|
+
authority_boundary: [
|
|
417
|
+
'this artifact is the VibePro-native Design System scaffold',
|
|
418
|
+
'Story, Spec, Architecture, current code, and VibePro gates remain implementation authority',
|
|
419
|
+
'empty sections require evidence before the DS can be treated as complete'
|
|
420
|
+
],
|
|
421
|
+
external_generator_required: false,
|
|
422
|
+
source_evidence: {
|
|
423
|
+
routes: [],
|
|
424
|
+
graphify: emptyGraphifyEvidence(),
|
|
425
|
+
current_ui_code: [],
|
|
426
|
+
style_files: []
|
|
427
|
+
},
|
|
428
|
+
product_semantics: {
|
|
429
|
+
schema_version: '0.1.0',
|
|
430
|
+
product,
|
|
431
|
+
primary_domain: 'needs_evidence',
|
|
432
|
+
language_policy: 'needs_evidence',
|
|
433
|
+
interaction_model: 'needs_evidence',
|
|
434
|
+
domain_concepts: []
|
|
435
|
+
},
|
|
436
|
+
theme_tokens: {
|
|
437
|
+
schema_version: '0.1.0',
|
|
438
|
+
css_variables: [],
|
|
439
|
+
class_hints: [],
|
|
440
|
+
color_values: [],
|
|
441
|
+
spacing_values: []
|
|
442
|
+
},
|
|
443
|
+
semantic_tokens: {
|
|
444
|
+
schema_version: '0.1.0',
|
|
445
|
+
color_roles: [],
|
|
446
|
+
state_semantics: [],
|
|
447
|
+
cta_priority: [],
|
|
448
|
+
domain_semantics: [],
|
|
449
|
+
raw_token_coverage: {
|
|
450
|
+
css_variable_count: 0,
|
|
451
|
+
color_value_count: 0,
|
|
452
|
+
spacing_value_count: 0
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
component_roles: {
|
|
456
|
+
schema_version: '0.1.0',
|
|
457
|
+
roles: []
|
|
458
|
+
},
|
|
459
|
+
component_states: {
|
|
460
|
+
schema_version: '0.1.0',
|
|
461
|
+
required_states: [],
|
|
462
|
+
discovered_states: [],
|
|
463
|
+
state_policy: []
|
|
464
|
+
},
|
|
465
|
+
screen_patterns: {
|
|
466
|
+
schema_version: '0.1.0',
|
|
467
|
+
graphify_status: 'not_available',
|
|
468
|
+
patterns: []
|
|
469
|
+
},
|
|
470
|
+
cta_policy: {
|
|
471
|
+
schema_version: '0.1.0',
|
|
472
|
+
hierarchy: [],
|
|
473
|
+
discovered_ctas: [],
|
|
474
|
+
rules: []
|
|
475
|
+
},
|
|
476
|
+
density_policy: {
|
|
477
|
+
schema_version: '0.1.0',
|
|
478
|
+
policy: 'needs_evidence',
|
|
479
|
+
evidence: {
|
|
480
|
+
spacing_values: [],
|
|
481
|
+
compact_class_hints: []
|
|
482
|
+
},
|
|
483
|
+
rules: []
|
|
484
|
+
},
|
|
485
|
+
navigation_policy: {
|
|
486
|
+
schema_version: '0.1.0',
|
|
487
|
+
policy: 'needs_evidence',
|
|
488
|
+
navigation_targets: [],
|
|
489
|
+
rules: []
|
|
490
|
+
},
|
|
491
|
+
anti_patterns: {
|
|
492
|
+
schema_version: '0.1.0',
|
|
493
|
+
global_rules: []
|
|
494
|
+
},
|
|
495
|
+
implementation_mapping: {
|
|
496
|
+
schema_version: '0.1.0',
|
|
497
|
+
mapping_source: 'needs_evidence',
|
|
498
|
+
screen_mappings: [],
|
|
499
|
+
source_file_sample: [],
|
|
500
|
+
shared_component_candidates: []
|
|
501
|
+
},
|
|
502
|
+
evidence_coverage: {
|
|
503
|
+
schema_version: '0.1.0',
|
|
504
|
+
status: 'needs_evidence',
|
|
505
|
+
findings: [
|
|
506
|
+
{
|
|
507
|
+
id: 'DS-EVIDENCE-SCAFFOLD',
|
|
508
|
+
status: 'needs_evidence',
|
|
509
|
+
summary: 'Design System scaffold exists, but route, code, token, component, state, CTA, density, and navigation evidence have not been attached.'
|
|
510
|
+
}
|
|
511
|
+
]
|
|
512
|
+
},
|
|
513
|
+
ds_gate: {
|
|
514
|
+
schema_version: '0.1.0',
|
|
515
|
+
status: 'needs_evidence',
|
|
516
|
+
fallback_allowed: false,
|
|
517
|
+
checks: [
|
|
518
|
+
{
|
|
519
|
+
id: 'DS-GATE-SCAFFOLD-EVIDENCE',
|
|
520
|
+
status: 'needs_evidence',
|
|
521
|
+
statement: 'A scaffolded Design System must collect product evidence before it can pass DS gate review.'
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: 'DS-GATE-AUTHORITY-BOUNDARY',
|
|
525
|
+
status: 'pass',
|
|
526
|
+
statement: 'The scaffold is VibePro-native and does not make external/generated visuals authoritative.'
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function buildCssExport(designSystem, designSystemId) {
|
|
534
|
+
const themeTokens = designSystem.theme_tokens ?? {};
|
|
535
|
+
const semanticTokens = designSystem.semantic_tokens ?? {};
|
|
536
|
+
const cssVariables = unique(themeTokens.css_variables ?? []);
|
|
537
|
+
const colorValues = unique(themeTokens.color_values ?? []);
|
|
538
|
+
const spacingValues = unique(themeTokens.spacing_values ?? []);
|
|
539
|
+
const colorRoles = Array.isArray(semanticTokens.color_roles) ? semanticTokens.color_roles : [];
|
|
540
|
+
const semanticAliases = colorRoles.flatMap((role) => {
|
|
541
|
+
const name = sanitizeId(role.name ?? role.role ?? 'color');
|
|
542
|
+
const candidates = Array.isArray(role.candidate_tokens) ? role.candidate_tokens : [];
|
|
543
|
+
if (candidates.length > 0) return [` --vibepro-${name}: var(${candidates[0]});`];
|
|
544
|
+
return [];
|
|
545
|
+
});
|
|
546
|
+
const themeAliases = cssVariables.map((token) => ` --vibepro-theme-${sanitizeId(token.replace(/^--/, ''))}: var(${token});`);
|
|
547
|
+
const colorValueTokens = colorValues.map((value, index) => ` --vibepro-color-${index + 1}: ${value};`);
|
|
548
|
+
const spacingValueTokens = spacingValues.map((value, index) => ` --vibepro-space-${index + 1}: ${value};`);
|
|
549
|
+
const declarations = unique([
|
|
550
|
+
...themeAliases,
|
|
551
|
+
...semanticAliases,
|
|
552
|
+
...colorValueTokens,
|
|
553
|
+
...spacingValueTokens
|
|
554
|
+
]);
|
|
555
|
+
if (declarations.length === 0) {
|
|
556
|
+
return {
|
|
557
|
+
schema_version: '0.1.0',
|
|
558
|
+
workflow: 'design-system-export',
|
|
559
|
+
design_system_id: designSystemId,
|
|
560
|
+
format: 'css',
|
|
561
|
+
status: 'needs_tokens',
|
|
562
|
+
content_type: 'text/css',
|
|
563
|
+
content: `/* VibePro Design System ${designSystemId}: needs_tokens - no semantic or theme tokens are available. */\n`
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
schema_version: '0.1.0',
|
|
568
|
+
workflow: 'design-system-export',
|
|
569
|
+
design_system_id: designSystemId,
|
|
570
|
+
format: 'css',
|
|
571
|
+
status: 'pass',
|
|
572
|
+
content_type: 'text/css',
|
|
573
|
+
content: [
|
|
574
|
+
`/* VibePro Design System export: ${designSystemId} */`,
|
|
575
|
+
':root {',
|
|
576
|
+
...declarations,
|
|
577
|
+
'}',
|
|
578
|
+
''
|
|
579
|
+
].join('\n')
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function writeDesignSystemArtifacts(outDir, designSystem) {
|
|
584
|
+
const artifacts = {
|
|
585
|
+
'design-system.json': designSystem,
|
|
586
|
+
'product-semantics.json': designSystem.product_semantics,
|
|
587
|
+
'theme-tokens.json': designSystem.theme_tokens,
|
|
588
|
+
'semantic-tokens.json': designSystem.semantic_tokens,
|
|
589
|
+
'component-roles.json': designSystem.component_roles,
|
|
590
|
+
'component-states.json': designSystem.component_states,
|
|
591
|
+
'screen-patterns.json': designSystem.screen_patterns,
|
|
592
|
+
'cta-policy.json': designSystem.cta_policy,
|
|
593
|
+
'density-policy.json': designSystem.density_policy,
|
|
594
|
+
'navigation-policy.json': designSystem.navigation_policy,
|
|
595
|
+
'anti-patterns.json': designSystem.anti_patterns,
|
|
596
|
+
'implementation-mapping.json': designSystem.implementation_mapping,
|
|
597
|
+
'evidence-coverage.json': designSystem.evidence_coverage,
|
|
598
|
+
'ds-gate.json': designSystem.ds_gate
|
|
599
|
+
};
|
|
600
|
+
if (designSystem.visual_foundations) {
|
|
601
|
+
artifacts['visual-foundations.json'] = designSystem.visual_foundations;
|
|
602
|
+
}
|
|
603
|
+
if (designSystem.external_bundle) {
|
|
604
|
+
artifacts['external-bundle.json'] = designSystem.external_bundle;
|
|
605
|
+
}
|
|
606
|
+
await Promise.all(Object.entries(artifacts).map(([fileName, content]) => (
|
|
607
|
+
writeFile(path.join(outDir, fileName), `${JSON.stringify(content, null, 2)}\n`)
|
|
608
|
+
)));
|
|
609
|
+
if (designSystem.visual_foundations) {
|
|
610
|
+
await writeFile(path.join(outDir, 'visual-foundations.md'), renderVisualFoundationsSummary(designSystem.visual_foundations, designSystem.output?.language ?? 'ja'));
|
|
611
|
+
}
|
|
612
|
+
await writeFile(path.join(outDir, 'design-system.md'), renderNativeDesignSystemSummary(designSystem, designSystem.output?.language ?? 'ja'));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function readVisualFoundations(root, { designSystemId, product, briefFile }) {
|
|
616
|
+
if (!briefFile) return null;
|
|
617
|
+
const absolutePath = path.isAbsolute(briefFile) ? briefFile : path.join(root, briefFile);
|
|
618
|
+
const text = await readFile(absolutePath, 'utf8');
|
|
619
|
+
return extractVisualFoundations({
|
|
620
|
+
designSystemId,
|
|
621
|
+
product,
|
|
622
|
+
source: path.relative(root, absolutePath).split(path.sep).join('/'),
|
|
623
|
+
text
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function extractVisualFoundations({ designSystemId, product, source, text }) {
|
|
628
|
+
const normalized = String(text ?? '');
|
|
629
|
+
return {
|
|
630
|
+
schema_version: '0.1.0',
|
|
631
|
+
design_system_id: designSystemId,
|
|
632
|
+
product,
|
|
633
|
+
source,
|
|
634
|
+
authority: 'visual_reference_only_current_code_and_gates_remain_authoritative',
|
|
635
|
+
design_language: extractLines(normalized, /(design language|tone|brand|らしさ|トーン|世界観)/i),
|
|
636
|
+
platform_density: extractLines(normalized, /(platform|mobile|desktop|density|compact|dense|scan|密度|モバイル)/i),
|
|
637
|
+
semantic_color_roles: extractLines(normalized, /(color|colour|semantic|surface|text|brand|success|warning|色|カラー)/i),
|
|
638
|
+
typography: extractLines(normalized, /(typography|font|type|line-height|文字|フォント|タイポ)/i),
|
|
639
|
+
spacing_radius_motion_shadow: extractLines(normalized, /(spacing|space|radius|radii|motion|shadow|elevation|余白|角丸|影|モーション)/i),
|
|
640
|
+
component_visual_requirements: extractLines(normalized, /(component|button|card|chip|sheet|navigation|cta|コンポーネント|カード|ボタン)/i),
|
|
641
|
+
composition_requirements: extractLines(normalized, /(composition|layout|screen|hierarchy|section|画面|構成|レイアウト|階層)/i),
|
|
642
|
+
native_cta_language: extractLines(normalized, /(cta|action|button|call to action|電話|確認|探す|予約|文言)/i),
|
|
643
|
+
forbidden_generic_ctas: extractForbiddenCtas(normalized),
|
|
644
|
+
authority_boundary: [
|
|
645
|
+
'current code, route evidence, implementation mapping, and VibePro gates remain implementation authority',
|
|
646
|
+
'visual foundations may guide tone, component feel, density, and composition but must not override preserved UX invariants'
|
|
647
|
+
]
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function extractLines(text, pattern) {
|
|
652
|
+
return unique(String(text ?? '')
|
|
653
|
+
.split(/\n+/)
|
|
654
|
+
.map((line) => line.replace(/^[-*#\s]+/, '').trim())
|
|
655
|
+
.filter((line) => line.length > 0 && line.length < 240)
|
|
656
|
+
.filter((line) => pattern.test(line)))
|
|
657
|
+
.slice(0, 24);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function extractForbiddenCtas(text) {
|
|
661
|
+
const lines = extractLines(text, /(forbidden|avoid|do not|generic|禁止|避ける|汎用)/i);
|
|
662
|
+
const known = [];
|
|
663
|
+
if (/book now/i.test(text)) known.push('Book Now');
|
|
664
|
+
if (/予約する/.test(text) && /禁止|avoid|generic|汎用/i.test(text)) known.push('予約する');
|
|
665
|
+
return unique([...known, ...lines]).slice(0, 16);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function mergeVisualFoundationsGate(dsGate, visualFoundations) {
|
|
669
|
+
if (!visualFoundations) return dsGate;
|
|
670
|
+
const base = dsGate ?? {
|
|
671
|
+
schema_version: '0.1.0',
|
|
672
|
+
fallback_allowed: false,
|
|
673
|
+
checks: []
|
|
674
|
+
};
|
|
675
|
+
const checks = [
|
|
676
|
+
...(base.checks ?? []).filter((check) => check.id !== 'DS-GATE-VISUAL-FOUNDATIONS-AUTHORITY'),
|
|
677
|
+
{
|
|
678
|
+
id: 'DS-GATE-VISUAL-FOUNDATIONS-AUTHORITY',
|
|
679
|
+
statement: 'Visual foundations are reference material only; current code, graph evidence, implementation mapping, and VibePro gates remain authoritative.'
|
|
680
|
+
}
|
|
681
|
+
];
|
|
682
|
+
return {
|
|
683
|
+
...base,
|
|
684
|
+
fallback_allowed: false,
|
|
685
|
+
checks
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function renderVisualFoundationsSummary(foundations, language = 'ja') {
|
|
690
|
+
return `${localizedText(language, { ja: `# Visual Foundations: ${foundations.product}`, en: `# Visual Foundations: ${foundations.product}` })}
|
|
691
|
+
|
|
692
|
+
- source: ${foundations.source}
|
|
693
|
+
- authority: ${foundations.authority}
|
|
694
|
+
|
|
695
|
+
## ${localizedText(language, { ja: '正本境界', en: 'Authority Boundary' })}
|
|
696
|
+
|
|
697
|
+
${foundations.authority_boundary.map((item) => `- ${item}`).join('\n')}
|
|
698
|
+
|
|
699
|
+
## ${localizedText(language, { ja: 'デザイン言語', en: 'Design Language' })}
|
|
700
|
+
|
|
701
|
+
${formatList(foundations.design_language)}
|
|
702
|
+
|
|
703
|
+
## ${localizedText(language, { ja: '色の役割', en: 'Color Roles' })}
|
|
704
|
+
|
|
705
|
+
${formatList(foundations.semantic_color_roles)}
|
|
706
|
+
|
|
707
|
+
## ${localizedText(language, { ja: 'Typography / Density / Motion', en: 'Typography / Density / Motion' })}
|
|
708
|
+
|
|
709
|
+
${formatList([
|
|
710
|
+
...foundations.typography,
|
|
711
|
+
...foundations.platform_density,
|
|
712
|
+
...foundations.spacing_radius_motion_shadow
|
|
713
|
+
])}
|
|
714
|
+
|
|
715
|
+
## ${localizedText(language, { ja: 'Components / Composition / CTA', en: 'Components / Composition / CTA' })}
|
|
716
|
+
|
|
717
|
+
${formatList([
|
|
718
|
+
...foundations.component_visual_requirements,
|
|
719
|
+
...foundations.composition_requirements,
|
|
720
|
+
...foundations.native_cta_language,
|
|
721
|
+
...foundations.forbidden_generic_ctas
|
|
722
|
+
])}
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function formatList(items) {
|
|
727
|
+
return items.length > 0 ? items.map((item) => `- ${item}`).join('\n') : '- not extracted';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function createBundleIngestBaseDesignSystem({ designSystemId, product }) {
|
|
731
|
+
return {
|
|
732
|
+
schema_version: '0.1.0',
|
|
733
|
+
workflow: 'native-design-system-bundle-ingest',
|
|
734
|
+
design_system_id: designSystemId,
|
|
735
|
+
product,
|
|
736
|
+
authority: 'vibepro_native_design_system',
|
|
737
|
+
external_generator_required: false,
|
|
738
|
+
source_evidence: {
|
|
739
|
+
routes: [],
|
|
740
|
+
graphify: emptyGraphifyEvidence(),
|
|
741
|
+
current_ui_code: [],
|
|
742
|
+
style_files: []
|
|
743
|
+
},
|
|
744
|
+
product_semantics: {
|
|
745
|
+
schema_version: '0.1.0',
|
|
746
|
+
product,
|
|
747
|
+
primary_domain: 'product_ui',
|
|
748
|
+
language_policy: 'preserve_current_product_language',
|
|
749
|
+
interaction_model: 'existing_product_workflow',
|
|
750
|
+
domain_concepts: []
|
|
751
|
+
},
|
|
752
|
+
screen_patterns: {
|
|
753
|
+
schema_version: '0.1.0',
|
|
754
|
+
graphify_status: 'not_available',
|
|
755
|
+
patterns: []
|
|
756
|
+
},
|
|
757
|
+
implementation_mapping: {
|
|
758
|
+
schema_version: '0.1.0',
|
|
759
|
+
mapping_source: 'external_bundle_reference_only',
|
|
760
|
+
screen_mappings: [],
|
|
761
|
+
source_file_sample: [],
|
|
762
|
+
shared_component_candidates: []
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function emptyGraphifyEvidence() {
|
|
768
|
+
return {
|
|
769
|
+
status: 'not_available',
|
|
770
|
+
graphify_executed: false,
|
|
771
|
+
artifact_dir: null,
|
|
772
|
+
route_count: 0,
|
|
773
|
+
component_count: 0,
|
|
774
|
+
edge_count: 0
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function extractBundlePayload(bundle) {
|
|
779
|
+
const source = bundle && typeof bundle === 'object' ? bundle : {};
|
|
780
|
+
const payload = source.bundle && typeof source.bundle === 'object' ? source.bundle : source;
|
|
781
|
+
return {
|
|
782
|
+
tokens: payload.tokens
|
|
783
|
+
?? payload.designTokens
|
|
784
|
+
?? payload.files?.tokens
|
|
785
|
+
?? source.semantic_tokens
|
|
786
|
+
?? source.theme_tokens
|
|
787
|
+
?? [payload.theme, payload.styles].filter(Boolean).join('\n')
|
|
788
|
+
?? {},
|
|
789
|
+
components: payload.components
|
|
790
|
+
?? source.files?.components
|
|
791
|
+
?? source.component_roles?.roles
|
|
792
|
+
?? source.component_roles
|
|
793
|
+
?? [payload.componentsCss, payload.componentsJs].filter(Boolean).join('\n')
|
|
794
|
+
?? [],
|
|
795
|
+
guidelines: payload.guidelines
|
|
796
|
+
?? source.files?.guidelines
|
|
797
|
+
?? source.overview
|
|
798
|
+
?? payload.documentation
|
|
799
|
+
?? []
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildExternalBundleReference({ root, bundlePath, designSystemId, sanitized, bundleSummary }) {
|
|
804
|
+
return {
|
|
805
|
+
schema_version: '0.1.0',
|
|
806
|
+
design_system_id: designSystemId,
|
|
807
|
+
source: path.relative(root, bundlePath).split(path.sep).join('/'),
|
|
808
|
+
authority: 'external_bundle_reference_only_current_code_and_vibepro_gates_remain_authoritative',
|
|
809
|
+
imported_at: new Date().toISOString(),
|
|
810
|
+
redacted_value_count: sanitized.redactedCount,
|
|
811
|
+
token_summary: bundleSummary.token_summary,
|
|
812
|
+
component_summary: bundleSummary.component_summary,
|
|
813
|
+
guideline_summary: bundleSummary.guideline_summary,
|
|
814
|
+
constraints: bundleSummary.constraints,
|
|
815
|
+
boundary: [
|
|
816
|
+
'external bundle content may inform DS tokens, component roles, state semantics, CTA policy, density, and navigation constraints',
|
|
817
|
+
'external bundle content must not override current code, Story, Spec, Architecture, or VibePro gates',
|
|
818
|
+
'raw external CSS/JS exports are not persisted as implementation authority'
|
|
819
|
+
]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function collectBundleTokenEvidence(tokens) {
|
|
824
|
+
const text = flattenText(tokens);
|
|
825
|
+
return {
|
|
826
|
+
schema_version: '0.1.0',
|
|
827
|
+
css_variables: unique([
|
|
828
|
+
...collectCssVariables(text),
|
|
829
|
+
...flattenKeys(tokens).filter((key) => /color|surface|text|space|font|radius|shadow|motion|state|semantic/i.test(key))
|
|
830
|
+
]).slice(0, 160),
|
|
831
|
+
class_hints: unique(collectClassHints(text)).slice(0, 80),
|
|
832
|
+
color_values: unique(collectColorValues(text)).slice(0, 80),
|
|
833
|
+
spacing_values: unique(collectSpacingValues(text)).slice(0, 80),
|
|
834
|
+
token_keys: unique(flattenKeys(tokens)).slice(0, 200)
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function collectBundleComponentEvidence(components) {
|
|
839
|
+
const text = flattenText(components);
|
|
840
|
+
const customElements = [...text.matchAll(/\b([a-z][a-z0-9]*-[a-z0-9-]+)\b/g)].map((match) => match[1]);
|
|
841
|
+
const names = Array.isArray(components)
|
|
842
|
+
? components.map((item) => typeof item === 'string' ? item : item?.name ?? item?.title ?? item?.role).filter(Boolean)
|
|
843
|
+
: flattenKeys(components);
|
|
844
|
+
return {
|
|
845
|
+
schema_version: '0.1.0',
|
|
846
|
+
names: unique([...names.map(String), ...customElements, ...collectClassHints(text)]).slice(0, 120)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function collectBundleGuidelineEvidence(guidelines) {
|
|
851
|
+
const text = flattenText(guidelines);
|
|
852
|
+
const topics = typeof guidelines === 'string'
|
|
853
|
+
? guidelines.split(/\n+/).map((line) => line.replace(/^[-*#\s]+/, '').trim()).filter(Boolean)
|
|
854
|
+
: flattenKeys(guidelines);
|
|
855
|
+
return {
|
|
856
|
+
schema_version: '0.1.0',
|
|
857
|
+
text,
|
|
858
|
+
topics: unique(topics.map(String)).slice(0, 120)
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function mergeThemeTokens(existing, tokenEvidence) {
|
|
863
|
+
return {
|
|
864
|
+
...(existing ?? {}),
|
|
865
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
866
|
+
css_variables: unique([...(existing?.css_variables ?? []), ...tokenEvidence.css_variables]).slice(0, 200),
|
|
867
|
+
class_hints: unique([...(existing?.class_hints ?? []), ...tokenEvidence.class_hints]).slice(0, 200),
|
|
868
|
+
color_values: unique([...(existing?.color_values ?? []), ...tokenEvidence.color_values]).slice(0, 120),
|
|
869
|
+
spacing_values: unique([...(existing?.spacing_values ?? []), ...tokenEvidence.spacing_values]).slice(0, 120),
|
|
870
|
+
external_bundle_token_keys: unique([...(existing?.external_bundle_token_keys ?? []), ...tokenEvidence.token_keys]).slice(0, 200)
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function mergeSemanticTokens(existing, tokenEvidence, guidelineEvidence) {
|
|
875
|
+
const existingRoles = existing?.color_roles ?? [];
|
|
876
|
+
return {
|
|
877
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
878
|
+
...(existing ?? {}),
|
|
879
|
+
color_roles: mergeNamedItems(existingRoles, inferExternalColorRoles(tokenEvidence, guidelineEvidence)).slice(0, 80),
|
|
880
|
+
state_semantics: unique([...(existing?.state_semantics ?? []), ...inferExternalStates(guidelineEvidence)]).slice(0, 40),
|
|
881
|
+
cta_priority: unique([...(existing?.cta_priority ?? []), 'primary', 'secondary', 'tertiary']).slice(0, 20),
|
|
882
|
+
domain_semantics: unique([...(existing?.domain_semantics ?? []), ...inferExternalDomainSemantics(guidelineEvidence)]).slice(0, 60)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function mergeComponentRoles(existing, componentEvidence) {
|
|
887
|
+
const inferred = componentEvidence.names.map((name) => ({
|
|
888
|
+
name: normalizeRoleName(name),
|
|
889
|
+
source: 'external_bundle_reference',
|
|
890
|
+
responsibility: inferComponentResponsibility(name)
|
|
891
|
+
}));
|
|
892
|
+
return {
|
|
893
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
894
|
+
roles: mergeNamedItems(existing?.roles ?? [], inferred).slice(0, 120)
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function mergeComponentStates(existing, guidelineEvidence) {
|
|
899
|
+
const states = inferExternalStates(guidelineEvidence);
|
|
900
|
+
return {
|
|
901
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
902
|
+
required_states: unique([...(existing?.required_states ?? []), ...states]).slice(0, 40),
|
|
903
|
+
discovered_states: unique([...(existing?.discovered_states ?? []), ...states]).slice(0, 40),
|
|
904
|
+
state_policy: unique([
|
|
905
|
+
...(existing?.state_policy ?? []),
|
|
906
|
+
'external bundle states are reference constraints and must be verified against current implementation',
|
|
907
|
+
'loading, disabled, error, empty, selected, success, available, limited, and unavailable states must stay visually distinguishable when present'
|
|
908
|
+
]).slice(0, 40)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function mergeCtaPolicy(existing, guidelineEvidence, componentEvidence) {
|
|
913
|
+
return {
|
|
914
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
915
|
+
hierarchy: existing?.hierarchy?.length > 0 ? existing.hierarchy : [
|
|
916
|
+
{ priority: 'primary', role: 'main product action', source: 'external_bundle_reference' },
|
|
917
|
+
{ priority: 'secondary', role: 'supporting navigation or refinement action', source: 'external_bundle_reference' },
|
|
918
|
+
{ priority: 'tertiary', role: 'low-emphasis utility action', source: 'external_bundle_reference' }
|
|
919
|
+
],
|
|
920
|
+
discovered_ctas: unique([...(existing?.discovered_ctas ?? []), ...inferExternalCtas(guidelineEvidence, componentEvidence)]).slice(0, 80),
|
|
921
|
+
rules: unique([
|
|
922
|
+
...(existing?.rules ?? []),
|
|
923
|
+
'external CTA labels are candidates only; preserve current product-native wording unless Story/Spec changes it',
|
|
924
|
+
'do not promote external secondary actions above the current primary product action'
|
|
925
|
+
]).slice(0, 40)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function mergeDensityPolicy(existing, guidelineEvidence, tokenEvidence) {
|
|
930
|
+
return {
|
|
931
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
932
|
+
policy: existing?.policy ?? inferDensityFromText(guidelineEvidence.text),
|
|
933
|
+
evidence: {
|
|
934
|
+
...(existing?.evidence ?? {}),
|
|
935
|
+
external_bundle_spacing_values: tokenEvidence.spacing_values.slice(0, 40),
|
|
936
|
+
external_bundle_topics: guidelineEvidence.topics.filter((topic) => /density|compact|spacing|layout|scan|grid|余白|密度/i.test(topic)).slice(0, 40)
|
|
937
|
+
},
|
|
938
|
+
rules: unique([
|
|
939
|
+
...(existing?.rules ?? []),
|
|
940
|
+
'external density guidance must not drop current required information',
|
|
941
|
+
'spacing and layout guidance remain subject to current screen invariants'
|
|
942
|
+
]).slice(0, 40)
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function mergeNavigationPolicy(existing, guidelineEvidence) {
|
|
947
|
+
return {
|
|
948
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
949
|
+
policy: existing?.policy ?? 'preserve_current_navigation_model',
|
|
950
|
+
discovered_targets: existing?.discovered_targets ?? [],
|
|
951
|
+
composition_rules: existing?.composition_rules ?? [],
|
|
952
|
+
rules: unique([
|
|
953
|
+
...(existing?.rules ?? []),
|
|
954
|
+
...guidelineEvidence.topics.filter((topic) => /nav|route|tab|back|menu|sheet|navigation|遷移|ナビ/i.test(topic)).slice(0, 12),
|
|
955
|
+
'external navigation guidance must not rewrite current route purpose or existing navigation anchors'
|
|
956
|
+
]).slice(0, 40)
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function mergeAntiPatterns(existing, guidelineEvidence) {
|
|
961
|
+
const forbidden = guidelineEvidence.topics.filter((topic) => /avoid|forbid|do not|never|禁止|避ける|anti/i.test(topic));
|
|
962
|
+
return {
|
|
963
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
964
|
+
items: uniqueItemsByStatement([...(existing?.items ?? []), ...forbidden.map((statement) => ({ statement, source: 'external_bundle_reference' }))]).slice(0, 80),
|
|
965
|
+
global_rules: unique([
|
|
966
|
+
...(existing?.global_rules ?? []),
|
|
967
|
+
'do not treat external bundle visuals as implementation authority',
|
|
968
|
+
'do not persist external secret values or service tokens',
|
|
969
|
+
'do not override current UX invariants with external bundle defaults'
|
|
970
|
+
]).slice(0, 40)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function mergeBundleEvidenceCoverage(existing, { tokenEvidence, componentEvidence, guidelineEvidence }) {
|
|
975
|
+
const findings = mergeFindings(existing?.findings ?? [], [
|
|
976
|
+
{
|
|
977
|
+
id: 'DS-EVIDENCE-EXTERNAL-BUNDLE-TOKENS',
|
|
978
|
+
status: tokenEvidence.css_variables.length > 0 || tokenEvidence.token_keys.length > 0 ? 'pass' : 'warn',
|
|
979
|
+
summary: `${tokenEvidence.css_variables.length + tokenEvidence.token_keys.length} external token signal(s) extracted`
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
id: 'DS-EVIDENCE-EXTERNAL-BUNDLE-COMPONENTS',
|
|
983
|
+
status: componentEvidence.names.length > 0 ? 'pass' : 'warn',
|
|
984
|
+
summary: `${componentEvidence.names.length} external component signal(s) extracted`
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
id: 'DS-EVIDENCE-EXTERNAL-BUNDLE-GUIDELINES',
|
|
988
|
+
status: guidelineEvidence.topics.length > 0 ? 'pass' : 'warn',
|
|
989
|
+
summary: `${guidelineEvidence.topics.length} external guideline topic(s) extracted`
|
|
990
|
+
}
|
|
991
|
+
]);
|
|
992
|
+
return {
|
|
993
|
+
schema_version: existing?.schema_version ?? '0.1.0',
|
|
994
|
+
status: findings.some((finding) => finding.status === 'fail')
|
|
995
|
+
? 'fail'
|
|
996
|
+
: findings.some((finding) => finding.status === 'warn')
|
|
997
|
+
? 'needs_review'
|
|
998
|
+
: 'pass',
|
|
999
|
+
findings
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function mergeExternalBundleGate(dsGate, externalBundle) {
|
|
1004
|
+
const base = dsGate ?? {
|
|
1005
|
+
schema_version: '0.1.0',
|
|
1006
|
+
fallback_allowed: false,
|
|
1007
|
+
checks: []
|
|
1008
|
+
};
|
|
1009
|
+
return {
|
|
1010
|
+
...base,
|
|
1011
|
+
fallback_allowed: false,
|
|
1012
|
+
checks: [
|
|
1013
|
+
...(base.checks ?? []).filter((check) => check.id !== 'DS-GATE-EXTERNAL-BUNDLE-AUTHORITY'),
|
|
1014
|
+
{
|
|
1015
|
+
id: 'DS-GATE-EXTERNAL-BUNDLE-AUTHORITY',
|
|
1016
|
+
statement: `External bundle ${externalBundle.source} is reference evidence only; VibePro-native DS, current code, Story/Spec/Architecture, and gates remain implementation authority. Redacted values: ${externalBundle.redacted_value_count}.`
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function collectGraphifyEvidence(root, options) {
|
|
1023
|
+
if (options.runGraphify) {
|
|
1024
|
+
const imported = await importGraphifyArtifacts(root, {
|
|
1025
|
+
runGraphify: true,
|
|
1026
|
+
sourceDir: options.graphifyOut ?? 'graphify-out'
|
|
1027
|
+
});
|
|
1028
|
+
return {
|
|
1029
|
+
status: 'imported',
|
|
1030
|
+
graphify_executed: imported.graphifyExecuted,
|
|
1031
|
+
artifact_dir: path.relative(root, imported.graphifyDir).split(path.sep).join('/')
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const graphPath = path.join(root, '.vibepro', 'graphify', 'graph.json');
|
|
1035
|
+
try {
|
|
1036
|
+
const graph = JSON.parse(await readFile(graphPath, 'utf8'));
|
|
1037
|
+
return summarizeGraphifyGraph(graph, path.relative(root, graphPath).split(path.sep).join('/'));
|
|
1038
|
+
} catch {
|
|
1039
|
+
return {
|
|
1040
|
+
status: 'not_available',
|
|
1041
|
+
graphify_executed: false,
|
|
1042
|
+
artifact_dir: null,
|
|
1043
|
+
route_count: 0,
|
|
1044
|
+
component_count: 0,
|
|
1045
|
+
edge_count: 0
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function summarizeGraphifyGraph(graph, artifact) {
|
|
1051
|
+
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
1052
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
1053
|
+
const nodeText = nodes.map((node) => JSON.stringify(node)).join('\n');
|
|
1054
|
+
return {
|
|
1055
|
+
status: 'available',
|
|
1056
|
+
graphify_executed: false,
|
|
1057
|
+
artifact,
|
|
1058
|
+
route_count: countMatches(nodeText, /route|page|screen|\/api\//gi),
|
|
1059
|
+
component_count: countMatches(nodeText, /component|tsx|jsx/gi),
|
|
1060
|
+
edge_count: edges.length
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function collectStyleEvidence(root) {
|
|
1065
|
+
const files = (await listFiles(root))
|
|
1066
|
+
.filter((file) => STYLE_EXTENSIONS.has(path.extname(file)) || /tailwind\.config|theme|tokens/i.test(file))
|
|
1067
|
+
.slice(0, 80);
|
|
1068
|
+
const reports = [];
|
|
1069
|
+
for (const file of files) {
|
|
1070
|
+
const content = await readFile(path.join(root, file), 'utf8').catch(() => '');
|
|
1071
|
+
reports.push({
|
|
1072
|
+
path: file,
|
|
1073
|
+
css_variables: collectCssVariables(content),
|
|
1074
|
+
class_hints: collectClassHints(content),
|
|
1075
|
+
color_values: collectColorValues(content),
|
|
1076
|
+
spacing_values: collectSpacingValues(content)
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
files: reports,
|
|
1081
|
+
theme_tokens: {
|
|
1082
|
+
css_variables: unique(reports.flatMap((file) => file.css_variables)).slice(0, 160),
|
|
1083
|
+
class_hints: unique(reports.flatMap((file) => file.class_hints)).slice(0, 160),
|
|
1084
|
+
color_values: unique(reports.flatMap((file) => file.color_values)).slice(0, 80),
|
|
1085
|
+
spacing_values: unique(reports.flatMap((file) => file.spacing_values)).slice(0, 80)
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function collectSourceEvidence(root) {
|
|
1091
|
+
const files = (await listFiles(root))
|
|
1092
|
+
.filter((file) => SOURCE_EXTENSIONS.has(path.extname(file)))
|
|
1093
|
+
.filter((file) => /component|app|pages|ui|screen|layout|route/i.test(file))
|
|
1094
|
+
.slice(0, 160);
|
|
1095
|
+
return {
|
|
1096
|
+
file_count: files.length,
|
|
1097
|
+
files: files.map((file) => ({ path: file }))
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function buildRoutePatterns({ screens, graphify }) {
|
|
1102
|
+
return {
|
|
1103
|
+
schema_version: '0.1.0',
|
|
1104
|
+
graphify_status: graphify.status,
|
|
1105
|
+
patterns: screens.map((screen) => ({
|
|
1106
|
+
route: screen.route,
|
|
1107
|
+
intent: inferIntentFromScreen(screen),
|
|
1108
|
+
files: screen.evidence.files.map((file) => file.path),
|
|
1109
|
+
component_names: unique(screen.evidence.files.flatMap((file) => file.components)).slice(0, 24),
|
|
1110
|
+
current_ctas: unique(screen.evidence.files.flatMap((file) => file.ctas)).slice(0, 16),
|
|
1111
|
+
state_names: unique(screen.evidence.files.flatMap((file) => file.states)).slice(0, 16),
|
|
1112
|
+
data_dependencies: unique(screen.evidence.files.flatMap((file) => file.data_dependencies)).slice(0, 16),
|
|
1113
|
+
navigation_targets: unique(screen.evidence.files.flatMap((file) => file.navigation)).slice(0, 16),
|
|
1114
|
+
ux_invariants: [
|
|
1115
|
+
'preserve route purpose',
|
|
1116
|
+
'preserve CTA order unless Story or Spec changes it',
|
|
1117
|
+
'preserve data dependency shape unless implementation evidence changes it'
|
|
1118
|
+
]
|
|
1119
|
+
}))
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function buildImplementationMapping({ screens, sourceEvidence, graphify }) {
|
|
1124
|
+
const screenMappings = screens.map((screen) => ({
|
|
1125
|
+
route: screen.route,
|
|
1126
|
+
files: screen.evidence.files.map((file) => file.path),
|
|
1127
|
+
components: unique(screen.evidence.files.flatMap((file) => file.components)).slice(0, 32),
|
|
1128
|
+
states: unique(screen.evidence.files.flatMap((file) => file.states)).slice(0, 32),
|
|
1129
|
+
data_dependencies: unique(screen.evidence.files.flatMap((file) => file.data_dependencies)).slice(0, 32)
|
|
1130
|
+
}));
|
|
1131
|
+
return {
|
|
1132
|
+
schema_version: '0.1.0',
|
|
1133
|
+
mapping_source: graphify.status === 'available' || graphify.status === 'imported'
|
|
1134
|
+
? 'current_code_and_graphify'
|
|
1135
|
+
: 'current_code',
|
|
1136
|
+
screen_mappings: screenMappings,
|
|
1137
|
+
source_file_sample: sourceEvidence.files.slice(0, 60),
|
|
1138
|
+
shared_component_candidates: unique(screenMappings.flatMap((mapping) => mapping.components)).slice(0, 80)
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function buildSemanticTokens({ derivedDesignSystem, styleEvidence }) {
|
|
1143
|
+
const cssVars = styleEvidence.theme_tokens.css_variables;
|
|
1144
|
+
return {
|
|
1145
|
+
schema_version: '0.1.0',
|
|
1146
|
+
color_roles: derivedDesignSystem.semantic_tokens.color_roles.map((role) => ({
|
|
1147
|
+
...role,
|
|
1148
|
+
candidate_tokens: cssVars.filter((token) => tokenMatchesRole(token, role.name, role.purpose)).slice(0, 12)
|
|
1149
|
+
})),
|
|
1150
|
+
state_semantics: derivedDesignSystem.semantic_tokens.state_semantics,
|
|
1151
|
+
cta_priority: derivedDesignSystem.semantic_tokens.cta_priority,
|
|
1152
|
+
domain_semantics: derivedDesignSystem.semantic_tokens.domain_semantics,
|
|
1153
|
+
raw_token_coverage: {
|
|
1154
|
+
css_variable_count: cssVars.length,
|
|
1155
|
+
color_value_count: styleEvidence.theme_tokens.color_values.length,
|
|
1156
|
+
spacing_value_count: styleEvidence.theme_tokens.spacing_values.length
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function buildStateSemantics({ derivedDesignSystem, screens }) {
|
|
1162
|
+
const discoveredStates = unique(screens.flatMap((screen) => (
|
|
1163
|
+
screen.evidence.files.flatMap((file) => file.states)
|
|
1164
|
+
)));
|
|
1165
|
+
return {
|
|
1166
|
+
schema_version: '0.1.0',
|
|
1167
|
+
required_states: derivedDesignSystem.semantic_tokens.state_semantics,
|
|
1168
|
+
discovered_states: discoveredStates,
|
|
1169
|
+
state_policy: [
|
|
1170
|
+
'states must be visually distinguishable',
|
|
1171
|
+
'disabled and loading states must not look actionable',
|
|
1172
|
+
'error and empty states need explicit copy or affordance evidence'
|
|
1173
|
+
]
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function buildCtaPolicy({ derivedDesignSystem, screens }) {
|
|
1178
|
+
return {
|
|
1179
|
+
schema_version: '0.1.0',
|
|
1180
|
+
hierarchy: derivedDesignSystem.cta_hierarchy,
|
|
1181
|
+
discovered_ctas: unique(screens.flatMap((screen) => (
|
|
1182
|
+
screen.evidence.files.flatMap((file) => file.ctas)
|
|
1183
|
+
))).slice(0, 60),
|
|
1184
|
+
rules: [
|
|
1185
|
+
'preserve product-native primary action wording unless Story or Spec changes it',
|
|
1186
|
+
'do not promote secondary navigation above the primary domain action',
|
|
1187
|
+
'route-level CTA changes require current route regression evidence'
|
|
1188
|
+
]
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function buildDensityPolicy({ derivedDesignSystem, styleEvidence }) {
|
|
1193
|
+
return {
|
|
1194
|
+
schema_version: '0.1.0',
|
|
1195
|
+
policy: derivedDesignSystem.foundations.density_policy,
|
|
1196
|
+
evidence: {
|
|
1197
|
+
spacing_values: styleEvidence.theme_tokens.spacing_values.slice(0, 40),
|
|
1198
|
+
compact_class_hints: styleEvidence.theme_tokens.class_hints.filter((item) => /compact|dense|small|sm|xs|gap|space/i.test(item)).slice(0, 40)
|
|
1199
|
+
},
|
|
1200
|
+
rules: [
|
|
1201
|
+
'improve scanability without dropping required information',
|
|
1202
|
+
'keep repeated item dimensions stable across loading, hover, and selected states',
|
|
1203
|
+
'do not replace operational density with marketing composition'
|
|
1204
|
+
]
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function buildNavigationPolicy({ derivedDesignSystem, screens }) {
|
|
1209
|
+
const targets = unique(screens.flatMap((screen) => (
|
|
1210
|
+
screen.evidence.files.flatMap((file) => file.navigation)
|
|
1211
|
+
)));
|
|
1212
|
+
return {
|
|
1213
|
+
schema_version: '0.1.0',
|
|
1214
|
+
policy: 'preserve_current_navigation_model',
|
|
1215
|
+
discovered_targets: targets.slice(0, 80),
|
|
1216
|
+
composition_rules: derivedDesignSystem.composition_guidelines.rules,
|
|
1217
|
+
rules: [
|
|
1218
|
+
'preserve route purpose and existing navigation anchors',
|
|
1219
|
+
'navigation model changes require Story or Spec evidence',
|
|
1220
|
+
'back, tab, and primary route transitions must stay reviewable'
|
|
1221
|
+
]
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function buildAntiPatterns(derivedDesignSystem) {
|
|
1226
|
+
return {
|
|
1227
|
+
schema_version: '0.1.0',
|
|
1228
|
+
items: derivedDesignSystem.anti_patterns,
|
|
1229
|
+
global_rules: [
|
|
1230
|
+
'do not invent a new product concept',
|
|
1231
|
+
'do not invent backend data or unavailable states',
|
|
1232
|
+
'do not collapse product workflows into a landing page',
|
|
1233
|
+
'do not implement visual candidates without DS gate review'
|
|
1234
|
+
]
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function buildEvidenceCoverage({ screens, styleEvidence, sourceEvidence, graphify, semanticTokens, implementationMapping }) {
|
|
1239
|
+
const findings = [
|
|
1240
|
+
{
|
|
1241
|
+
id: 'DS-EVIDENCE-ROUTES',
|
|
1242
|
+
status: screens.some((screen) => screen.evidence.files.length > 0) ? 'pass' : 'warn',
|
|
1243
|
+
summary: `${screens.filter((screen) => screen.evidence.files.length > 0).length}/${screens.length} routes have code evidence`
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
id: 'DS-EVIDENCE-STYLES',
|
|
1247
|
+
status: styleEvidence.files.length > 0 ? 'pass' : 'warn',
|
|
1248
|
+
summary: `${styleEvidence.files.length} style or token files scanned`
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: 'DS-EVIDENCE-GRAPH',
|
|
1252
|
+
status: graphify.status === 'available' || graphify.status === 'imported' ? 'pass' : 'warn',
|
|
1253
|
+
summary: graphify.status === 'not_available' ? 'graph evidence not available; derived from code only' : 'graph evidence available'
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
id: 'DS-EVIDENCE-IMPLEMENTATION',
|
|
1257
|
+
status: implementationMapping.screen_mappings.some((mapping) => mapping.files.length > 0) ? 'pass' : 'warn',
|
|
1258
|
+
summary: `${sourceEvidence.file_count} source files sampled for implementation mapping`
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
id: 'DS-EVIDENCE-SEMANTICS',
|
|
1262
|
+
status: semanticTokens.color_roles.length > 0 ? 'pass' : 'fail',
|
|
1263
|
+
summary: `${semanticTokens.color_roles.length} semantic color roles derived`
|
|
1264
|
+
}
|
|
1265
|
+
];
|
|
1266
|
+
return {
|
|
1267
|
+
schema_version: '0.1.0',
|
|
1268
|
+
status: findings.some((finding) => finding.status === 'fail')
|
|
1269
|
+
? 'fail'
|
|
1270
|
+
: findings.some((finding) => finding.status === 'warn')
|
|
1271
|
+
? 'needs_review'
|
|
1272
|
+
: 'pass',
|
|
1273
|
+
findings
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function summarizeScreenEvidence(screens) {
|
|
1278
|
+
return screens.map((screen) => ({
|
|
1279
|
+
route: screen.route,
|
|
1280
|
+
file_count: screen.evidence.files.length,
|
|
1281
|
+
component_count: unique(screen.evidence.files.flatMap((file) => file.components)).length,
|
|
1282
|
+
cta_count: unique(screen.evidence.files.flatMap((file) => file.ctas)).length,
|
|
1283
|
+
state_count: unique(screen.evidence.files.flatMap((file) => file.states)).length
|
|
1284
|
+
}));
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function collectDesignValidationStoryContext(root, storyId) {
|
|
1288
|
+
const files = (await listFiles(root))
|
|
1289
|
+
.filter((file) => /\.(md|mdx|json)$/.test(file))
|
|
1290
|
+
.filter((file) => !file.startsWith('.vibepro/'))
|
|
1291
|
+
.filter((file) => file.includes(storyId) || /docs\/(management\/stories|specs|architecture)\//.test(file))
|
|
1292
|
+
.slice(0, 120);
|
|
1293
|
+
const sources = [];
|
|
1294
|
+
for (const file of files) {
|
|
1295
|
+
const absolutePath = path.join(root, file);
|
|
1296
|
+
const text = await readFile(absolutePath, 'utf8').catch(() => '');
|
|
1297
|
+
if (!text.includes(storyId) && !file.includes(storyId)) continue;
|
|
1298
|
+
sources.push({
|
|
1299
|
+
path: file,
|
|
1300
|
+
kind: inferDesignValidationSourceKind(file),
|
|
1301
|
+
excerpt: text.slice(0, 4000)
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
const combined = sources.map((source) => source.excerpt).join('\n');
|
|
1305
|
+
return {
|
|
1306
|
+
story_id: storyId,
|
|
1307
|
+
sources: sources.map(({ path: sourcePath, kind }) => ({ path: sourcePath, kind })),
|
|
1308
|
+
ui_signal: /ui|ux|screen|visual|design|cta|component|navigation|density|画面|導線|見た目|コンポーネント/.test(combined),
|
|
1309
|
+
design_system_signal: /design system|design-system|ds|token|component role|state semantic|デザインシステム/.test(combined),
|
|
1310
|
+
cta_signal: /cta|button|action|primary|secondary|ボタン|導線/.test(combined),
|
|
1311
|
+
state_signal: /state|loading|disabled|error|empty|selected|状態|読込|エラー/.test(combined),
|
|
1312
|
+
navigation_signal: /navigation|route|tab|back|link|遷移|ナビ|導線/.test(combined),
|
|
1313
|
+
density_signal: /density|compact|scan|spacing|dense|情報密度|余白/.test(combined)
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function inferDesignValidationSourceKind(file) {
|
|
1318
|
+
if (file.includes('/management/stories/')) return 'story';
|
|
1319
|
+
if (file.includes('/specs/')) return 'spec';
|
|
1320
|
+
if (file.includes('/architecture/')) return 'architecture';
|
|
1321
|
+
return 'context';
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function readDesignSystemArtifactTexts(outDir) {
|
|
1325
|
+
const entries = await readdir(outDir, { withFileTypes: true }).catch(() => []);
|
|
1326
|
+
const texts = [];
|
|
1327
|
+
for (const entry of entries) {
|
|
1328
|
+
if (!entry.isFile() || !/\.(json|md|css|js|txt)$/.test(entry.name)) continue;
|
|
1329
|
+
const filePath = path.join(outDir, entry.name);
|
|
1330
|
+
texts.push({
|
|
1331
|
+
path: entry.name,
|
|
1332
|
+
text: await readFile(filePath, 'utf8').catch(() => '')
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
return texts;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function validateDesignSystemShape(designSystem) {
|
|
1339
|
+
const findings = [];
|
|
1340
|
+
findings.push(validationFinding({
|
|
1341
|
+
id: 'DS-VALIDATE-DRIFT',
|
|
1342
|
+
status: designSystem.authority === 'vibepro_native_design_system' ? 'pass' : 'block',
|
|
1343
|
+
summary: designSystem.authority === 'vibepro_native_design_system'
|
|
1344
|
+
? 'Design System authority is VibePro-native.'
|
|
1345
|
+
: `Design System authority is ${designSystem.authority ?? 'missing'}; implementation must not trust external/generated DS as authoritative.`
|
|
1346
|
+
}));
|
|
1347
|
+
const ctaHierarchy = designSystem.cta_policy?.hierarchy;
|
|
1348
|
+
findings.push(validationFinding({
|
|
1349
|
+
id: 'DS-VALIDATE-CTA-PRIORITY',
|
|
1350
|
+
status: Array.isArray(ctaHierarchy) && ctaHierarchy.length > 0 ? 'pass' : 'needs_evidence',
|
|
1351
|
+
summary: Array.isArray(ctaHierarchy) && ctaHierarchy.length > 0
|
|
1352
|
+
? `${ctaHierarchy.length} CTA hierarchy item(s) are defined.`
|
|
1353
|
+
: 'CTA priority hierarchy is missing.'
|
|
1354
|
+
}));
|
|
1355
|
+
const requiredStates = designSystem.component_states?.required_states;
|
|
1356
|
+
findings.push(validationFinding({
|
|
1357
|
+
id: 'DS-VALIDATE-STATE-SEMANTICS',
|
|
1358
|
+
status: Array.isArray(requiredStates) && requiredStates.length > 0 ? 'pass' : 'needs_evidence',
|
|
1359
|
+
summary: Array.isArray(requiredStates) && requiredStates.length > 0
|
|
1360
|
+
? `${requiredStates.length} state semantic rule(s) are defined.`
|
|
1361
|
+
: 'State semantics are missing.'
|
|
1362
|
+
}));
|
|
1363
|
+
const componentRoles = designSystem.component_roles?.roles;
|
|
1364
|
+
findings.push(validationFinding({
|
|
1365
|
+
id: 'DS-VALIDATE-COMPONENT-ROLES',
|
|
1366
|
+
status: Array.isArray(componentRoles) && componentRoles.length > 0 ? 'pass' : 'needs_evidence',
|
|
1367
|
+
summary: Array.isArray(componentRoles) && componentRoles.length > 0
|
|
1368
|
+
? `${componentRoles.length} component role(s) are defined.`
|
|
1369
|
+
: 'Component roles are missing.'
|
|
1370
|
+
}));
|
|
1371
|
+
const navigationRules = designSystem.navigation_policy?.rules;
|
|
1372
|
+
const densityRules = designSystem.density_policy?.rules;
|
|
1373
|
+
findings.push(validationFinding({
|
|
1374
|
+
id: 'DS-VALIDATE-NAV-DENSITY',
|
|
1375
|
+
status: Array.isArray(navigationRules) && navigationRules.length > 0 && Array.isArray(densityRules) && densityRules.length > 0 ? 'pass' : 'needs_evidence',
|
|
1376
|
+
summary: Array.isArray(navigationRules) && navigationRules.length > 0 && Array.isArray(densityRules) && densityRules.length > 0
|
|
1377
|
+
? 'Navigation and density policies are both defined.'
|
|
1378
|
+
: 'Navigation or density policy is missing.'
|
|
1379
|
+
}));
|
|
1380
|
+
return findings;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function validateDesignSystemStoryDrift({ designSystem, storyContext }) {
|
|
1384
|
+
const findings = [];
|
|
1385
|
+
findings.push(validationFinding({
|
|
1386
|
+
id: 'DS-VALIDATE-STORY-CONTEXT',
|
|
1387
|
+
status: storyContext.sources.length > 0 ? 'pass' : 'needs_evidence',
|
|
1388
|
+
summary: storyContext.sources.length > 0
|
|
1389
|
+
? `${storyContext.sources.length} Story/Spec/Architecture source(s) found.`
|
|
1390
|
+
: 'No Story/Spec/Architecture context found for this story.'
|
|
1391
|
+
}));
|
|
1392
|
+
const hasUiSignal = storyContext.ui_signal || storyContext.design_system_signal;
|
|
1393
|
+
findings.push(validationFinding({
|
|
1394
|
+
id: 'DS-VALIDATE-STORY-UI-SIGNAL',
|
|
1395
|
+
status: hasUiSignal ? 'pass' : 'needs_review',
|
|
1396
|
+
summary: hasUiSignal
|
|
1397
|
+
? 'Story context contains UI/Design System signals.'
|
|
1398
|
+
: 'Story context does not clearly say this is a UI/Design System change.'
|
|
1399
|
+
}));
|
|
1400
|
+
const missing = [];
|
|
1401
|
+
if (storyContext.cta_signal && !(designSystem.cta_policy?.hierarchy?.length > 0)) missing.push('cta_policy.hierarchy');
|
|
1402
|
+
if (storyContext.state_signal && !(designSystem.component_states?.required_states?.length > 0)) missing.push('component_states.required_states');
|
|
1403
|
+
if (storyContext.navigation_signal && !(designSystem.navigation_policy?.rules?.length > 0)) missing.push('navigation_policy.rules');
|
|
1404
|
+
if (storyContext.density_signal && !(designSystem.density_policy?.rules?.length > 0)) missing.push('density_policy.rules');
|
|
1405
|
+
findings.push(validationFinding({
|
|
1406
|
+
id: 'DS-VALIDATE-STORY-DS-ALIGNMENT',
|
|
1407
|
+
status: missing.length === 0 ? 'pass' : 'needs_review',
|
|
1408
|
+
summary: missing.length === 0
|
|
1409
|
+
? 'Story signals are covered by Design System sections.'
|
|
1410
|
+
: `Story signals require missing DS sections: ${missing.join(', ')}.`
|
|
1411
|
+
}));
|
|
1412
|
+
return findings;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function validateSecretLeakage(artifactTexts) {
|
|
1416
|
+
const matches = [];
|
|
1417
|
+
for (const artifact of artifactTexts) {
|
|
1418
|
+
if (hasLikelySecretMaterial(artifact.text)) matches.push(artifact.path);
|
|
1419
|
+
}
|
|
1420
|
+
return [validationFinding({
|
|
1421
|
+
id: 'DS-VALIDATE-SECRET-SCAN',
|
|
1422
|
+
status: matches.length > 0 ? 'block' : 'pass',
|
|
1423
|
+
summary: matches.length > 0
|
|
1424
|
+
? `Potential secret material found in DS artifacts: ${matches.join(', ')}.`
|
|
1425
|
+
: 'No likely secret material detected in DS artifacts.'
|
|
1426
|
+
})];
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function validationFinding({ id, status, summary }) {
|
|
1430
|
+
return {
|
|
1431
|
+
id,
|
|
1432
|
+
status,
|
|
1433
|
+
summary,
|
|
1434
|
+
release_blocking: status === 'block'
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function summarizeValidationStatus(findings) {
|
|
1439
|
+
const block = findings.filter((finding) => finding.status === 'block').length;
|
|
1440
|
+
const needsEvidence = findings.filter((finding) => finding.status === 'needs_evidence').length;
|
|
1441
|
+
const needsReview = findings.filter((finding) => finding.status === 'needs_review').length;
|
|
1442
|
+
return {
|
|
1443
|
+
status: block > 0 ? 'block' : needsEvidence > 0 ? 'needs_evidence' : needsReview > 0 ? 'needs_review' : 'pass',
|
|
1444
|
+
pass: findings.filter((finding) => finding.status === 'pass').length,
|
|
1445
|
+
needs_review: needsReview,
|
|
1446
|
+
needs_evidence: needsEvidence,
|
|
1447
|
+
block
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function readJsonIfExists(filePath) {
|
|
1452
|
+
try {
|
|
1453
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
if (error.code === 'ENOENT') return null;
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function sanitizeExternalBundle(value) {
|
|
1461
|
+
let redactedCount = 0;
|
|
1462
|
+
const sanitize = (item, key = '') => {
|
|
1463
|
+
if (typeof item === 'string') {
|
|
1464
|
+
if (isSecretKey(key)) {
|
|
1465
|
+
redactedCount += 1;
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
const redacted = redactLikelySecretText(item);
|
|
1469
|
+
redactedCount += redacted.count;
|
|
1470
|
+
return redacted.text;
|
|
1471
|
+
}
|
|
1472
|
+
if (Array.isArray(item)) {
|
|
1473
|
+
return item.map((entry) => sanitize(entry, key)).filter((entry) => entry !== undefined);
|
|
1474
|
+
}
|
|
1475
|
+
if (item && typeof item === 'object') {
|
|
1476
|
+
const next = {};
|
|
1477
|
+
for (const [entryKey, entryValue] of Object.entries(item)) {
|
|
1478
|
+
if (isSecretKey(entryKey) && typeof entryValue === 'string') {
|
|
1479
|
+
redactedCount += 1;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
const sanitized = sanitize(entryValue, entryKey);
|
|
1483
|
+
if (sanitized !== undefined) next[entryKey] = sanitized;
|
|
1484
|
+
}
|
|
1485
|
+
return next;
|
|
1486
|
+
}
|
|
1487
|
+
return item;
|
|
1488
|
+
};
|
|
1489
|
+
return {
|
|
1490
|
+
value: sanitize(value),
|
|
1491
|
+
redactedCount
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function isSecretKey(key) {
|
|
1496
|
+
const text = String(key ?? '');
|
|
1497
|
+
return /secret|password|passwd|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|api[_-]?token|bearer|credential|^token$/i.test(text);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function hasLikelySecretMaterial(value) {
|
|
1501
|
+
return secretMaterialPatterns().some((pattern) => pattern.test(String(value ?? '')));
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function redactLikelySecretText(value) {
|
|
1505
|
+
const text = String(value ?? '');
|
|
1506
|
+
let count = 0;
|
|
1507
|
+
let redacted = text;
|
|
1508
|
+
for (const pattern of secretMaterialPatterns()) {
|
|
1509
|
+
redacted = redacted.replace(pattern, (...args) => {
|
|
1510
|
+
count += 1;
|
|
1511
|
+
const prefix = typeof args[1] === 'string' && pattern.source.startsWith('((?:') ? args[1] : null;
|
|
1512
|
+
return prefix ? `${prefix}[REDACTED:secret]` : '[REDACTED:secret]';
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
return { text: redacted, count };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function secretMaterialPatterns() {
|
|
1519
|
+
return [
|
|
1520
|
+
/sk_live_[A-Za-z0-9_]{16,}/g,
|
|
1521
|
+
/ghp_[A-Za-z0-9_]{24,}/g,
|
|
1522
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
|
|
1523
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
1524
|
+
/Bearer\s+[A-Za-z0-9._-]{24,}/gi,
|
|
1525
|
+
/((?:password|passwd|secret|credential|api[_-]?key|api[_-]?token|access[_-]?token|refresh[_-]?token|auth[_-]?token|private[_-]?key)["']?\s*[:=]\s*["']?)([^"'\s,;)}\]]{4,})/gi
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function flattenText(value) {
|
|
1530
|
+
if (typeof value === 'string') return value;
|
|
1531
|
+
if (Array.isArray(value)) return value.map(flattenText).join('\n');
|
|
1532
|
+
if (value && typeof value === 'object') {
|
|
1533
|
+
return Object.entries(value).map(([key, item]) => `${key}\n${flattenText(item)}`).join('\n');
|
|
1534
|
+
}
|
|
1535
|
+
return '';
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function flattenKeys(value, prefix = '') {
|
|
1539
|
+
if (typeof value === 'string') return value ? [prefix || value] : [];
|
|
1540
|
+
if (Array.isArray(value)) {
|
|
1541
|
+
return value.flatMap((item, index) => flattenKeys(item, prefix ? `${prefix}.${index}` : String(index)));
|
|
1542
|
+
}
|
|
1543
|
+
if (value && typeof value === 'object') {
|
|
1544
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
1545
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1546
|
+
if (item && typeof item === 'object') return flattenKeys(item, nextPrefix);
|
|
1547
|
+
return [nextPrefix];
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return prefix ? [prefix] : [];
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function inferExternalColorRoles(tokenEvidence, guidelineEvidence) {
|
|
1554
|
+
const text = `${tokenEvidence.css_variables.join(' ')} ${tokenEvidence.token_keys.join(' ')} ${guidelineEvidence.text}`.toLowerCase();
|
|
1555
|
+
const roleSpecs = [
|
|
1556
|
+
['brand', /brand|primary|interactive|accent/],
|
|
1557
|
+
['surface', /surface|background|card|sheet/],
|
|
1558
|
+
['text', /text|foreground|muted|label/],
|
|
1559
|
+
['success', /success|available|positive/],
|
|
1560
|
+
['warning', /warning|caution|limited|urgency/],
|
|
1561
|
+
['error', /error|danger|negative/],
|
|
1562
|
+
['disabled', /disabled|inactive/],
|
|
1563
|
+
['selected', /selected|active/]
|
|
1564
|
+
];
|
|
1565
|
+
return roleSpecs
|
|
1566
|
+
.filter(([, pattern]) => pattern.test(text))
|
|
1567
|
+
.map(([name]) => ({
|
|
1568
|
+
name,
|
|
1569
|
+
purpose: `External bundle candidate role: ${name}`,
|
|
1570
|
+
source: 'external_bundle_reference',
|
|
1571
|
+
candidate_tokens: tokenEvidence.css_variables.filter((token) => token.toLowerCase().includes(name)).slice(0, 12)
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function inferExternalStates(guidelineEvidence) {
|
|
1576
|
+
const text = `${guidelineEvidence.text} ${guidelineEvidence.topics.join(' ')}`;
|
|
1577
|
+
const states = ['loading', 'empty', 'error', 'selected', 'disabled', 'success', 'available', 'limited', 'unavailable'];
|
|
1578
|
+
return states.filter((state) => new RegExp(state, 'i').test(text));
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function inferExternalDomainSemantics(guidelineEvidence) {
|
|
1582
|
+
return guidelineEvidence.topics
|
|
1583
|
+
.filter((topic) => /search|map|hotel|booking|inventory|availability|filter|detail|result|domain|concept/i.test(topic))
|
|
1584
|
+
.slice(0, 40);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function inferExternalCtas(guidelineEvidence, componentEvidence) {
|
|
1588
|
+
const text = `${guidelineEvidence.text}\n${componentEvidence.names.join('\n')}`;
|
|
1589
|
+
return unique([
|
|
1590
|
+
...[...text.matchAll(/(?:CTA|Action|Button|ボタン|導線)[:\s-]+([^\n.。]+)/gi)].map((match) => match[1].trim()),
|
|
1591
|
+
...componentEvidence.names.filter((name) => /cta|button|action|submit|confirm|reserve|search|電話/i.test(name))
|
|
1592
|
+
]).slice(0, 40);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function inferDensityFromText(text) {
|
|
1596
|
+
if (/dense|compact|scan|密度|一覧|comparison/i.test(text)) return 'preserve_dense_scannable_product_layout';
|
|
1597
|
+
return 'preserve_current_information_density';
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function normalizeRoleName(value) {
|
|
1601
|
+
const raw = String(value ?? 'component').trim();
|
|
1602
|
+
const parts = raw
|
|
1603
|
+
.replace(/^ds[-_]/i, '')
|
|
1604
|
+
.split(/[^A-Za-z0-9]+/)
|
|
1605
|
+
.filter(Boolean);
|
|
1606
|
+
const pascal = parts.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join('');
|
|
1607
|
+
return pascal || raw;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function inferComponentResponsibility(name) {
|
|
1611
|
+
const text = String(name ?? '').toLowerCase();
|
|
1612
|
+
if (/cta|button|action|confirm|電話/.test(text)) return 'primary or secondary action surface';
|
|
1613
|
+
if (/card|result|hotel|item/.test(text)) return 'structured result or entity display';
|
|
1614
|
+
if (/sheet|modal|drawer/.test(text)) return 'layered navigation or detail surface';
|
|
1615
|
+
if (/nav|tab|menu/.test(text)) return 'navigation surface';
|
|
1616
|
+
if (/chip|filter|search/.test(text)) return 'search and refinement control';
|
|
1617
|
+
return 'external bundle component role candidate';
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function mergeNamedItems(existing, incoming) {
|
|
1621
|
+
const byName = new Map();
|
|
1622
|
+
for (const item of [...existing, ...incoming]) {
|
|
1623
|
+
const name = item?.name ?? item;
|
|
1624
|
+
if (!name) continue;
|
|
1625
|
+
byName.set(String(name), typeof item === 'object' ? item : { name: String(name) });
|
|
1626
|
+
}
|
|
1627
|
+
return [...byName.values()];
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function uniqueItemsByStatement(items) {
|
|
1631
|
+
const byStatement = new Map();
|
|
1632
|
+
for (const item of items) {
|
|
1633
|
+
const statement = typeof item === 'string' ? item : item?.statement;
|
|
1634
|
+
if (!statement) continue;
|
|
1635
|
+
byStatement.set(String(statement), typeof item === 'object' ? item : { statement });
|
|
1636
|
+
}
|
|
1637
|
+
return [...byStatement.values()];
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function mergeFindings(existing, incoming) {
|
|
1641
|
+
const byId = new Map();
|
|
1642
|
+
for (const finding of [...existing, ...incoming]) {
|
|
1643
|
+
if (!finding?.id) continue;
|
|
1644
|
+
byId.set(finding.id, finding);
|
|
1645
|
+
}
|
|
1646
|
+
return [...byId.values()];
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
async function listFiles(root, dir = root) {
|
|
1650
|
+
let entries;
|
|
1651
|
+
try {
|
|
1652
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1653
|
+
} catch {
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
const files = [];
|
|
1657
|
+
for (const entry of entries) {
|
|
1658
|
+
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
|
1659
|
+
const fullPath = path.join(dir, entry.name);
|
|
1660
|
+
if (entry.isDirectory()) {
|
|
1661
|
+
files.push(...await listFiles(root, fullPath));
|
|
1662
|
+
} else if (entry.isFile()) {
|
|
1663
|
+
files.push(path.relative(root, fullPath).split(path.sep).join('/'));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return files;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function collectCssVariables(content) {
|
|
1670
|
+
const variables = [];
|
|
1671
|
+
for (const match of content.matchAll(/--([A-Za-z0-9_-]+)\s*:/g)) variables.push(`--${match[1]}`);
|
|
1672
|
+
return unique(variables);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function collectClassHints(content) {
|
|
1676
|
+
const hints = [];
|
|
1677
|
+
for (const match of content.matchAll(/\.([A-Za-z][A-Za-z0-9_-]+)/g)) hints.push(match[1]);
|
|
1678
|
+
return unique(hints);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function collectColorValues(content) {
|
|
1682
|
+
return unique([
|
|
1683
|
+
...[...content.matchAll(/#[0-9a-fA-F]{3,8}\b/g)].map((match) => match[0]),
|
|
1684
|
+
...[...content.matchAll(/(?:rgb|hsl)a?\([^)]+\)/g)].map((match) => match[0])
|
|
1685
|
+
]);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function collectSpacingValues(content) {
|
|
1689
|
+
return unique([...content.matchAll(/\b\d+(?:\.\d+)?(?:px|rem|em)\b/g)].map((match) => match[0]));
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function tokenMatchesRole(token, name, purpose) {
|
|
1693
|
+
const text = `${token} ${name} ${purpose}`.toLowerCase();
|
|
1694
|
+
return /brand|primary|surface|text|success|available|geo|distance|urgency|warning|plan|cta|interactive/.test(text)
|
|
1695
|
+
&& text.split(/[-_\s]+/).some((part) => token.toLowerCase().includes(part));
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function inferIntentFromScreen(screen) {
|
|
1699
|
+
const route = screen.route.toLowerCase();
|
|
1700
|
+
if (route.includes('map')) return 'spatial exploration';
|
|
1701
|
+
if (route.includes('detail')) return 'filter refinement or detail review';
|
|
1702
|
+
if (route.includes('hotel')) return 'entity detail and decision support';
|
|
1703
|
+
if (route.includes('home')) return 'entry and discovery';
|
|
1704
|
+
return 'existing product route';
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function countMatches(text, pattern) {
|
|
1708
|
+
return [...text.matchAll(pattern)].length;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function sanitizeId(value) {
|
|
1712
|
+
return String(value ?? 'design-system')
|
|
1713
|
+
.trim()
|
|
1714
|
+
.toLowerCase()
|
|
1715
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
1716
|
+
.replace(/^-|-$/g, '') || 'design-system';
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function sanitizeStoryId(value) {
|
|
1720
|
+
return String(value ?? 'story')
|
|
1721
|
+
.trim()
|
|
1722
|
+
.replace(/[^A-Za-z0-9_-]+/g, '-')
|
|
1723
|
+
.replace(/^-|-$/g, '') || 'story';
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function inferProductName(repoRoot) {
|
|
1727
|
+
return path.basename(repoRoot).replace(/^session-\d+-/i, '').replace(/^g\d+-/i, '') || 'product';
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function unique(items) {
|
|
1731
|
+
return [...new Set(items.filter(Boolean))];
|
|
1732
|
+
}
|