dreamteamer 0.6.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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/README.md +83 -0
  4. package/agents/dreamteamer.agent.md +7 -0
  5. package/bin/dreamteamer.js +65 -0
  6. package/collection-templates/docs.collection-template.yaml +14 -0
  7. package/collection-templates/entity.collection-template.yaml +16 -0
  8. package/collections/agents.collection.yaml +33 -0
  9. package/collections/collection-templates.collection.yaml +20 -0
  10. package/collections/collections.collection.yaml +78 -0
  11. package/collections/command-bindings.collection.yaml +46 -0
  12. package/collections/commands.collection.yaml +36 -0
  13. package/collections/repos.collection.yaml +42 -0
  14. package/collections/skills.collection.yaml +22 -0
  15. package/collections/ui-views.collection.yaml +48 -0
  16. package/collections/users.collection.yaml +21 -0
  17. package/package.json +58 -0
  18. package/skills/building-dreamteamer/SKILL.md +117 -0
  19. package/skills/building-dreamteamer/references/agents.md +44 -0
  20. package/skills/building-dreamteamer/references/before-you-build.md +42 -0
  21. package/skills/building-dreamteamer/references/collections.md +120 -0
  22. package/skills/building-dreamteamer/references/commands.md +69 -0
  23. package/skills/building-dreamteamer/references/skills.md +73 -0
  24. package/skills/building-dreamteamer/references/ui-components.md +78 -0
  25. package/skills/building-dreamteamer/references/ui-views.md +59 -0
  26. package/skills/using-dreamteamer/SKILL.md +100 -0
  27. package/skills/using-dreamteamer/references/git-events.md +64 -0
  28. package/skills/using-dreamteamer/references/records.md +102 -0
  29. package/src/check.js +193 -0
  30. package/src/cli.js +250 -0
  31. package/src/collections-cli.js +389 -0
  32. package/src/commit.js +117 -0
  33. package/src/compile.js +747 -0
  34. package/src/events.js +124 -0
  35. package/src/field-values.js +69 -0
  36. package/src/filter.js +107 -0
  37. package/src/harnesses.js +233 -0
  38. package/src/history.js +64 -0
  39. package/src/init.js +307 -0
  40. package/src/presentation.js +190 -0
  41. package/src/record-commands.js +84 -0
  42. package/src/records.js +73 -0
  43. package/src/runtime.js +96 -0
  44. package/src/schema-ops.js +263 -0
  45. package/src/semver.js +32 -0
  46. package/src/server.js +291 -0
  47. package/src/store.js +450 -0
  48. package/src/template.js +98 -0
  49. package/src/temporal.js +149 -0
  50. package/src/workspace.js +51 -0
  51. package/src/yaml.js +6 -0
@@ -0,0 +1,149 @@
1
+ // temporal values — the `format: date` and `format: date-time` fields — normalized on WRITE and
2
+ // compared as INSTANTS, never as strings.
3
+ //
4
+ // Two decisions live here, and they are a pair:
5
+ //
6
+ // 1. A date-time keeps its LOCAL OFFSET (`2026-07-28T12:00:00+03:00`), it is not folded to Z.
7
+ // These records are markdown files a human reads and reviews in a git diff. A meeting at noon
8
+ // must say 12:00 in the file, not 09:00 with the reader expected to do timezone arithmetic in
9
+ // their head. The offset is what makes that wall-clock reading unambiguous rather than merely
10
+ // convenient — the value still denotes exactly one instant.
11
+ //
12
+ // 2. Because (1) means two correct values can carry different offsets, ordering CANNOT be a string
13
+ // compare. `compareValues` parses both sides to epoch ms first. Everything that orders records
14
+ // — `_lt`/`_gt`/`_lte`/`_gte`/`_between` in filter.js and the `?sort=` in server.js and the
15
+ // extension's api.ts — goes through it. Sorting temporals lexicographically is exactly the bug
16
+ // (1) would otherwise have introduced: `…T12:00:00+03:00` sorts after `…T11:00:00+01:00`
17
+ // (an EARLIER instant) on every naive comparison.
18
+ //
19
+ // The write-side normalizer is why the strictness is bearable: ajv's `date-time` accepts one
20
+ // spelling, but humans type `2026-07-28 12:00` and `<input type="datetime-local">` emits
21
+ // `2026-07-28T12:00`. Both become the canonical form before validation, so the CLI and the studio
22
+ // accept the same input — the engine/UI parity test applied to a data format.
23
+
24
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
25
+ const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3})\d*)?\s*(Z|[+-]\d{2}:?\d{2})?$/i;
26
+
27
+ /** `+03:00` for the machine's local offset AT that wall clock — DST-correct, unlike a bare "now" offset. */
28
+ function localOffsetAt(y, mo, d, h, mi, s) {
29
+ const mins = -new Date(y, mo - 1, d, h, mi, s).getTimezoneOffset();
30
+ const sign = mins < 0 ? '-' : '+';
31
+ const abs = Math.abs(mins);
32
+ return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
33
+ }
34
+
35
+ /** `+0300` / `+03:00` / `z` → `+03:00` / `Z`. One spelling on disk keeps diffs quiet. */
36
+ function canonicalZone(zone) {
37
+ if (!zone) return null;
38
+ if (/^z$/i.test(zone)) return 'Z';
39
+ return zone.length === 5 ? `${zone.slice(0, 3)}:${zone.slice(3)}` : zone;
40
+ }
41
+
42
+ /**
43
+ * A temporal string → epoch ms, or null when the value isn't one (so callers can fall back).
44
+ *
45
+ * A date-only value resolves at UTC midnight, NOT local midnight: it names a calendar day, and
46
+ * anchoring it to the machine's zone would make the same two records sort differently on two
47
+ * laptops. A zoneless date-time is the one case that IS machine-local — that is what "no zone"
48
+ * means, and the normalizer stamps an explicit offset on it before it ever reaches disk.
49
+ */
50
+ export function parseTemporal(value) {
51
+ if (typeof value !== 'string' || value === '') return null;
52
+
53
+ const d = DATE_ONLY.exec(value);
54
+ if (d) return Date.UTC(Number(d[1]), Number(d[2]) - 1, Number(d[3]));
55
+
56
+ const m = DATE_TIME.exec(value);
57
+ if (!m) return null;
58
+ const [, y, mo, day, h, mi, s = '0', ms = '0', zone] = m;
59
+ const z = canonicalZone(zone);
60
+ if (!z) {
61
+ return new Date(Number(y), Number(mo) - 1, Number(day), Number(h), Number(mi), Number(s), Number(ms.padEnd(3, '0'))).getTime();
62
+ }
63
+ const t = Date.parse(`${y}-${mo}-${day}T${h}:${mi}:${s.padStart(2, '0')}.${ms.padEnd(3, '0')}${z}`);
64
+ return Number.isNaN(t) ? null : t;
65
+ }
66
+
67
+ /**
68
+ * Coerce one value to the canonical spelling for its declared `format`. Anything unrecognized is
69
+ * returned untouched — ajv rejects it a moment later with a better message than we could write.
70
+ *
71
+ * `date-time`: seconds are filled in, the zone is canonicalized, and a MISSING zone becomes the
72
+ * machine's local offset at that wall clock. `date`: a date-time is truncated to the calendar day
73
+ * AS WRITTEN — the day in the value's own offset, not in UTC and not in the reader's zone, because
74
+ * "2026-07-28T23:00+03:00" is a meeting on the 28th to everyone who cares about it.
75
+ */
76
+ export function normalizeTemporal(value, format) {
77
+ if (typeof value !== 'string' || value === '') return value;
78
+
79
+ if (format === 'date') {
80
+ const m = DATE_TIME.exec(value);
81
+ return m ? `${m[1]}-${m[2]}-${m[3]}` : value;
82
+ }
83
+
84
+ if (format !== 'date-time') return value;
85
+
86
+ const d = DATE_ONLY.exec(value);
87
+ if (d) return `${value}T00:00:00${localOffsetAt(Number(d[1]), Number(d[2]), Number(d[3]), 0, 0, 0)}`;
88
+
89
+ const m = DATE_TIME.exec(value);
90
+ if (!m) return value;
91
+ const [, y, mo, day, h, mi, s = '00', , zone] = m;
92
+ const sec = String(s).padStart(2, '0');
93
+ const z = canonicalZone(zone) ?? localOffsetAt(Number(y), Number(mo), Number(day), Number(h), Number(mi), Number(sec));
94
+ return `${y}-${mo}-${day}T${h}:${mi}:${sec}${z}`;
95
+ }
96
+
97
+ /**
98
+ * Normalize every temporal in a record IN PLACE, walking the schema (not the data) so only
99
+ * declared date/date-time fields are touched — a plain string that happens to look like a date
100
+ * stays exactly as the author typed it. Recurses through object properties and array items.
101
+ */
102
+ export function normalizeRecord(schema, fields) {
103
+ if (!schema || fields === null || typeof fields !== 'object') return fields;
104
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
105
+ const value = fields[key];
106
+ if (value === undefined || value === null) continue;
107
+ if (prop?.format === 'date' || prop?.format === 'date-time') {
108
+ fields[key] = normalizeTemporal(value, prop.format);
109
+ } else if (prop?.type === 'object') {
110
+ normalizeRecord(prop, value);
111
+ } else if (prop?.type === 'array' && Array.isArray(value)) {
112
+ const items = prop.items;
113
+ if (items?.format === 'date' || items?.format === 'date-time') {
114
+ fields[key] = value.map((v) => normalizeTemporal(v, items.format));
115
+ } else if (items?.type === 'object') {
116
+ for (const row of value) normalizeRecord(items, row);
117
+ }
118
+ }
119
+ }
120
+ return fields;
121
+ }
122
+
123
+ /**
124
+ * The ordering used by every range filter and every `?sort=`. Temporals first (as instants),
125
+ * then numbers numerically, then locale string order.
126
+ *
127
+ * The numeric branch deliberately excludes `''`: `Number('')` is 0, which would sort a blank field
128
+ * as "zero" and slot it between real numbers instead of grouping the blanks together.
129
+ */
130
+ export function compareValues(a, b) {
131
+ const ta = parseTemporal(a);
132
+ const tb = parseTemporal(b);
133
+ if (ta !== null && tb !== null) return ta - tb;
134
+
135
+ if (a !== '' && b !== '' && a != null && b != null) {
136
+ const na = Number(a);
137
+ const nb = Number(b);
138
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb;
139
+ }
140
+ return String(a ?? '').localeCompare(String(b ?? ''));
141
+ }
142
+
143
+ /** `?sort=field` / `?sort=-field`, ordered with `compareValues`. Mutates and returns `rows`. */
144
+ export function sortRows(rows, sort) {
145
+ if (!sort) return rows;
146
+ const desc = String(sort).startsWith('-');
147
+ const key = desc ? String(sort).slice(1) : String(sort);
148
+ return rows.sort((a, b) => compareValues(a[key] ?? '', b[key] ?? '') * (desc ? -1 : 1));
149
+ }
@@ -0,0 +1,51 @@
1
+ // workspace discovery: the NEAREST package.json with a `dreamteamer` section wins, except when
2
+ // that candidate is only nested inside a higher one as a MODULE — modules are themselves
3
+ // dreamteamer packages (fractal), so `git_modules/dreamteamer`, `node_modules/@dreamteamer/*` and
4
+ // `modules/<workspace-module>` must all resolve outward to the workspace that contains them.
5
+ // It used to be "topmost wins", which got the module cases right by accident and every genuinely
6
+ // nested workspace wrong: a workspace living under another one's tree (a vault may keep per-identity
7
+ // repos at projects/<identity>/<repo>/) resolved to the OUTER workspace, so every command
8
+ // silently operated on the wrong repo — compile wrote the wrong runtime, check counted the wrong
9
+ // records, all reporting success. `projects/` is not a module segment, so nesting there is a
10
+ // real workspace and now resolves as one.
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ // path segments that mean "the thing below me is a module of the thing above me", never a workspace
15
+ const MODULE_SEGMENTS = new Set(['node_modules', 'git_modules', 'modules']);
16
+
17
+ export function findWorkspace(start = process.cwd()) {
18
+ let dir = path.resolve(start);
19
+ const candidates = []; // nearest → topmost
20
+ while (true) {
21
+ const p = path.join(dir, 'package.json');
22
+ if (fs.existsSync(p)) {
23
+ try {
24
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
25
+ if ('dreamteamer' in pkg) candidates.push({ root: dir, pkg });
26
+ } catch { /* unparseable package.json never disqualifies a dir */ }
27
+ }
28
+ const parent = path.dirname(dir);
29
+ if (parent === dir) break;
30
+ dir = parent;
31
+ }
32
+ if (!candidates.length) {
33
+ throw new Error('not a dreamteamer workspace — no package.json with a "dreamteamer" section found here or above');
34
+ }
35
+ // climb out of module nesting only: stop at the first ancestor that contains the current pick
36
+ // as something OTHER than a module.
37
+ let found = candidates[0];
38
+ for (const higher of candidates.slice(1)) {
39
+ if (!nestedAsModule(higher.root, found.root)) break;
40
+ found = higher;
41
+ }
42
+ return found;
43
+ }
44
+
45
+ /** Is `inner` reached from `outer` by descending through a module folder? */
46
+ export function nestedAsModule(outer, inner) {
47
+ return path
48
+ .relative(outer, inner)
49
+ .split(path.sep)
50
+ .some((seg) => MODULE_SEGMENTS.has(seg));
51
+ }
package/src/yaml.js ADDED
@@ -0,0 +1,6 @@
1
+ // contract rule: YAML is parsed with the CORE schema — unquoted dates stay strings,
2
+ // never timestamp objects. ALL dreamteamer tooling loads YAML through here.
3
+ import yaml from 'js-yaml';
4
+
5
+ export const load = (text) => yaml.load(text, { schema: yaml.CORE_SCHEMA });
6
+ export const dump = (obj, opts = {}) => yaml.dump(obj, { lineWidth: 120, ...opts });