@morphemeris/armilla-core 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/dist/armilla.d.ts +41 -0
  4. package/dist/armilla.js +98 -0
  5. package/dist/armilla.test.d.ts +1 -0
  6. package/dist/armilla.test.js +160 -0
  7. package/dist/basemap.generated.d.ts +8 -0
  8. package/dist/basemap.generated.js +9 -0
  9. package/dist/basemap.test.d.ts +1 -0
  10. package/dist/basemap.test.js +21 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.js +11 -0
  13. package/dist/index.test.d.ts +1 -0
  14. package/dist/index.test.js +14 -0
  15. package/dist/index.web.d.ts +3 -0
  16. package/dist/index.web.js +24 -0
  17. package/dist/runtime.d.ts +13 -0
  18. package/dist/runtime.js +20 -0
  19. package/dist/surface.d.ts +10 -0
  20. package/dist/surface.js +9 -0
  21. package/dist/types.d.ts +55 -0
  22. package/dist/types.generated.d.ts +1274 -0
  23. package/dist/types.generated.js +4 -0
  24. package/dist/types.js +24 -0
  25. package/dist/vocabulary.generated.d.ts +8 -0
  26. package/dist/vocabulary.generated.js +9 -0
  27. package/dist/vocabulary.test.d.ts +1 -0
  28. package/dist/vocabulary.test.js +36 -0
  29. package/dist/wasm-types.d.ts +19 -0
  30. package/dist/wasm-types.js +1 -0
  31. package/dist/wasm.d.ts +2 -0
  32. package/dist/wasm.js +16 -0
  33. package/dist/web.test.d.ts +1 -0
  34. package/dist/web.test.js +34 -0
  35. package/dist/workspace.test.d.ts +1 -0
  36. package/dist/workspace.test.js +43 -0
  37. package/docs/changelog.md +37 -0
  38. package/docs/coordinates.md +52 -0
  39. package/docs/themes.md +111 -0
  40. package/docs/vocabulary.json +1501 -0
  41. package/docs/vocabulary.md +86 -0
  42. package/package.json +73 -0
  43. package/wasm/README.md +21 -0
  44. package/wasm/armilla_wasm.d.ts +104 -0
  45. package/wasm/armilla_wasm.js +497 -0
  46. package/wasm/armilla_wasm_bg.wasm +0 -0
  47. package/wasm/armilla_wasm_bg.wasm.d.ts +21 -0
  48. package/wasm/package.json +23 -0
  49. package/wasm-web/README.md +21 -0
  50. package/wasm-web/armilla_wasm.d.ts +150 -0
  51. package/wasm-web/armilla_wasm.js +583 -0
  52. package/wasm-web/armilla_wasm_bg.wasm +0 -0
  53. package/wasm-web/armilla_wasm_bg.wasm.d.ts +21 -0
  54. package/wasm-web/package.json +27 -0
@@ -0,0 +1,4 @@
1
+ // GENERATED FILE — do not edit. The Rust definitions are the source
2
+ // of truth (spec §13.5); regenerate with
3
+ // UPDATE_TYPES=1 cargo test -p armilla-core --features ts-gen --test types
4
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The binding's type surface. Structural types are GENERATED from the
3
+ * Rust definitions into `types.generated.ts` (spec §13.5, §19) —
4
+ * regenerate with
5
+ * `UPDATE_TYPES=1 cargo test -p armilla-core --features ts-gen --test types`;
6
+ * CI fails on drift. This file keeps only the runtime values and the
7
+ * few aliases that have no Rust counterpart.
8
+ */
9
+ export * from './types.generated.js';
10
+ /** The scene schema version this binding understands (spec §3.9). */
11
+ export const SCENE_SCHEMA_VERSION = 1;
12
+ /** Narrow an unknown value to a Scene by its schema tag (spec §3.9). */
13
+ export function isScene(value) {
14
+ if (typeof value !== 'object' || value === null)
15
+ return false;
16
+ const v = value;
17
+ if (v['schema_version'] !== SCENE_SCHEMA_VERSION)
18
+ return false;
19
+ const space = v['space'];
20
+ if (typeof space !== 'object' || space === null)
21
+ return false;
22
+ const s = space;
23
+ return typeof s['width'] === 'number' && typeof s['height'] === 'number';
24
+ }
@@ -0,0 +1,8 @@
1
+ import type { Vocabulary } from './types.js';
2
+ /**
3
+ * The predicate vocabulary as data (spec §13.5, §19): every §7.3 tag key
4
+ * with its value space and carrying roles, the §5.3 measures, the §7.2
5
+ * roles, and the §10.5 rule-settable style properties — what a host's
6
+ * rule builder and mark inspector drive their pickers and walks from.
7
+ */
8
+ export declare const vocabulary: Vocabulary;
@@ -0,0 +1,9 @@
1
+ // GENERATED FILE — do not edit. Emitted from the core's predicate
2
+ // vocabulary by `UPDATE_DOCS=1 cargo test -p armilla-core --test docs`.
3
+ /**
4
+ * The predicate vocabulary as data (spec §13.5, §19): every §7.3 tag key
5
+ * with its value space and carrying roles, the §5.3 measures, the §7.2
6
+ * roles, and the §10.5 rule-settable style properties — what a host's
7
+ * rule builder and mark inspector drive their pickers and walks from.
8
+ */
9
+ export const vocabulary = JSON.parse(`{"keys":[{"key":"role","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"]},{"key":"chart","roles":["cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","period_block","period_glyph","period_label","ref_line","acg_line","acg_glyph","acg_label","legend"],"values":"open"},{"key":"body","roles":["division_sector","division_glyph","division_label","tick","tick_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","acg_line","acg_glyph","acg_label","legend"],"values":"open"},{"key":"class","roles":["body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","curve","curve_fill","curve_marker","ref_line","acg_line","acg_glyph","acg_label","legend"],"values":"open"},{"key":"sign","roles":["division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","ref_line"],"values":["aries","taurus","gemini","cancer","leo","virgo","libra","scorpio","sagittarius","capricorn","aquarius","pisces"]},{"key":"element","roles":["division_sector","division_glyph","division_label","tick","tick_label","body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","ref_line"],"values":["fire","earth","air","water"]},{"key":"modality","roles":["division_sector","division_glyph","division_label","tick","tick_label","body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","ref_line"],"values":["cardinal","fixed","mutable"]},{"key":"house","roles":["cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","period_block","period_glyph","period_label","ref_line","acg_line","acg_glyph","acg_label"],"values":["1","2","3","4","5","6","7","8","9","10","11","12"]},{"key":"hemisphere","roles":["body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","ref_line"],"values":["north","south","eastern","western"]},{"key":"motion","roles":["body_glyph","motion_mark","position_label","sign_label","pointer","event_bar","event_station","divider","table_row","almanac_glyph","almanac_label","almanac_bar","ref_line"],"values":["retrograde","direct","stationary"]},{"key":"bounds","roles":["division_sector","division_glyph","division_label","tick","tick_label","body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","ref_line"],"values":["in","out"]},{"key":"dignity","roles":["body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","ref_line"],"values":"open"},{"key":"derived","roles":["body_glyph","motion_mark","position_label","sign_label","pointer","divider","table_row","ref_line"],"values":["midpoint","lot","antiscion","contra_antiscion"]},{"key":"frame","roles":["frame_box","relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","table_row"],"values":["longitude","declination","separation","index","orb"]},{"key":"aspect","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","table_row"],"values":["conjunction","opposition","trine","square","sextile","semi_sextile","quincunx","semi_square","sesquiquadrate","quintile","bi_quintile","septile","bi_septile","tri_septile","quindecile","parallel","contraparallel"]},{"key":"harmonic","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","table_row"],"values":["1","2","3","4","5","6","7","8","9","10","11","12"]},{"key":"aspect_class","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","table_row"],"values":["hard","soft","adjusting","creative","major","minor"]},{"key":"phase","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","table_row"],"values":["applying","separating"]},{"key":"exact","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","table_row"],"values":["true","false"]},{"key":"from","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","edge"],"values":"open"},{"key":"to","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","edge"],"values":"open"},{"key":"scope","roles":["relation_line","relation_glyph","orb_label","event_bar","event_exact","event_station","event_glyph","event_label","table_row"],"values":["intra","inter","event"]},{"key":"angle","roles":["cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","table_row","acg_line","acg_glyph","acg_label"],"values":["asc","mc","dsc","ic"]},{"key":"pattern","roles":["pattern_fill"],"values":["t_square","grand_trine","grand_cross","yod","kite","stellium","mystic_rectangle"]},{"key":"step","roles":["division_sector","division_glyph","division_label","tick","tick_label"],"values":["1","5","10","30","half","quarter","day","week","month","year","decade"]},{"key":"part","roles":["band_boundary","grid_line","division_sector","division_glyph","division_label","tick","tick_label","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","event_glyph","event_label","divider","edge","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","indicator"],"values":["name","degree","vertical","horizontal","diagonal","moving","target","enter","exact","leave","loop","curve","ref","bound","index","extreme","mirror","north","south","station","peak","bond","branch","open","current","convergent","now","group","readout","scale","lane","age","house","meridian","parallel","land","border"]},{"key":"month","roles":["division_sector","division_glyph","division_label","tick","tick_label","title"],"values":"open"},{"key":"week","roles":["division_sector","division_glyph","division_label","tick","tick_label"],"values":"open"},{"key":"day","roles":["division_sector","division_glyph","division_label","tick","tick_label","almanac_glyph","almanac_label","almanac_bar"],"values":"open"},{"key":"year","roles":["division_sector","division_glyph","division_label","tick","tick_label"],"values":"open"},{"key":"weekday","roles":["division_sector","division_glyph","division_label","tick","tick_label","almanac_glyph","almanac_label","almanac_bar"],"values":["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]},{"key":"almanac","roles":["almanac_glyph","almanac_label","almanac_bar"],"values":["lunation","ingress","station","void"]},{"key":"lunation","roles":["almanac_glyph","almanac_label","almanac_bar"],"values":["new","first_quarter","full","last_quarter"]},{"key":"eclipse","roles":["almanac_glyph","almanac_label","almanac_bar"],"values":["solar","lunar"]},{"key":"technique","roles":["period_block","period_glyph","period_label"],"values":["profections","firdaria","releasing"]},{"key":"level","roles":["period_block","period_glyph","period_label"],"values":["1","2","3","4"]},{"key":"mode","roles":["acg_line","acg_glyph","acg_label"],"values":["zodiacal","mundane"]},{"key":"band","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":"open"},{"key":"lane","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":"open"},{"key":"medium","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["screen","print","export"]},{"key":"scheme","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["light","dark"]},{"key":"contrast","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["normal","high"]},{"key":"density","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["normal","compact"]},{"key":"kind","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["wheel","dial","strip","declination","grid","tally","timeline","midpoints","dispositors","calendar","ephemeris","cycles","periods","map","table"]},{"key":"modulus","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":["360","90","45","30","72","22.5"]},{"key":"id","roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"],"values":"open"}],"measures":["longitude","speed","latitude","declination","orb","orb_ratio","displacement"],"properties":["stroke","fill","width","dash","opacity","font_size","font_weight","glyph_variant","decoration","min_width","template"],"roles":["band_boundary","frame_box","grid_line","division_sector","division_glyph","division_label","tick","tick_label","cusp_line","cusp_label","angle_line","quadrant_sector","angle_label","body_glyph","motion_mark","position_label","sign_label","pointer","relation_line","relation_glyph","orb_label","pattern_fill","event_bar","event_exact","event_station","event_glyph","event_label","divider","edge","table_row","almanac_glyph","almanac_label","almanac_bar","period_block","period_glyph","period_label","curve","curve_fill","curve_marker","ref_line","basemap","acg_line","acg_glyph","acg_label","place_marker","indicator","title","subtitle","legend"]}`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { vocabulary } from './index.js';
5
+ describe('the vocabulary export (spec §13.5, §19)', () => {
6
+ it('matches the shipped docs/vocabulary.json byte for byte', () => {
7
+ const shipped = JSON.parse(readFileSync(fileURLToPath(new URL('../docs/vocabulary.json', import.meta.url)), 'utf8'));
8
+ expect(vocabulary).toEqual(shipped);
9
+ });
10
+ it('carries the tag keys a rule builder needs', () => {
11
+ const byKey = new Map(vocabulary.keys.map((entry) => [entry.key, entry]));
12
+ expect(byKey.get('role')?.values).toContain('body_glyph');
13
+ expect(byKey.get('aspect_class')?.values).toContain('minor');
14
+ expect(byKey.get('body')?.values).toBe('open');
15
+ expect(byKey.get('house')?.values).toContain('7');
16
+ for (const entry of vocabulary.keys) {
17
+ expect(entry.roles.length, entry.key).toBeGreaterThan(0);
18
+ if (entry.values !== 'open') {
19
+ expect(entry.values.length, entry.key).toBeGreaterThan(0);
20
+ }
21
+ }
22
+ });
23
+ it('carries the measures and roles', () => {
24
+ expect(vocabulary.measures).toContain('orb_ratio');
25
+ expect(vocabulary.roles).toContain('acg_line');
26
+ });
27
+ // Spec §13.5 (2026-09-06): the §10.5 rule-settable properties ride along
28
+ // so an explain host walks them without a hand-maintained list.
29
+ it('carries the rule-settable style properties', () => {
30
+ expect(vocabulary.properties).toContain('fill');
31
+ expect(vocabulary.properties).toContain('opacity');
32
+ expect(vocabulary.properties).toContain('template');
33
+ expect(vocabulary.properties).not.toContain('stroke_only');
34
+ expect(vocabulary.properties.length).toBeGreaterThanOrEqual(11);
35
+ });
36
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The raw §13.5 surface: JSON strings in, JSON strings out. Both
3
+ * entries — the Node CJS artifact and the browser web-target artifact —
4
+ * satisfy this shape; the wrappers in `armilla.ts` only ever see it.
5
+ */
6
+ export interface WasmModule {
7
+ layout(chartInput: string, themeChain: string, viewState: string, environment: string, options: string): string;
8
+ lint(themeChain: string): string;
9
+ migrate(kind: string, doc: string): string;
10
+ bookmark(viewState: string): string;
11
+ unbookmark(s: string): string;
12
+ explain(scene: string, themeChain: string, markId: string, property: string): string;
13
+ validate_workspace(workspace: string): string;
14
+ default_theme(): string;
15
+ paint_svg(scene: string, packManifests: string): string;
16
+ table_rows(chartInput: string, viewState: string, environment: string, options: string): string;
17
+ map_lines(chartInput: string, viewState: string, environment: string): string;
18
+ version(): string;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/wasm.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Ready immediately on Node; the artifact loads lazily and synchronously. */
2
+ export declare function init(source?: unknown): Promise<void>;
package/dist/wasm.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The Node loader: the CJS artifact built by `pnpm build:wasm`
3
+ * (wasm-pack, `--target nodejs`) into `packages/core/wasm/` loads
4
+ * synchronously through `createRequire` on first use — importing this
5
+ * module registers the loader, so Node consumers never call `init()`
6
+ * (it resolves immediately for entry parity with the browser).
7
+ */
8
+ import { createRequire } from 'node:module';
9
+ import { provideLoader } from './runtime.js';
10
+ const require = createRequire(import.meta.url);
11
+ provideLoader(() => require('../wasm/armilla_wasm.js'));
12
+ /** Ready immediately on Node; the artifact loads lazily and synchronously. */
13
+ export function init(source) {
14
+ void source;
15
+ return Promise.resolve();
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The browser entry (plinth/planning/browser-packaging-and-viewer.md,
3
+ * Proposal A): the same surface as the Node entry behind one async
4
+ * `init()`. The test boots the web-target artifact from raw bytes —
5
+ * exactly what a browser does after fetch, minus the network — so the
6
+ * whole dual-target contract runs under Node.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { describe, expect, it } from 'vitest';
10
+ import * as web from './index.web.js';
11
+ const natal = JSON.parse(readFileSync(new URL('../../../fixtures/charts/natal_timed.json', import.meta.url), 'utf-8'));
12
+ const environment = {
13
+ medium: 'screen',
14
+ scheme: 'light',
15
+ contrast: 'normal',
16
+ motion: 'full',
17
+ density: 'normal',
18
+ };
19
+ describe('the web entry', () => {
20
+ it('demands init() before any surface call', () => {
21
+ expect(() => web.version()).toThrow(/init\(\)/);
22
+ });
23
+ it('serves the full surface after init, and init is idempotent', async () => {
24
+ const bytes = readFileSync(new URL('../wasm-web/armilla_wasm_bg.wasm', import.meta.url));
25
+ await web.init(bytes);
26
+ await web.init(bytes); // second call is a no-op
27
+ expect(web.version()).toMatch(/^\d+\.\d+\.\d+/);
28
+ const scene = web.layout(natal, [], { schema_version: 1 }, environment, { kind: 'wheel' });
29
+ expect(web.isScene(scene)).toBe(true);
30
+ expect(scene.marks.length).toBeGreaterThan(0);
31
+ // The non-WASM exports ride both entries.
32
+ expect(web.naturalEarth110m.id).toBe('natural_earth_110m');
33
+ });
34
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ArmillaError, validateWorkspace } from './index.js';
3
+ const single = {
4
+ schema_version: 1,
5
+ id: 'single',
6
+ name: 'Single wheel',
7
+ chart_roles: ['primary'],
8
+ layout: { kind: 'grid', columns: 1, rows: 1 },
9
+ panels: [
10
+ {
11
+ id: 'wheel',
12
+ kind: 'wheel',
13
+ charts: { chart_1: 'primary' },
14
+ theme: 'inherit',
15
+ view: 'own',
16
+ cell: { col: 0, row: 0, col_span: 1, row_span: 1 },
17
+ },
18
+ ],
19
+ };
20
+ describe('validateWorkspace', () => {
21
+ it('returns the normalized workspace', () => {
22
+ const ws = validateWorkspace(single);
23
+ expect(ws.id).toBe('single');
24
+ expect(ws.panels[0]?.view).toBe('own');
25
+ expect(ws.panels[0]?.theme).toBe('inherit');
26
+ });
27
+ it('throws the spec error record for a broken reference', () => {
28
+ const broken = {
29
+ ...single,
30
+ panels: [{ ...single.panels[0], frame: 'visitor' }],
31
+ };
32
+ expect(() => validateWorkspace(broken)).toThrow(ArmillaError);
33
+ try {
34
+ validateWorkspace(broken);
35
+ }
36
+ catch (e) {
37
+ const err = e;
38
+ expect(err.code).toBe('REF');
39
+ expect(err.document).toBe('workspace');
40
+ expect(err.path).toBe('/panels/0/frame');
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,37 @@
1
+ # Schema changelog
2
+
3
+ Migration notes per schema version (spec §4.2, §19). Documents migrate forward with
4
+ `migrate(kind, doc)`; the scene is never migrated — painters reject unknown scene versions.
5
+
6
+ ## Version 1
7
+
8
+ Initial schemas: chart input, theme, view state, workspace, environment (unversioned by design —
9
+ its field set is fixed by the spec), and scene. No migrations exist yet; the first schema bump
10
+ adds its upgrade function to `crates/core/src/migrate.rs` and a `migration_v1_*` fixture, and
11
+ documents the change here.
12
+
13
+ Additions within version 1 (optional fields; older documents parse unchanged):
14
+
15
+ - 2026-09-06 — view-state `TreatRule` gains `disabled` (default false; the rule rides the
16
+ document and its bookmark but the treatment cascade skips it) and `name` (host-display
17
+ label, never interpreted). Spec §3.5.
18
+ - 2026-09-20 — theme `TrackGeometry` gains `decan_rulers` (`"chaldean"` | `"triplicity"`,
19
+ default chaldean) and `term_table` (`"egyptian"` | `"ptolemaic"`, default egyptian), and
20
+ the band roles `decans` and `terms` become structural (360-axis subdivision rings).
21
+ Spec §3.4, §6.7.
22
+ - 2026-09-20 — view state gains `expanded` (period subject keys whose child periods draw —
23
+ the §6.16 drill-down; unknown keys are ignored), and `geometry.periods.technique` gains
24
+ `"stack"` (every technique present, grouped in canonical order over one axis). Spec §3.4,
25
+ §3.5, §6.16.
26
+ - 2026-09-20 — `geometry.cycles` gains `shape: "orb"` with `aspect` (degrees 0-180, default
27
+ 0) and `orb` (the window, default 10) — the aspect-orb waveform, exact at the top
28
+ (§6.15). Older documents parse unchanged.
29
+ - 2026-09-20 — the predicate vocabulary gains the ambient `modulus` tag (§7.3): the resolved
30
+ periodic fold, present like `kind` wherever one exists (wheel 360; dial/strip resolution;
31
+ midpoint sort; longitude ephemeris) and absent on non-periodic tracks. Closed values
32
+ `360, 90, 45, 30, 72, 22.5`. No document schema change; rules naming `modulus` fail
33
+ validation on older cores.
34
+ - 2026-09-20 — treatment semantics (§9.2, §11.2): visibility and emphasis become separate
35
+ cascade channels — a later `dim`/`emphasize` no longer resurrects a hidden mark; only
36
+ `show` cancels a hide (restoring normal emphasis, as before). Behavioral, no schema
37
+ change; rule lists that relied on dim-after-hide re-showing marks now keep them hidden.
@@ -0,0 +1,52 @@
1
+ # Coordinate mapping
2
+
3
+ Worked examples for the §6 track mapping, generated from the `coordinates`
4
+ fixture by `UPDATE_DOCS=1 cargo test -p armilla-core --test docs`; do not
5
+ edit by hand. The fixture pins an ascendant of 100° with whole-sign houses;
6
+ each table lists the fixture's bodies and where their `body_glyph` lands in
7
+ the default space for that track kind (wheel and dial 1000×1000, strip and
8
+ declination 1000×400, margin 20, default theme, empty view state).
9
+
10
+ ## Wheel
11
+
12
+ Origin longitude 100°, reference angle 180°, modulus 360°.
13
+
14
+ | body | longitude | declination | x | y |
15
+ | --- | --- | --- | --- | --- |
16
+ | sun | 100° | 10° | 109.524 | 500.000 |
17
+ | moon | 130° | 10° | 161.838 | 695.238 |
18
+ | mercury | 190° | 23.9° | 500.000 | 890.476 |
19
+ | venus | 280° | -23° | 890.476 | 500.000 |
20
+
21
+ ## Dial
22
+
23
+ Origin longitude 0°, reference angle 90°, modulus 90°.
24
+
25
+ | body | longitude | declination | x | y |
26
+ | --- | --- | --- | --- | --- |
27
+ | sun | 100° | 10° | 779.613 | 166.771 |
28
+ | moon | 130° | 10° | 648.779 | 908.766 |
29
+ | mercury | 190° | 23.9° | 758.082 | 149.831 |
30
+ | venus | 280° | -23° | 800.034 | 185.032 |
31
+
32
+ ## Strip
33
+
34
+ Origin longitude 0°, reference angle 0°, modulus 360°.
35
+
36
+ | body | longitude | declination | x | y |
37
+ | --- | --- | --- | --- | --- |
38
+ | sun | 100° | 10° | 286.667 | 239.769 |
39
+ | moon | 130° | 10° | 366.667 | 239.769 |
40
+ | mercury | 190° | 23.9° | 526.667 | 239.769 |
41
+ | venus | 280° | -23° | 766.667 | 239.769 |
42
+
43
+ ## Declination
44
+
45
+ Origin longitude 0°, reference angle 0°, modulus none (bounded axis).
46
+
47
+ | body | longitude | declination | x | y |
48
+ | --- | --- | --- | --- | --- |
49
+ | sun | 100° | 10° | 678.376 | 105.000 |
50
+ | moon | 130° | 10° | 633.624 | 105.000 |
51
+ | mercury | 190° | 23.9° | 888.517 | 105.000 |
52
+ | venus | 280° | -23° | 157.211 | 105.000 |
package/docs/themes.md ADDED
@@ -0,0 +1,111 @@
1
+ # Theme authoring guide
2
+
3
+ A theme is a JSON document: identity, a glyph pack reference, typography, a palette, per-track
4
+ geometry, and an ordered list of style rules. Everything visual in a rendered chart that is not
5
+ chart data comes from a theme. This guide covers the schema (spec §3.4), the cascade (§10.1–10.2),
6
+ the palette (§10.3), `scale()` (§10.4), environment tags (§9.1), and every linter code (§10.7).
7
+
8
+ ## The document
9
+
10
+ ```json
11
+ {
12
+ "schema_version": 1,
13
+ "id": "midnight",
14
+ "name": "Midnight",
15
+ "version": "1.0.0",
16
+ "author": "you",
17
+ "license": "MIT",
18
+ "extends": "default",
19
+ "palette": { "accent": { "light": "#7a3b2e", "dark": "#e0a899" } },
20
+ "rules": [
21
+ { "when": "role=body_glyph class=luminary", "style": { "fill": "$accent" } }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ `id` is an identifier (`[a-z0-9_]+`). `extends` names the parent theme; the host resolves the
27
+ chain and passes `[root, …, leaf]` to the core. `glyph_pack`, `typography`, and per-track
28
+ `geometry` (bands, band order, reference angle, origin, sense, modulus, glyph and label scales,
29
+ axis inset, ruler edge, legend alignment, collision strategy) are optional at every level except the default root, which defines all of them.
30
+
31
+ ## The cascade
32
+
33
+ The chain is validated (depth ≤ 8, each `extends` naming its predecessor) and rules concatenate
34
+ **root-first**: the compiled-in default theme is always the implicit root, your ancestors follow,
35
+ your leaf comes last. Within the concatenated list, later rules win property-by-property — a rule
36
+ sets only the style keys it names, and everything else flows through from earlier matches.
37
+ `palette`, `typography`, `geometry`, and `glyph_pack` deep-merge leaf-over-root instead: a child
38
+ may override one token, one size, or one band extent without restating the rest.
39
+
40
+ A rule is a predicate over tags plus a partial style:
41
+
42
+ ```json
43
+ { "when": "aspect_class=hard scope=inter orb<2", "style": { "width": 2, "stroke": "$accent" } }
44
+ ```
45
+
46
+ The predicate language (§9.1) is a conjunction of `key=value` terms and `measure<n` / `measure>n`
47
+ comparisons. Keys and closed value sets are listed in the generated
48
+ [vocabulary reference](vocabulary.md).
49
+
50
+ ## The palette
51
+
52
+ `$token` in any color-valued property resolves to `palette[token][scheme]` for the environment's
53
+ scheme. If `dark` is absent, the core derives it from `light` by a fixed transform, so a
54
+ light-only palette still renders on dark grounds. Naming a token the merged palette lacks is a
55
+ validation error (`L005`), not a silent fallback.
56
+
57
+ ## `scale()` — measures into styles
58
+
59
+ `scale(measure, [d0, d1], [r0, r1])` interpolates the mark's measure linearly from domain to
60
+ range, clamped at the ends. The classic use is orb-proportional aspect fading:
61
+
62
+ ```json
63
+ { "when": "role=relation_line", "style": { "opacity": "scale(orb, [0, 6], [1, 0.15])" } }
64
+ ```
65
+
66
+ Domain bounds must be finite and distinct (`L006`). A mark that lacks the measure keeps the
67
+ property's prior cascade value.
68
+
69
+ ## `template` — text from data
70
+
71
+ Roles that compose text from data honor the `template` property (today: `orb_label`). A non-empty
72
+ template replaces the role's built-in format; `{p}` is the phase letter (`A`/`S`, empty when the
73
+ record carries no phase), `{d}` the degrees, `{m}` the zero-padded minutes; everything else is
74
+ literal. `{ "when": "role=orb_label", "style": { "template": "{p}{d}°{m}′" } }` turns `2S57` into
75
+ `S2°57′`. Like every property it cascades, so a rule can scope a format to one aspect class or
76
+ phase. An empty or unset template means the built-in form.
77
+
78
+ ## Environment tags
79
+
80
+ The five environment fields — `medium`, `scheme`, `contrast`, `motion`, `density` — are available
81
+ to predicates as tags with the same keys, so a theme adapts without forking:
82
+
83
+ ```json
84
+ { "when": "medium=print", "style": { "min_width": 0.75 } }
85
+ ```
86
+
87
+ `density=compact` additionally applies the theme's `geometry.compact` overrides before layout.
88
+
89
+ ## The linter
90
+
91
+ `lint(theme_chain)` returns findings; errors fail validation, warnings do not block. Every code,
92
+ with a trigger that fires it:
93
+
94
+ <!-- generated: linter table -->
95
+
96
+ | code | severity | meaning | example trigger |
97
+ | --- | --- | --- | --- |
98
+ | L001 | Warning | Shadowed: every property a rule sets is reset by a later, broader rule. | two rules with `"when": "body=sun"` both setting `opacity` |
99
+ | L002 | Warning | Broad after specific: a rule without a role term follows one with one. | `"when": "role=body_glyph"` then `"when": "sign=aries"` |
100
+ | L003 | Error | Unknown predicate key. | `"when": "planet=sun"` — the key is `body` |
101
+ | L004 | Error | Unknown value for a closed-value key. | `"when": "sign=orion"` |
102
+ | L005 | Error | `$token` is not in the merged palette. | `"fill": "$missing"` with no palette entry |
103
+ | L006 | Error | Degenerate `scale()`: non-finite bounds or an empty domain. | `"opacity": "scale(orb, [5, 5], [0, 1])"` |
104
+ | L007 | Error | Style property unknown or not settable from a rule. | `"style": { "sparkle": 1 }` |
105
+ | L008 | Warning | The predicate can never match. | `"when": "sign=aries sign=taurus"` — `sign` is single-valued |
106
+ | L009 | Error | The chain repeats a theme id. | two themes in the chain with `"id": "dup"` |
107
+
108
+ <!-- /generated -->
109
+
110
+ Run the linter from the binding (`lint(chain)`) or through any host that exposes it; findings
111
+ carry the concatenated `rule_index`, so index arithmetic matches `explain()`.