330 lines
16 KiB
JavaScript
330 lines
16 KiB
JavaScript
|
|
import crypto from 'node:crypto';
|
|||
|
|
import { readFileSync } from 'node:fs';
|
|||
|
|
|
|||
|
|
export const CONCEPT_STATUSES = new Set(['approved', 'rejected']);
|
|||
|
|
|
|||
|
|
// What a concept is actually strong at. Worlds carry a durable visual
|
|||
|
|
// identity (their palette/type half is the magnet); compositions carry a
|
|||
|
|
// staging or interaction idea (their topology half is the magnet) that can be
|
|||
|
|
// dressed in any committed identity; duals fuse both inseparably. Direction
|
|||
|
|
// seeds draw world|dual, surface seeds draw composition|dual.
|
|||
|
|
export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']);
|
|||
|
|
|
|||
|
|
// Challenger tiers, ordered by translation cost: graphic grammars map to
|
|||
|
|
// interface almost directly, instrument languages carry interaction physics,
|
|||
|
|
// atmosphere worlds need the largest translation step. Every seed roll draws
|
|||
|
|
// one challenger from each tier so at least one directly-usable graphic
|
|||
|
|
// system is always on the table.
|
|||
|
|
export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere'];
|
|||
|
|
|
|||
|
|
const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i;
|
|||
|
|
export const SYSTEM_PREFIXES = [
|
|||
|
|
'Palette/material:',
|
|||
|
|
'Type/composition:',
|
|||
|
|
'Topology/navigation:',
|
|||
|
|
'Controls/state:',
|
|||
|
|
'Responsive/motion:',
|
|||
|
|
];
|
|||
|
|
const BLAND_FORM_RE = /\b(?:control room|command center|operations center|dispatch desk|review queue|speaker queue|management console|admin console|operator loop|coordination system|tracking system|planning system|software platform|digital platform|operations cockpit|app portal|web portal|data hub|dashboard|workflow|planner|tracker|orchestrator)\b/i;
|
|||
|
|
|
|||
|
|
export function normalizeConceptForm(value) {
|
|||
|
|
return String(value || '')
|
|||
|
|
.normalize('NFKD')
|
|||
|
|
.toLowerCase()
|
|||
|
|
.replace(/[’‘]/g, "'")
|
|||
|
|
.replace(/[^a-z0-9]+/g, ' ')
|
|||
|
|
.trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function validateConceptEntry(concept, { existingForms = new Map() } = {}) {
|
|||
|
|
const errors = [];
|
|||
|
|
const id = concept?.id || '(unknown)';
|
|||
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(concept?.id || '')) {
|
|||
|
|
errors.push(`invalid concept id: ${String(concept?.id)}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const normalized = normalizeConceptForm(concept?.form);
|
|||
|
|
if (!normalized) {
|
|||
|
|
errors.push(`concept ${id} needs a form`);
|
|||
|
|
} else if (existingForms.has(normalized)) {
|
|||
|
|
errors.push(`duplicate concept form: ${id} and ${existingForms.get(normalized)}`);
|
|||
|
|
}
|
|||
|
|
if (typeof concept?.form !== 'string'
|
|||
|
|
|| concept.form.trim().length < 40
|
|||
|
|
|| concept.form.trim().length > 360
|
|||
|
|
|| !concept.form.includes(',')) {
|
|||
|
|
errors.push(`concept ${id} must name a form and inherited structure after a comma`);
|
|||
|
|
}
|
|||
|
|
if (typeof concept?.lineage !== 'string'
|
|||
|
|
|| concept.lineage.trim().length < 12
|
|||
|
|
|| concept.lineage.trim().length > 200) {
|
|||
|
|
errors.push(`concept ${id} needs specific lineage metadata of 12–200 characters`);
|
|||
|
|
}
|
|||
|
|
if (!CONCEPT_STRENGTHS.has(concept?.strength)) {
|
|||
|
|
errors.push(`concept ${id} needs a strength of ${[...CONCEPT_STRENGTHS].join(', ')}`);
|
|||
|
|
}
|
|||
|
|
if (!Array.isArray(concept?.tags)
|
|||
|
|
|| concept.tags.length !== 3
|
|||
|
|
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
|||
|
|
errors.push(`concept ${id} must have exactly three structural tags`);
|
|||
|
|
}
|
|||
|
|
if (!Array.isArray(concept?.system)
|
|||
|
|
|| concept.system.length !== SYSTEM_PREFIXES.length
|
|||
|
|
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
|||
|
|
errors.push(`concept ${id} needs system grammar with exactly five rules of 12–180 characters`);
|
|||
|
|
} else {
|
|||
|
|
const uniqueRules = new Set(concept.system.map(normalizeConceptForm));
|
|||
|
|
if (uniqueRules.size !== SYSTEM_PREFIXES.length) {
|
|||
|
|
errors.push(`concept ${id} has duplicate system grammar rules`);
|
|||
|
|
}
|
|||
|
|
if (concept.system.some((rule, index) => !rule.startsWith(SYSTEM_PREFIXES[index]))) {
|
|||
|
|
errors.push(`concept ${id} system grammar must use palette, type, topology, controls, and responsive prefixes in order`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (typeof concept?.spark !== 'string'
|
|||
|
|
|| concept.spark.trim().length < 80
|
|||
|
|
|| concept.spark.trim().length > 320) {
|
|||
|
|
errors.push(`concept ${id} needs a vivid creative spark of 80–320 characters`);
|
|||
|
|
}
|
|||
|
|
if (typeof concept?.webLeverage !== 'string'
|
|||
|
|
|| concept.webLeverage.trim().length < 20
|
|||
|
|
|| concept.webLeverage.trim().length > 240) {
|
|||
|
|
errors.push(`concept ${id} needs web leverage of 20–240 characters`);
|
|||
|
|
}
|
|||
|
|
if (/\b(?:live digital system|shared participatory system) modeled on\b/i.test(concept?.form || '')) {
|
|||
|
|
errors.push(`concept ${id} is a generic wrapper around another artifact`);
|
|||
|
|
}
|
|||
|
|
if (/\b(?:in the style of|styled like|copy of)\b/i.test(concept?.form || '')) {
|
|||
|
|
errors.push(`concept ${id} contains imitation language`);
|
|||
|
|
}
|
|||
|
|
if (BLAND_FORM_RE.test(concept?.form || '')) {
|
|||
|
|
errors.push(`concept ${id} is framed as a literal software or operations archetype instead of an inspiring visual world`);
|
|||
|
|
}
|
|||
|
|
return errors;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fingerprint of everything a reviewer judged. Reviews carry this hash so an
|
|||
|
|
// approval cannot silently survive a content edit: the validator rejects any
|
|||
|
|
// review whose hash no longer matches the concept it points at.
|
|||
|
|
export function conceptContentHash(concept) {
|
|||
|
|
const payload = [
|
|||
|
|
concept?.form ?? '',
|
|||
|
|
concept?.lineage ?? '',
|
|||
|
|
JSON.stringify(concept?.tags ?? []),
|
|||
|
|
JSON.stringify(concept?.system ?? []),
|
|||
|
|
concept?.spark ?? '',
|
|||
|
|
concept?.webLeverage ?? '',
|
|||
|
|
].join('\n');
|
|||
|
|
return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function readConceptCatalog(catalogPath, reviewsPath) {
|
|||
|
|
const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|||
|
|
const reviewData = JSON.parse(readFileSync(reviewsPath, 'utf8'));
|
|||
|
|
const reviews = reviewData.reviews || {};
|
|||
|
|
const wellsById = new Map((catalog.wells || []).map(well => [well.id, well]));
|
|||
|
|
const concepts = [];
|
|||
|
|
|
|||
|
|
for (const family of catalog.families || []) {
|
|||
|
|
for (const concept of family.concepts || []) {
|
|||
|
|
concepts.push({
|
|||
|
|
...concept,
|
|||
|
|
familyId: family.id,
|
|||
|
|
familyLabel: family.label,
|
|||
|
|
wellId: family.well || null,
|
|||
|
|
wellLabel: wellsById.get(family.well)?.label || null,
|
|||
|
|
wellTier: wellsById.get(family.well)?.tier || null,
|
|||
|
|
status: reviews[concept.id]?.status || 'pending',
|
|||
|
|
review: reviews[concept.id] || null,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { catalog, reviewData, reviews, concepts };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function validateConceptCatalog(catalog, reviewData, {
|
|||
|
|
expectedTotal,
|
|||
|
|
minimumTotal,
|
|||
|
|
requireApprovedMinimum = true,
|
|||
|
|
} = {}) {
|
|||
|
|
const errors = [];
|
|||
|
|
const warnings = [];
|
|||
|
|
const familyIds = new Set();
|
|||
|
|
const conceptIds = new Set();
|
|||
|
|
const normalizedForms = new Map();
|
|||
|
|
const concepts = [];
|
|||
|
|
|
|||
|
|
if (!Number.isInteger(catalog?.schemaVersion) || catalog.schemaVersion < 7) {
|
|||
|
|
errors.push('catalog.schemaVersion must be 7 or newer');
|
|||
|
|
}
|
|||
|
|
if (typeof catalog?.catalogVersion !== 'string' || !catalog.catalogVersion.trim()) {
|
|||
|
|
errors.push('catalog.catalogVersion must be a non-empty string');
|
|||
|
|
}
|
|||
|
|
if (typeof catalog?.qualityBar?.principle !== 'string' || catalog.qualityBar.principle.trim().length < 80) {
|
|||
|
|
errors.push('catalog.qualityBar.principle must define the universal creative bar');
|
|||
|
|
}
|
|||
|
|
if (!Array.isArray(catalog?.qualityBar?.rejectIf) || catalog.qualityBar.rejectIf.length < 5) {
|
|||
|
|
errors.push('catalog.qualityBar.rejectIf must define at least five rejection gates');
|
|||
|
|
}
|
|||
|
|
if (!Array.isArray(catalog?.qualityBar?.reviewAxes) || catalog.qualityBar.reviewAxes.length < 8) {
|
|||
|
|
errors.push('catalog.qualityBar.reviewAxes must define at least eight review axes');
|
|||
|
|
}
|
|||
|
|
if (!Array.isArray(catalog?.families) || catalog.families.length < 3) {
|
|||
|
|
errors.push('catalog.families must contain at least three families');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const wellIds = new Set();
|
|||
|
|
if (!Array.isArray(catalog?.wells) || catalog.wells.length < 5) {
|
|||
|
|
errors.push('catalog.wells must define at least five inspiration wells');
|
|||
|
|
}
|
|||
|
|
for (const well of catalog?.wells || []) {
|
|||
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(well.id || '')) {
|
|||
|
|
errors.push(`invalid well id: ${String(well.id)}`);
|
|||
|
|
} else if (wellIds.has(well.id)) {
|
|||
|
|
errors.push(`duplicate well id: ${well.id}`);
|
|||
|
|
}
|
|||
|
|
wellIds.add(well.id);
|
|||
|
|
if (typeof well.label !== 'string' || !well.label.trim()) {
|
|||
|
|
errors.push(`well ${well.id || '(unknown)'} needs a label`);
|
|||
|
|
}
|
|||
|
|
if (typeof well.description !== 'string' || well.description.trim().length < 40) {
|
|||
|
|
errors.push(`well ${well.id || '(unknown)'} needs a description of at least 40 characters`);
|
|||
|
|
}
|
|||
|
|
if (!WELL_TIERS.includes(well.tier)) {
|
|||
|
|
errors.push(`well ${well.id || '(unknown)'} needs a tier of ${WELL_TIERS.join(', ')}, got: ${String(well.tier)}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const tiersPresent = new Set((catalog?.wells || []).map(well => well.tier).filter(tier => WELL_TIERS.includes(tier)));
|
|||
|
|
for (const tier of WELL_TIERS) {
|
|||
|
|
if ((catalog?.wells || []).length > 0 && !tiersPresent.has(tier)) {
|
|||
|
|
errors.push(`no well declares the ${tier} tier`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const populatedWells = new Set();
|
|||
|
|
|
|||
|
|
for (const family of catalog?.families || []) {
|
|||
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(family.id || '')) {
|
|||
|
|
errors.push(`invalid family id: ${String(family.id)}`);
|
|||
|
|
} else if (familyIds.has(family.id)) {
|
|||
|
|
errors.push(`duplicate family id: ${family.id}`);
|
|||
|
|
}
|
|||
|
|
familyIds.add(family.id);
|
|||
|
|
if (typeof family.label !== 'string' || !family.label.trim()) {
|
|||
|
|
errors.push(`family ${family.id || '(unknown)'} needs a label`);
|
|||
|
|
}
|
|||
|
|
if (!wellIds.has(family.well)) {
|
|||
|
|
errors.push(`family ${family.id || '(unknown)'} must belong to a declared well, got: ${String(family.well)}`);
|
|||
|
|
} else {
|
|||
|
|
populatedWells.add(family.well);
|
|||
|
|
}
|
|||
|
|
if (!Array.isArray(family.concepts) || family.concepts.length === 0) {
|
|||
|
|
errors.push(`family ${family.id || '(unknown)'} has no concepts`);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const concept of family.concepts) {
|
|||
|
|
concepts.push(concept);
|
|||
|
|
if (conceptIds.has(concept.id)) {
|
|||
|
|
errors.push(`duplicate concept id: ${concept.id}`);
|
|||
|
|
}
|
|||
|
|
errors.push(...validateConceptEntry(concept, { existingForms: normalizedForms }));
|
|||
|
|
conceptIds.add(concept.id);
|
|||
|
|
const normalized = normalizeConceptForm(concept.form);
|
|||
|
|
if (normalized) normalizedForms.set(normalized, concept.id);
|
|||
|
|
if (typeof concept.webLeverage === 'string' && !WEB_LEVERAGE_RE.test(concept.webLeverage)) {
|
|||
|
|
warnings.push(`concept ${concept.id} web leverage should be checked for a specific browser-native capability`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const well of catalog?.wells || []) {
|
|||
|
|
if (well.id && !populatedWells.has(well.id)) {
|
|||
|
|
errors.push(`well ${well.id} has no families`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (expectedTotal !== undefined && concepts.length !== expectedTotal) {
|
|||
|
|
errors.push(`expected ${expectedTotal} concepts, found ${concepts.length}`);
|
|||
|
|
}
|
|||
|
|
if (minimumTotal !== undefined && concepts.length < minimumTotal) {
|
|||
|
|
errors.push(`expected at least ${minimumTotal} concepts, found ${concepts.length}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!Number.isInteger(reviewData?.schemaVersion) || reviewData.schemaVersion < 2) {
|
|||
|
|
errors.push('reviews.schemaVersion must be 2 or newer');
|
|||
|
|
}
|
|||
|
|
const conceptsById = new Map(concepts.map(concept => [concept.id, concept]));
|
|||
|
|
for (const [id, review] of Object.entries(reviewData?.reviews || {})) {
|
|||
|
|
if (!conceptIds.has(id)) errors.push(`review references missing concept: ${id}`);
|
|||
|
|
if (!CONCEPT_STATUSES.has(review?.status)) errors.push(`invalid review status for ${id}: ${String(review?.status)}`);
|
|||
|
|
if (typeof review?.reviewedBy !== 'string' || !review.reviewedBy.trim()) {
|
|||
|
|
errors.push(`review ${id} needs reviewedBy`);
|
|||
|
|
}
|
|||
|
|
if (typeof review?.reviewedAt !== 'string' || Number.isNaN(Date.parse(review.reviewedAt))) {
|
|||
|
|
errors.push(`review ${id} needs an ISO reviewedAt timestamp`);
|
|||
|
|
}
|
|||
|
|
if (typeof review?.formHash !== 'string' || !review.formHash.trim()) {
|
|||
|
|
errors.push(`review ${id} needs a formHash of the reviewed content`);
|
|||
|
|
} else if (conceptsById.has(id) && review.formHash !== conceptContentHash(conceptsById.get(id))) {
|
|||
|
|
errors.push(`review ${id} is stale: concept content changed since it was reviewed; reset or re-review it`);
|
|||
|
|
}
|
|||
|
|
if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) {
|
|||
|
|
errors.push(`review ${id} note must be a non-empty string of 500 characters or fewer`);
|
|||
|
|
}
|
|||
|
|
// Rating grades how strong an approved concept is (3 exceptional, 2 solid,
|
|||
|
|
// 1 marginal keep). Optional, approved-only, and read as a calibration
|
|||
|
|
// signal for future authoring rounds.
|
|||
|
|
if (review?.rating !== undefined) {
|
|||
|
|
if (![1, 2, 3].includes(review.rating)) {
|
|||
|
|
errors.push(`review ${id} rating must be 1, 2, or 3`);
|
|||
|
|
} else if (review.status !== 'approved') {
|
|||
|
|
errors.push(`review ${id} rating only applies to approved concepts`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const wellTierById = new Map((catalog?.wells || []).map(well => [well.id, well.tier]));
|
|||
|
|
const approved = concepts.filter(concept => reviewData?.reviews?.[concept.id]?.status === 'approved');
|
|||
|
|
const approvedTiers = new Set(
|
|||
|
|
(catalog?.families || [])
|
|||
|
|
.filter(family => family.concepts?.some(concept => reviewData?.reviews?.[concept.id]?.status === 'approved'))
|
|||
|
|
.map(family => wellTierById.get(family.well))
|
|||
|
|
.filter(tier => WELL_TIERS.includes(tier))
|
|||
|
|
);
|
|||
|
|
if (requireApprovedMinimum && approved.length < 3) errors.push('at least three concepts must be approved');
|
|||
|
|
if (requireApprovedMinimum && approvedTiers.size < WELL_TIERS.length) {
|
|||
|
|
errors.push('approved concepts must cover every challenger tier');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
errors,
|
|||
|
|
warnings,
|
|||
|
|
stats: {
|
|||
|
|
wells: wellIds.size,
|
|||
|
|
families: familyIds.size,
|
|||
|
|
concepts: concepts.length,
|
|||
|
|
approved: approved.length,
|
|||
|
|
pending: concepts.length - Object.keys(reviewData?.reviews || {}).length,
|
|||
|
|
rejected: Object.values(reviewData?.reviews || {}).filter(review => review?.status === 'rejected').length,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function approvedPoolRevision(concepts) {
|
|||
|
|
const payload = concepts
|
|||
|
|
.filter(concept => concept.status === 'approved')
|
|||
|
|
.map(concept => `${concept.familyId}:${concept.id}:${concept.strength}:${concept.form}:${concept.spark}:${JSON.stringify(concept.system)}:${concept.webLeverage}`)
|
|||
|
|
.sort()
|
|||
|
|
.join('\n');
|
|||
|
|
return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function deterministicRank(items, input, idFor = item => item.id) {
|
|||
|
|
return [...items].sort((a, b) => {
|
|||
|
|
const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex');
|
|||
|
|
const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex');
|
|||
|
|
return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b));
|
|||
|
|
});
|
|||
|
|
}
|