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.
- package/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +83 -0
- package/agents/dreamteamer.agent.md +7 -0
- package/bin/dreamteamer.js +65 -0
- package/collection-templates/docs.collection-template.yaml +14 -0
- package/collection-templates/entity.collection-template.yaml +16 -0
- package/collections/agents.collection.yaml +33 -0
- package/collections/collection-templates.collection.yaml +20 -0
- package/collections/collections.collection.yaml +78 -0
- package/collections/command-bindings.collection.yaml +46 -0
- package/collections/commands.collection.yaml +36 -0
- package/collections/repos.collection.yaml +42 -0
- package/collections/skills.collection.yaml +22 -0
- package/collections/ui-views.collection.yaml +48 -0
- package/collections/users.collection.yaml +21 -0
- package/package.json +58 -0
- package/skills/building-dreamteamer/SKILL.md +117 -0
- package/skills/building-dreamteamer/references/agents.md +44 -0
- package/skills/building-dreamteamer/references/before-you-build.md +42 -0
- package/skills/building-dreamteamer/references/collections.md +120 -0
- package/skills/building-dreamteamer/references/commands.md +69 -0
- package/skills/building-dreamteamer/references/skills.md +73 -0
- package/skills/building-dreamteamer/references/ui-components.md +78 -0
- package/skills/building-dreamteamer/references/ui-views.md +59 -0
- package/skills/using-dreamteamer/SKILL.md +100 -0
- package/skills/using-dreamteamer/references/git-events.md +64 -0
- package/skills/using-dreamteamer/references/records.md +102 -0
- package/src/check.js +193 -0
- package/src/cli.js +250 -0
- package/src/collections-cli.js +389 -0
- package/src/commit.js +117 -0
- package/src/compile.js +747 -0
- package/src/events.js +124 -0
- package/src/field-values.js +69 -0
- package/src/filter.js +107 -0
- package/src/harnesses.js +233 -0
- package/src/history.js +64 -0
- package/src/init.js +307 -0
- package/src/presentation.js +190 -0
- package/src/record-commands.js +84 -0
- package/src/records.js +73 -0
- package/src/runtime.js +96 -0
- package/src/schema-ops.js +263 -0
- package/src/semver.js +32 -0
- package/src/server.js +291 -0
- package/src/store.js +450 -0
- package/src/template.js +98 -0
- package/src/temporal.js +149 -0
- package/src/workspace.js +51 -0
- package/src/yaml.js +6 -0
package/src/temporal.js
ADDED
|
@@ -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
|
+
}
|
package/src/workspace.js
ADDED
|
@@ -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 });
|