@mcptoolshop/style-dataset-lab 3.1.0 → 3.3.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.es.md +96 -63
  3. package/README.fr.md +97 -64
  4. package/README.hi.md +108 -77
  5. package/README.it.md +97 -64
  6. package/README.ja.md +105 -72
  7. package/README.md +33 -0
  8. package/README.pt-BR.md +94 -61
  9. package/README.zh.md +98 -65
  10. package/bin/sdlab.js +71 -0
  11. package/lib/adapters/ai-toolkit.js +209 -0
  12. package/lib/adapters/comfyui-runner.js +36 -2
  13. package/lib/adapters/comfyui-workflows.js +133 -2
  14. package/lib/adapters/diffusers-lora.js +46 -66
  15. package/lib/adapters/generic-image-caption.js +22 -50
  16. package/lib/canon-build/build.js +450 -0
  17. package/lib/canon-build/emitters/base.js +122 -0
  18. package/lib/canon-build/emitters/character.js +167 -0
  19. package/lib/canon-build/emitters/deity.js +165 -0
  20. package/lib/canon-build/emitters/faction.js +161 -0
  21. package/lib/canon-build/emitters/index.js +38 -0
  22. package/lib/canon-build/emitters/location.js +153 -0
  23. package/lib/canon-build/emitters/monster.js +131 -0
  24. package/lib/canon-build/emitters/relic.js +142 -0
  25. package/lib/canon-build/emitters/ship.js +154 -0
  26. package/lib/canon-build/emitters/species.js +165 -0
  27. package/lib/canon-build/frontmatter.js +76 -0
  28. package/lib/canon-build/lane-resolve.js +64 -0
  29. package/lib/canon-build/load-config.js +117 -0
  30. package/lib/canon-build/load-entry.js +62 -0
  31. package/lib/canon-build/load-schema.js +46 -0
  32. package/lib/captions.js +197 -0
  33. package/lib/config.js +19 -0
  34. package/lib/critique-engine.js +46 -1
  35. package/lib/critique-render.js +45 -0
  36. package/lib/freeze-events.js +109 -0
  37. package/lib/freeze-gate.js +208 -0
  38. package/lib/freeze-stamp.js +161 -0
  39. package/lib/rows.js +228 -0
  40. package/lib/run-manifest.js +308 -0
  41. package/lib/snapshot.js +7 -1
  42. package/lib/training-adapters.js +5 -0
  43. package/lib/training-packages.js +24 -18
  44. package/lib/training-profiles.js +33 -0
  45. package/lib/verifier.js +67 -0
  46. package/package.json +12 -3
  47. package/scripts/_batch_cut.py +71 -0
  48. package/scripts/_seam_cutout.py +74 -0
  49. package/scripts/canon-build.js +88 -0
  50. package/scripts/canon-drift.js +193 -0
  51. package/scripts/canon-freeze-status.js +85 -0
  52. package/scripts/canon-freeze.js +211 -0
  53. package/scripts/canon-unfreeze.js +137 -0
  54. package/scripts/critique.js +32 -7
  55. package/scripts/curate.js +33 -0
  56. package/scripts/generate-controlnet.js +12 -0
  57. package/scripts/generate-identity.js +20 -4
  58. package/scripts/generate-ipadapter.js +11 -0
  59. package/scripts/generate.js +262 -102
  60. package/scripts/qwen_generate.py +327 -0
  61. package/scripts/run-generate.js +3 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Monster emitter (D5: monster.schema.json).
3
+ *
4
+ * Caption: trigger + " style, a " + species_tag + " creature, " + signature_features[0..2]
5
+ * Prompt positive: visual cue + anatomy + lineage hint + scale; human_element suppressed unless species_tag="hybrid"
6
+ * Prompt negative: forbidden_inputs + synthetic human-guard when human_element is null
7
+ * Context sections: Identity, Anatomy, Combat intent, Forbidden, Sources
8
+ */
9
+
10
+ import { contextFrontmatter, renderKeyValueSection, renderSection, renderNegativeBase } from './base.js';
11
+
12
+ const SCHEMA_KIND = 'monster';
13
+
14
+ export function emitMonster(entry, ctx) {
15
+ const fm = entry.frontmatter;
16
+ return {
17
+ schemaKind: SCHEMA_KIND,
18
+ caption: buildCaption(fm, ctx),
19
+ prompt: buildPromptTemplate(fm),
20
+ context: buildContext(entry, ctx),
21
+ };
22
+ }
23
+
24
+ function buildCaption(fm, ctx) {
25
+ const segments = [];
26
+ if (ctx.trigger) segments.push(`${ctx.trigger} style`);
27
+
28
+ const species = fm.species_tag || 'creature';
29
+ segments.push(`a ${species} creature`);
30
+
31
+ const features = Array.isArray(fm.signature_features) ? fm.signature_features.slice(0, 2) : [];
32
+ if (features.length) {
33
+ segments.push(features.join(', '));
34
+ }
35
+
36
+ const notable = fm.anatomy_descriptor?.notable;
37
+ if (Array.isArray(notable) && notable.length) {
38
+ segments.push(notable[0]);
39
+ }
40
+
41
+ return segments.join(', ');
42
+ }
43
+
44
+ function buildPromptTemplate(fm) {
45
+ const lines = [];
46
+ lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
47
+ lines.push(`{{ trigger }} style, a ${fm.species_tag || 'creature'}, ${formatFeatures(fm.signature_features)}.`);
48
+
49
+ const anatomy = formatAnatomy(fm.anatomy_descriptor);
50
+ if (anatomy) lines.push(anatomy);
51
+
52
+ if (fm.lineage_reference && fm.lineage_reference !== 'none') {
53
+ lines.push(`{# lineage #} ${fm.lineage_reference} descent`);
54
+ }
55
+ if (fm.scale_indicator) {
56
+ lines.push(`{# scale #} ${fm.scale_indicator}`);
57
+ }
58
+
59
+ // Human-element handling — suppression is a hard guard against contamination.
60
+ if (fm.species_tag === 'hybrid' && fm.human_element) {
61
+ lines.push(`{# hybrid override #} human portion: ${fm.human_element.scope}`);
62
+ }
63
+
64
+ lines.push('---');
65
+ lines.push(`{{ negative_base }}`);
66
+ if (!fm.human_element) {
67
+ lines.push(', human features, human face, human torso');
68
+ }
69
+ return lines.join('\n') + '\n';
70
+ }
71
+
72
+ function buildContext(entry, ctx) {
73
+ const fm = entry.frontmatter;
74
+ const out = [];
75
+ out.push(contextFrontmatter({
76
+ entityId: fm.id,
77
+ schemaKind: SCHEMA_KIND,
78
+ generatedFrom: ctx.generatedFrom,
79
+ entryHash: ctx.entryHash,
80
+ }));
81
+
82
+ out.push(`# ${titleFromId(fm.id)}\n`);
83
+
84
+ out.push(renderKeyValueSection('Identity', [
85
+ ['species_tag', fm.species_tag],
86
+ ['lineage_reference', fm.lineage_reference],
87
+ ['lineage_detail', fm.lineage_detail],
88
+ ['scale_indicator', fm.scale_indicator],
89
+ ]));
90
+
91
+ out.push(renderSection('Anatomy', fm.anatomy_descriptor || null));
92
+
93
+ if (fm.human_element) {
94
+ out.push(renderSection('Human element', fm.human_element));
95
+ }
96
+
97
+ out.push(renderSection('Combat intent', fm.signature_features || null));
98
+ out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
99
+ out.push(renderSection('Sources', fm.sources || null));
100
+
101
+ // Canon prose body feeds context exclusively.
102
+ if (entry.body && entry.body.trim()) {
103
+ out.push(`## Notes\n${entry.body.trim()}\n`);
104
+ }
105
+
106
+ return out.join('');
107
+ }
108
+
109
+ function formatFeatures(features) {
110
+ if (!Array.isArray(features) || features.length === 0) return 'creature silhouette';
111
+ return features.join(', ');
112
+ }
113
+
114
+ function formatAnatomy(desc) {
115
+ if (!desc || typeof desc !== 'object') return '';
116
+ const parts = [];
117
+ if (typeof desc.heads === 'number') parts.push(`${desc.heads} head${desc.heads === 1 ? '' : 's'}`);
118
+ if (typeof desc.limbs === 'number') parts.push(`${desc.limbs} limb${desc.limbs === 1 ? '' : 's'}`);
119
+ if (typeof desc.wings === 'number' && desc.wings > 0) parts.push(`${desc.wings} wing${desc.wings === 1 ? '' : 's'}`);
120
+ if (typeof desc.tails === 'number' && desc.tails > 0) parts.push(`${desc.tails} tail${desc.tails === 1 ? '' : 's'}`);
121
+ if (Array.isArray(desc.notable) && desc.notable.length) parts.push(...desc.notable);
122
+ if (!parts.length) return '';
123
+ return `{# anatomy #} ${parts.join(', ')}`;
124
+ }
125
+
126
+ function titleFromId(id) {
127
+ if (!id) return 'Untitled';
128
+ return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
129
+ }
130
+
131
+ export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Relic emitter (D5: relic.schema.json).
3
+ *
4
+ * Caption: trigger + " style, a " + relic_type + ", " + silhouette_cue + ", " + materials.join(" and ")
5
+ * Prompt positive: silhouette_cue + signature_markings + materials + scale + wear_level + attachment_to_wearer + palette + signature_features
6
+ * Prompt negative: forbidden_inputs
7
+ * Context sections: Identity, Role & Significance, Powers, Ownership Chain, Associations, Mechanical (optional), Forbidden, Sources
8
+ */
9
+
10
+ import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
11
+
12
+ const SCHEMA_KIND = 'relic';
13
+
14
+ export function emitRelic(entry, ctx) {
15
+ const fm = entry.frontmatter;
16
+ return {
17
+ schemaKind: SCHEMA_KIND,
18
+ caption: buildCaption(fm, ctx),
19
+ prompt: buildPromptTemplate(fm),
20
+ context: buildContext(entry, ctx),
21
+ };
22
+ }
23
+
24
+ function buildCaption(fm, ctx) {
25
+ const v = fm.visual || {};
26
+ const segments = [];
27
+ if (ctx.trigger) segments.push(`${ctx.trigger} style`);
28
+
29
+ const head = fm.relic_type ? `a ${fm.relic_type}` : 'a relic';
30
+ segments.push(head);
31
+
32
+ if (v.silhouette_cue) segments.push(v.silhouette_cue);
33
+
34
+ if (Array.isArray(fm.materials) && fm.materials.length) {
35
+ segments.push(fm.materials.join(' and '));
36
+ }
37
+
38
+ return segments.join(', ');
39
+ }
40
+
41
+ function buildPromptTemplate(fm) {
42
+ const v = fm.visual || {};
43
+ const lines = [];
44
+ lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
45
+ lines.push(`{{ trigger }} style, a ${fm.relic_type || 'relic'}, ${v.silhouette_cue || ''}.`);
46
+
47
+ if (Array.isArray(v.signature_markings) && v.signature_markings.length) {
48
+ lines.push(`{# markings #} ${v.signature_markings.join(', ')}`);
49
+ }
50
+ if (Array.isArray(fm.materials) && fm.materials.length) {
51
+ lines.push(`{# materials #} ${fm.materials.join(', ')}`);
52
+ }
53
+ if (v.scale) lines.push(`{# scale #} ${v.scale}`);
54
+ if (v.wear_level) lines.push(`{# wear #} ${v.wear_level}`);
55
+ if (v.attachment_to_wearer) {
56
+ lines.push(`{# attachment #} {% if character_trigger %}${v.attachment_to_wearer}{% endif %}`);
57
+ }
58
+ if (Array.isArray(v.palette) && v.palette.length) {
59
+ lines.push(`{# palette #} ${v.palette.join(' ')}`);
60
+ }
61
+ if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
62
+ lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
63
+ }
64
+
65
+ lines.push('---');
66
+ lines.push(`{{ negative_base }}`);
67
+ return lines.join('\n') + '\n';
68
+ }
69
+
70
+ function buildContext(entry, ctx) {
71
+ const fm = entry.frontmatter;
72
+ const n = fm.narrative || {};
73
+ const m = fm.mechanical;
74
+ const out = [];
75
+
76
+ out.push(contextFrontmatter({
77
+ entityId: fm.id,
78
+ schemaKind: SCHEMA_KIND,
79
+ generatedFrom: ctx.generatedFrom,
80
+ entryHash: ctx.entryHash,
81
+ }));
82
+ out.push(`# ${titleFromId(fm.id)}\n`);
83
+
84
+ out.push(renderKeyValueSection('Identity', [
85
+ ['relic_type', fm.relic_type],
86
+ ['origin', fm.origin],
87
+ ['current_status', fm.current_status],
88
+ ]));
89
+
90
+ out.push(renderKeyValueSection('Role & Significance', [
91
+ ['role', n.role],
92
+ ['mythic_significance', n.mythic_significance],
93
+ ]));
94
+
95
+ if (Array.isArray(fm.powers) && fm.powers.length) {
96
+ const bullets = fm.powers.map((p) => {
97
+ const conditions = p.conditions ? ` — ${p.conditions}` : '';
98
+ const limits = p.limits ? ` — limits: ${p.limits}` : '';
99
+ return `${p.effect}${conditions}${limits}`;
100
+ });
101
+ out.push(renderSection('Powers', bullets));
102
+ }
103
+
104
+ if (Array.isArray(fm.ownership_chain) && fm.ownership_chain.length) {
105
+ const bullets = fm.ownership_chain.map((o) => {
106
+ const from = o.acquired_from ? ` from ${o.acquired_from}` : '';
107
+ const note = o.note ? ` (${o.note})` : '';
108
+ return `${o.era}: ${o.owner_id} via ${o.acquired_by}${from}${note}`;
109
+ });
110
+ out.push(renderSection('Ownership Chain', bullets));
111
+ }
112
+
113
+ out.push(renderKeyValueSection('Associations', [
114
+ ['associated_myths', n.associated_myths],
115
+ ['counterparts', n.counterparts],
116
+ ]));
117
+
118
+ if (m && typeof m === 'object' && Object.values(m).some((v) => v != null)) {
119
+ out.push(renderKeyValueSection('Mechanical', [
120
+ ['unlock_trigger', m.unlock_trigger],
121
+ ['grants', m.grants],
122
+ ['drawbacks', m.drawbacks],
123
+ ['slot', m.slot],
124
+ ]));
125
+ }
126
+
127
+ out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
128
+ out.push(renderSection('Sources', fm.sources || null));
129
+
130
+ if (entry.body && entry.body.trim()) {
131
+ out.push(`## Notes\n${entry.body.trim()}\n`);
132
+ }
133
+
134
+ return out.join('');
135
+ }
136
+
137
+ function titleFromId(id) {
138
+ if (!id) return 'Untitled';
139
+ return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
140
+ }
141
+
142
+ export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Ship emitter (Star Freight D5).
3
+ *
4
+ * silhouette_cue is the heaviest trainable leverage for ships — the player
5
+ * sees them at multiple scales (nav chart, exterior establishing, dock
6
+ * approach, interior cockpit). The emitter lifts it to the caption and
7
+ * foregrounds it in the prompt template.
8
+ *
9
+ * Caption: trigger + " style, a " + ship_class + " ship, " +
10
+ * silhouette_cue + ", " + material_dominant
11
+ * Prompt positive: silhouette_cue + tonnage_class + hull_repair_history +
12
+ * symmetry + material_dominant + hull_markings +
13
+ * scale_cue_object + cockpit_style + engine_signature +
14
+ * palette + signature_features
15
+ * Prompt negative: forbidden_inputs
16
+ */
17
+
18
+ import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
19
+
20
+ const SCHEMA_KIND = 'ship';
21
+
22
+ export function emitShip(entry, ctx) {
23
+ const fm = entry.frontmatter;
24
+ return {
25
+ schemaKind: SCHEMA_KIND,
26
+ caption: buildCaption(fm, ctx),
27
+ prompt: buildPromptTemplate(fm),
28
+ context: buildContext(entry, ctx),
29
+ };
30
+ }
31
+
32
+ function buildCaption(fm, ctx) {
33
+ const v = fm.visual || {};
34
+ const segments = [];
35
+ if (ctx.trigger) segments.push(`${ctx.trigger} style`);
36
+
37
+ const cls = fm.ship_class || 'ship';
38
+ segments.push(`a ${cls} ship`);
39
+
40
+ if (v.silhouette_cue) segments.push(v.silhouette_cue);
41
+ if (v.material_dominant) segments.push(v.material_dominant);
42
+
43
+ return segments.join(', ');
44
+ }
45
+
46
+ function buildPromptTemplate(fm) {
47
+ const v = fm.visual || {};
48
+ const lines = [];
49
+ lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
50
+ lines.push(`{{ trigger }} style, a ${fm.ship_class || 'ship'}, ${v.silhouette_cue || ''}.`);
51
+
52
+ if (fm.tonnage_class) lines.push(`{# tonnage #} ${fm.tonnage_class}`);
53
+ if (v.hull_repair_history) lines.push(`{# hull #} ${v.hull_repair_history}`);
54
+ if (v.symmetry) lines.push(`{# symmetry #} ${v.symmetry}`);
55
+ if (v.material_dominant) lines.push(`{# material #} ${v.material_dominant}`);
56
+ if (Array.isArray(v.hull_markings) && v.hull_markings.length) {
57
+ lines.push(`{# markings #} ${v.hull_markings.join(', ')}`);
58
+ }
59
+ if (v.scale_cue_object) lines.push(`{# scale #} ${v.scale_cue_object}`);
60
+ if (v.cockpit_style) lines.push(`{# cockpit #} ${v.cockpit_style}`);
61
+ if (v.engine_signature) lines.push(`{# engine #} ${v.engine_signature}`);
62
+ if (Array.isArray(v.palette) && v.palette.length) {
63
+ lines.push(`{# palette #} ${v.palette.join(' ')}`);
64
+ }
65
+ if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
66
+ lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
67
+ }
68
+
69
+ lines.push('---');
70
+ lines.push(`{{ negative_base }}`);
71
+ return lines.join('\n') + '\n';
72
+ }
73
+
74
+ function buildContext(entry, ctx) {
75
+ const fm = entry.frontmatter;
76
+ const n = fm.narrative || {};
77
+ const m = fm.mechanical;
78
+ const v = fm.visual || {};
79
+ const out = [];
80
+
81
+ out.push(contextFrontmatter({
82
+ entityId: fm.id,
83
+ schemaKind: SCHEMA_KIND,
84
+ generatedFrom: ctx.generatedFrom,
85
+ entryHash: ctx.entryHash,
86
+ }));
87
+ out.push(`# ${titleFromId(fm.id)}\n`);
88
+
89
+ out.push(renderKeyValueSection('Identity', [
90
+ ['ship_class', fm.ship_class],
91
+ ['tonnage_class', fm.tonnage_class],
92
+ ['faction_of_origin', fm.faction_of_origin],
93
+ ['current_operator', fm.current_operator],
94
+ ]));
95
+
96
+ out.push(renderKeyValueSection('Visual', [
97
+ ['silhouette_cue', v.silhouette_cue],
98
+ ['palette', v.palette],
99
+ ['hull_markings', v.hull_markings],
100
+ ['hull_repair_history', v.hull_repair_history],
101
+ ['symmetry', v.symmetry],
102
+ ['material_dominant', v.material_dominant],
103
+ ['scale_cue_object', v.scale_cue_object],
104
+ ['interior_architecture', v.interior_architecture],
105
+ ['cockpit_style', v.cockpit_style],
106
+ ['engine_signature', v.engine_signature],
107
+ ]));
108
+
109
+ out.push(renderKeyValueSection('Narrative', [
110
+ ['role', n.role],
111
+ ['current_status', n.current_status],
112
+ ['trade_capacity', n.trade_capacity],
113
+ ['named_crew', n.named_crew],
114
+ ['named_passengers', n.named_passengers],
115
+ ]));
116
+
117
+ out.push(renderSection('History beats', n.history_beats || null));
118
+
119
+ if (Array.isArray(n.relationships) && n.relationships.length) {
120
+ const relBullets = n.relationships.map((r) => {
121
+ const note = r.note ? ` — ${r.note}` : '';
122
+ return `${r.edge_type} ${r.target_id}${note}`;
123
+ });
124
+ out.push(renderSection('Relationships', relBullets));
125
+ }
126
+
127
+ if (m && typeof m === 'object' && Object.values(m).some((vv) => vv != null)) {
128
+ out.push(renderKeyValueSection('Mechanical', [
129
+ ['hull_integrity_starting', m.hull_integrity_starting],
130
+ ['fuel_capacity_starting', m.fuel_capacity_starting],
131
+ ['cargo_slots', m.cargo_slots],
132
+ ['modification_slots', m.modification_slots],
133
+ ['crew_capacity', m.crew_capacity],
134
+ ['combat_capability', m.combat_capability],
135
+ ]));
136
+ }
137
+
138
+ out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
139
+ out.push(renderSection('Sources', fm.sources || null));
140
+ out.push(renderSection('Canon refs', fm.canon_refs || null));
141
+
142
+ if (entry.body && entry.body.trim()) {
143
+ out.push(`## Notes\n${entry.body.trim()}\n`);
144
+ }
145
+
146
+ return out.join('');
147
+ }
148
+
149
+ function titleFromId(id) {
150
+ if (!id) return 'Untitled';
151
+ return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
152
+ }
153
+
154
+ export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Species emitter (Star Freight D6).
3
+ *
4
+ * Species-level forbidden_morphology_drift is the schema-level home for the
5
+ * "alien needs `human` in negative" rule (feedback_alien_negative_prompt.md).
6
+ * Every member character inherits it — species is the entry, not a per-character
7
+ * redundancy.
8
+ *
9
+ * Caption: trigger + " style, a " + biology_class + " species, " +
10
+ * body_plan + ", " + signature_appendages
11
+ * Prompt positive: body_plan + anatomy_descriptors +
12
+ * involuntary_expression_channel + palette + signature_features
13
+ * Prompt negative: forbidden_inputs + forbidden_morphology_drift (the species-
14
+ * level alien guard)
15
+ */
16
+
17
+ import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
18
+
19
+ const SCHEMA_KIND = 'species';
20
+
21
+ export function emitSpecies(entry, ctx) {
22
+ const fm = entry.frontmatter;
23
+ return {
24
+ schemaKind: SCHEMA_KIND,
25
+ caption: buildCaption(fm, ctx),
26
+ prompt: buildPromptTemplate(fm),
27
+ context: buildContext(entry, ctx),
28
+ };
29
+ }
30
+
31
+ function buildCaption(fm, ctx) {
32
+ const v = fm.visual || {};
33
+ const segments = [];
34
+ if (ctx.trigger) segments.push(`${ctx.trigger} style`);
35
+
36
+ const bio = fm.biology_class || 'species';
37
+ segments.push(`a ${bio} species`);
38
+
39
+ if (v.body_plan) segments.push(v.body_plan);
40
+ const anatomy = v.anatomy_descriptors || {};
41
+ if (anatomy.signature_appendages) segments.push(anatomy.signature_appendages);
42
+
43
+ return segments.join(', ');
44
+ }
45
+
46
+ function buildPromptTemplate(fm) {
47
+ const v = fm.visual || {};
48
+ const anatomy = v.anatomy_descriptors || {};
49
+ const lines = [];
50
+ lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
51
+ lines.push(`{{ trigger }} style, a ${fm.biology_class || 'species'}, ${v.body_plan || ''}.`);
52
+
53
+ if (typeof anatomy.arm_count === 'number') {
54
+ lines.push(`{# arms #} ${anatomy.arm_count} arms`);
55
+ }
56
+ if (anatomy.locomotion) lines.push(`{# locomotion #} ${anatomy.locomotion}`);
57
+ if (anatomy.sensory_organs) lines.push(`{# senses #} ${anatomy.sensory_organs}`);
58
+ if (anatomy.skin_or_integument) lines.push(`{# integument #} ${anatomy.skin_or_integument}`);
59
+ if (anatomy.signature_appendages) lines.push(`{# appendages #} ${anatomy.signature_appendages}`);
60
+ if (v.involuntary_expression_channel) {
61
+ lines.push(`{# expression #} ${v.involuntary_expression_channel}`);
62
+ }
63
+ if (Array.isArray(v.palette) && v.palette.length) {
64
+ lines.push(`{# palette #} ${v.palette.join(' ')}`);
65
+ }
66
+ if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
67
+ lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
68
+ }
69
+
70
+ lines.push('---');
71
+ lines.push(`{{ negative_base }}`);
72
+ if (Array.isArray(v.forbidden_morphology_drift) && v.forbidden_morphology_drift.length) {
73
+ lines.push(`, ${v.forbidden_morphology_drift.join(', ')}`);
74
+ }
75
+ return lines.join('\n') + '\n';
76
+ }
77
+
78
+ function buildContext(entry, ctx) {
79
+ const fm = entry.frontmatter;
80
+ const n = fm.narrative || {};
81
+ const v = fm.visual || {};
82
+ const comm = fm.communication_modality || {};
83
+ const culture = fm.cultural_stance || {};
84
+ const rel = fm.relation_to_humans || {};
85
+ const out = [];
86
+
87
+ out.push(contextFrontmatter({
88
+ entityId: fm.id,
89
+ schemaKind: SCHEMA_KIND,
90
+ generatedFrom: ctx.generatedFrom,
91
+ entryHash: ctx.entryHash,
92
+ }));
93
+ out.push(`# ${titleFromId(fm.id)}\n`);
94
+
95
+ out.push(renderKeyValueSection('Identity', [
96
+ ['biology_class', fm.biology_class],
97
+ ['scaffold', fm.scaffold],
98
+ ]));
99
+
100
+ out.push(renderKeyValueSection('Communication', [
101
+ ['primary_language', comm.primary_language],
102
+ ['nonverbal_channel', comm.nonverbal_channel],
103
+ ['can_lie', comm.can_lie],
104
+ ['signal_incompatibility_with_humans', comm.signal_incompatibility_with_humans],
105
+ ]));
106
+
107
+ out.push(renderKeyValueSection('Cultural stance', [
108
+ ['time_horizon', culture.time_horizon],
109
+ ['core_value', culture.core_value],
110
+ ['death_rite', culture.death_rite],
111
+ ['trade_idiom', culture.trade_idiom],
112
+ ['greeting_protocol', culture.greeting_protocol],
113
+ ]));
114
+
115
+ const anatomy = v.anatomy_descriptors || {};
116
+ out.push(renderKeyValueSection('Visual', [
117
+ ['body_plan', v.body_plan],
118
+ ['locomotion', anatomy.locomotion],
119
+ ['arm_count', anatomy.arm_count],
120
+ ['sensory_organs', anatomy.sensory_organs],
121
+ ['skin_or_integument', anatomy.skin_or_integument],
122
+ ['signature_appendages', anatomy.signature_appendages],
123
+ ['palette', v.palette],
124
+ ['involuntary_expression_channel', v.involuntary_expression_channel],
125
+ ['sexual_dimorphism', v.sexual_dimorphism],
126
+ ]));
127
+
128
+ out.push(renderSection('Forbidden morphology drift', v.forbidden_morphology_drift || null));
129
+
130
+ out.push(renderKeyValueSection('Relation to humans', [
131
+ ['baseline_stance', rel.baseline_stance],
132
+ ['narrative_framing', rel.narrative_framing],
133
+ ['gameplay_integration', rel.gameplay_integration],
134
+ ]));
135
+
136
+ out.push(renderKeyValueSection('Narrative', [
137
+ ['role', n.role],
138
+ ]));
139
+ out.push(renderSection('Canonical practices', n.canonical_practices || null));
140
+
141
+ if (Array.isArray(n.relationships) && n.relationships.length) {
142
+ const relBullets = n.relationships.map((r) => {
143
+ const note = r.note ? ` — ${r.note}` : '';
144
+ return `${r.edge_type} ${r.target_id}${note}`;
145
+ });
146
+ out.push(renderSection('Relationships', relBullets));
147
+ }
148
+
149
+ out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
150
+ out.push(renderSection('Sources', fm.sources || null));
151
+ out.push(renderSection('Canon refs', fm.canon_refs || null));
152
+
153
+ if (entry.body && entry.body.trim()) {
154
+ out.push(`## Notes\n${entry.body.trim()}\n`);
155
+ }
156
+
157
+ return out.join('');
158
+ }
159
+
160
+ function titleFromId(id) {
161
+ if (!id) return 'Untitled';
162
+ return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
163
+ }
164
+
165
+ export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Frontmatter parser for canon entry Markdown files.
3
+ *
4
+ * Canon entry shape:
5
+ * ---
6
+ * <YAML frontmatter>
7
+ * ---
8
+ * <Markdown body prose>
9
+ *
10
+ * The YAML block is the schema-validated object (fed to emitters' caption
11
+ * + prompt tracks). The body is free prose (fed to emitters' context track
12
+ * only, per the captions-are-load-bearing invariant — prose must never
13
+ * splice into captions).
14
+ */
15
+
16
+ import { parse as parseYaml } from 'yaml';
17
+
18
+ const FENCE = '---';
19
+
20
+ /**
21
+ * Split a file into { frontmatter, body }.
22
+ *
23
+ * Returns { frontmatter: Object, body: string } on success.
24
+ * Throws with a structured message on malformed input.
25
+ *
26
+ * Leading/trailing whitespace before/after the fences is tolerated; a file
27
+ * without a closing fence throws rather than guessing where the body starts.
28
+ *
29
+ * @param {string} text — full file contents
30
+ * @param {string} filename — for error messages only
31
+ */
32
+ export function parseFrontmatter(text, filename = '<input>') {
33
+ const normalized = text.replace(/\r\n/g, '\n');
34
+
35
+ // Must start with --- (optionally preceded by BOM / whitespace)
36
+ const stripped = normalized.replace(/^\uFEFF/, '').trimStart();
37
+ if (!stripped.startsWith(FENCE)) {
38
+ throw new Error(`parseFrontmatter: ${filename} does not begin with a "---" frontmatter fence`);
39
+ }
40
+
41
+ // Find the closing fence. It must be on its own line.
42
+ const afterOpen = stripped.slice(FENCE.length);
43
+ // Skip the newline after the opening fence.
44
+ const newlineIdx = afterOpen.indexOf('\n');
45
+ if (newlineIdx < 0) {
46
+ throw new Error(`parseFrontmatter: ${filename} has no newline after the opening fence`);
47
+ }
48
+
49
+ const rest = afterOpen.slice(newlineIdx + 1);
50
+ // Look for "\n---\n" or "\n---" at the end of rest.
51
+ const closeMatch = rest.match(/\n---(\r?\n|$)/);
52
+ if (!closeMatch) {
53
+ throw new Error(`parseFrontmatter: ${filename} has no closing "---" fence`);
54
+ }
55
+ const closeIdx = closeMatch.index;
56
+
57
+ const yamlSource = rest.slice(0, closeIdx);
58
+ const body = rest.slice(closeIdx + closeMatch[0].length);
59
+
60
+ let frontmatter;
61
+ try {
62
+ frontmatter = parseYaml(yamlSource);
63
+ } catch (err) {
64
+ throw new Error(`parseFrontmatter: ${filename} frontmatter is not valid YAML (${err.message})`);
65
+ }
66
+
67
+ if (frontmatter === null || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
68
+ throw new Error(`parseFrontmatter: ${filename} frontmatter must be a YAML object (got ${typeof frontmatter})`);
69
+ }
70
+
71
+ return {
72
+ frontmatter,
73
+ body: body.replace(/^\n+/, ''), // trim leading newlines before body
74
+ rawYaml: yamlSource,
75
+ };
76
+ }