dreamteamer 0.16.1 → 0.18.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.
@@ -105,6 +105,16 @@ schema:
105
105
  icon:
106
106
  type: string
107
107
  description: material-symbols-outlined icon name, drawn in the nav and page header. The VS Code tree maps it to the nearest codicon — an unmapped name falls back to a generic cylinder, so pick one that is already mapped or add the row.
108
+ unresolved_peers:
109
+ type: array
110
+ items: { type: string }
111
+ description: >-
112
+ DERIVED by compile, never authored — the collections THIS one references that its module
113
+ declared as a `peerDependencies` peer and nothing installed provides. `check` reads it to
114
+ excuse those references as unresolvable rather than wrong, which is what lets a module be
115
+ opened on its own. It is stated here as DATA precisely so the record layer never has to
116
+ learn what a module is; a bare string list rather than `x-reference: collections`, because
117
+ the whole point is that the target is absent.
108
118
  owner:
109
119
  type: string
110
120
  x-reference: modules
@@ -66,6 +66,14 @@ schema:
66
66
  items:
67
67
  type: string
68
68
  x-reference: collections
69
+ # SOFT, because the normal state of a module opened on its own is that its peers are NOT
70
+ # installed — that is the whole reason a peer is declared rather than depended on. Validated
71
+ # hard, this field made the record it is projected onto fail `check` for saying exactly what
72
+ # it exists to say, and there was no state in which an optional cross-module reference
73
+ # passed: dropping the declaration made `compile` fail instead, naming peerDependencies as
74
+ # the remedy. Present peers still resolve, and a peer that names a collection nobody ever
75
+ # installs is reported by `check` as a warning off `unresolved_peers`, not silently.
76
+ x-reference-soft: true
69
77
  collections:
70
78
  type: array
71
79
  description: >-
@@ -33,7 +33,11 @@ schema:
33
33
  description: 'Opaque path segment grouping clones by the account that owns them: <repos-path>/<identity>/<name>.'
34
34
  path:
35
35
  type: string
36
- description: Workspace-relative path override. Wins over the derived <repos-path>/<identity>/<name>.
36
+ description: >-
37
+ Where the working tree goes, winning over the derived <repos-path>/<identity>/<name>. Takes
38
+ the ${env:NAME} / ${workspaceFolder} / ${userHome} templates, rendered when the path is
39
+ needed — which is how a repo that lives OUTSIDE this workspace is expressed, since an
40
+ absolute rendering escapes the root while a plain relative one still anchors to it.
37
41
  visibility:
38
42
  type: string
39
43
  enum: [private, public]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "A workspace compiler for coding agents — schema-validated records as plain files over git, compiled into every harness",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Gilad Khen <giladkhen@gmail.com>",
@@ -48,7 +48,8 @@
48
48
  "ajv-formats": "^3.0.1",
49
49
  "express": "^5.2.1",
50
50
  "fractional-indexing": "^4.0.0",
51
- "js-yaml": "^4.1.0"
51
+ "js-yaml": "^4.1.0",
52
+ "yaml": "2.8.1"
52
53
  },
53
54
  "dreamteamer": {
54
55
  "title": "System"
@@ -71,8 +71,19 @@ unpublished schema is not a state the workspace should sit in. The verbs and eve
71
71
  or **edit the owning module's descriptor by hand** and compile (the change ships with the
72
72
  module) — the only exit for a removal. Pick by who should own the field — `data-modeling.md`
73
73
  Part III.
74
+ - **`--type <collection>` beats the type sugar, always.** A type that names a collection in the
75
+ runtime is a reference to it, whatever `string`/`enum`/`date`/`tags`/… would otherwise mean — so
76
+ in a workspace that ships a `tags` collection, `--type tags` points at it and the relation flags
77
+ work on it. Only a stated `--type` resolves this way; omitting it still means a plain string.
74
78
  - `remove-field` on a populated field **clears the values in the same write and reports the
75
- count** — a leftover key would make every later write to those records fail as unknown.
79
+ count** — a leftover key would make every later write to those records fail as unknown. It also
80
+ prunes the field out of **the same descriptor's `list_fields` and `sort_field`** (that is the
81
+ field's own presentation, and a dangling `sort_field` is a compile error), and **warns, by id,**
82
+ about any ui-view whose `options.columns` still names it — a different source, so it is named
83
+ rather than edited.
84
+ - **`add-field` inserts before the `x-body` field**, on the same rule as a `templates:` merge
85
+ below: property order is form order, and a record's body belongs last. `update-field` never
86
+ reorders — an existing field keeps the place its author gave it.
76
87
  - **`schema rename-collection <old> <new>`** moves the descriptor **in the module that ships it**
77
88
  (its guard is against writes an `npm install` would erase, not against modules), plus the
78
89
  records, the filenames and every inbound reference — `x-reference` targets in other descriptors
@@ -82,8 +93,8 @@ unpublished schema is not a state the workspace should sit in. The verbs and eve
82
93
  does NOT overrule two authored choices: a hand-set `storage.path` (records stay put, and it
83
94
  says so) and a `storage.suffix` that is not the singular of the old name.
84
95
  - An **empty value removes** in dotted `set-view` writes just as it does in `dt set` — so a
85
- setting whose meaningful value IS empty (`options.sort: ''`, see `ui-views.md`) must be
86
- hand-written in the source file; the CLI cannot express it.
96
+ setting whose meaningful value IS empty (`options.sort: ''`, see `ui-views.md`) is written
97
+ QUOTED, which is what says "the empty string is the value": `'options.sort=""'`.
87
98
 
88
99
  ## declaring a module
89
100
 
@@ -817,8 +817,11 @@ useless until the flood is drained.
817
817
  message being the ledger); `schema remove-field` the old one — which clears any leftovers and
818
818
  reports the count.
819
819
  - **Values** (an id, a reference target): `dt rename <collection>/<old> <new>` rewrites inbound
820
- references; qualified prose wikilinks are followed, bare-name prose is not one more reason
821
- references in bodies are written qualified.
820
+ references. Prose wikilinks are followed in both spellings`[[collection/id]]` always, and a
821
+ bare `[[id]]` when that basename names exactly ONE record in the workspace; when something else
822
+ claims it, the link is left alone and the rename says so, naming the file. A `#anchor` and a
823
+ `|label` both ride through untouched (`[[id#heading|see here]]`) — only the record moved. Raw
824
+ prose that is not a wikilink is counted and reported, never rewritten.
822
825
 
823
826
  ### 42. Migrations are scripts, run once, committed with their effects
824
827
 
@@ -18,13 +18,13 @@ commands, ui-views, collections) are *sources*: edit the file under the owning m
18
18
 
19
19
  the verbs and flags are `dt help`'s job; what to know *about* them:
20
20
 
21
- - narrowing a `list`: `--filter k=v` for one condition; **anything compound goes in one
22
- `--where`** its operator grammar is enumerated in `dt help`, and it is
21
+ - narrowing a `list`: `--filter k=v` per condition, **repeated to AND more of them**
22
+ (`--filter status=todo --filter owner=ana` wants both); **anything an equality cannot say goes
23
+ in one `--where`** — its operator grammar is enumerated in `dt help`, and it is
23
24
  the same one views and gates use — e.g.
24
25
  `dt list health/prescriptions --where '{"_and":[{"patient":{"_eq":"health/patients/dana-levi"}},{"status":{"_eq":"active"}}]}'`.
25
- two `--filter` flags do NOT combine (the last one wins), and an unknown field or a dangling
26
- ref **narrows to nothing** rather than erroring — filter field names deserve the same care as
27
- code.
26
+ an unknown field or a dangling ref **narrows to nothing** rather than erroring filter field
27
+ names deserve the same care as code.
28
28
  - ⚠ **there is no `@me` and no `users` collection** (both removed in 0.8.0). when a person is
29
29
  needed, read `git config user.name`; filter on a person only when this workspace ships its own
30
30
  collection of people.
@@ -42,8 +42,12 @@ the verbs and flags are `dt help`'s job; what to know *about* them:
42
42
  nothing written. a rejected write leaves no partial state.
43
43
  - **a write puts the record on disk; `dt commit` publishes it** — committing is workspace policy
44
44
  (`auto-commit` in `package.json`, default off), never part of the write.
45
- - `set <collection>/<id> <field>=` with an empty value **removes** the field; array fields take a
46
- comma-separated value; the `x-body` field is set like any other field.
45
+ - `set <collection>/<id> <field>=` with an empty value **removes** the field; the `x-body` field is
46
+ set like any other field.
47
+ - an **array field** takes a comma-separated value (`--tags a,b`, `tags=a,b`) — or the flag/pair
48
+ **repeated**, one element per sighting (`--tags a --tags b`), which is how a value that itself
49
+ contains a comma gets written. ⚠ repeating a **scalar** field is refused, naming it: it used to
50
+ keep the last value silently, so the first one never reached disk.
47
51
  - ids generate from the record's own creation-time values — pass `--id` only when the operator
48
52
  named one.
49
53
 
@@ -205,7 +209,8 @@ two hundred.
205
209
  |---|---|
206
210
  | `mv data/tasks/old.task.md …/new.task.md` | every inbound ref now dangles. `dt rename`. |
207
211
  | renaming a record because its title changed | the id is not a display name — edit the field |
208
- | two `--filter` flags to AND conditions | the last flag wins compound conditions go in one `--where` |
212
+ | a `--filter` per condition | right they AND; only what equality cannot express needs `--where` |
213
+ | `--tags a --tags b` on a scalar field | refused, naming the field — a repeat is an array ELEMENT, and a scalar has no room for two |
209
214
  | `--force` to get past an `rm` refusal | it leaves inbound refs dangling — retarget them first (unless the refusal named a *prose* mention, which isn't a real reference) |
210
215
  | omitting schema defaults from a hand-written file | the file stops being legible without the schema |
211
216
  | unquoted `due: 2026-07-28` in hand-written YAML | dreamteamer parses CORE_SCHEMA so it stays a string *here*, but a default-schema YAML reader turns it into a timestamp — quote dates |
@@ -121,9 +121,16 @@ same record. A misspelled key is read by nobody, silently. The two edges that bi
121
121
  naming a field the schema lacks is **dropped, not fallen back from** — which is how a core inbox
122
122
  view asking for `title` on a collection whose field is `name` rendered every row nameless with no
123
123
  error. Check the descriptor's real field names against every column you write.
124
+ - **A list-valued option takes the comma spelling**: `set-view <id> options.columns=name,status`
125
+ writes a real list, as does the JSON form `'["name","status"]'`. The keys that split are
126
+ `columns`, `ref_fields` and `value_fields` — `options` has no schema, so a key not on that list
127
+ keeps its commas as characters (`options.template=a, b` is one string), and a list of OBJECTS
128
+ (`options.arrangement`) has only the JSON spelling.
124
129
  - ⚠ **`options.sort` must be written even when empty** (`sort: ''`), or "unsorted" cannot
125
- round-trip and silently reverts to a fallback ordering on the next load. The CLI cannot express
126
- it (an empty `set-view` value removes the key) hand-write it in the source.
130
+ round-trip and silently reverts to a fallback ordering on the next load. The spelling is a
131
+ QUOTED value `dt schema set-view <id> 'options.sort=""'`because a bare `options.sort=`
132
+ removes the key, as an empty value does everywhere else. Quoting any value makes it a literal
133
+ string (`options.sort='"-date"'` is the same as `options.sort=-date`).
127
134
 
128
135
  Views are live records: a changed `filter` or `options` reaches an open tab on the next compile;
129
136
  an unchanged view re-renders nothing.
@@ -152,7 +159,7 @@ kind with full CLI write support, because it goes through the same compile gate.
152
159
  `add-view` writes the **workspace module** — right for an operator's own daily surface; a view
153
160
  that is part of a module's canonical shape belongs in that module's `ui-views/`, hand-written.
154
161
  And a dotted `key=` with an **empty value removes the key**, so the one setting whose meaningful
155
- value IS empty — `sort: ''` — must be hand-written in the source file (see options).
162
+ value IS empty — `sort: ''` — is written QUOTED: `'options.sort=""'` (see options).
156
163
 
157
164
  ## common mistakes
158
165
 
@@ -164,7 +171,7 @@ value IS empty — `sort: ''` — must be hand-written in the source file (see o
164
171
  | a filter using `@me` | gone in 0.8.0 with `users` — compile refuses it by name |
165
172
  | `filter` written inside `options` | accepted, saved, read by nobody — compile warns; move it up |
166
173
  | a column the schema does not have | dropped silently — the row loses that value with no error |
167
- | omitting `sort` to mean unsorted | write `sort: ''` or the ordering reverts on reload |
174
+ | omitting `sort` to mean unsorted | write `sort: ''` (`'options.sort=""'`) or the ordering reverts on reload |
168
175
  | a ui-view that restates the built-in fallback | a record to maintain for zero gain |
169
176
  | a `default: true` view with its own `path`/`nav` | the default IS the collection page — it never routes itself |
170
177
  | a module ui-view naming one person | a hard-coded id resolves in no other workspace |
package/src/check.js CHANGED
@@ -8,7 +8,7 @@ import addFormats from 'ajv-formats';
8
8
  import { parseRecord, patternRe, fmtAjvError, unknownFields, walk, idFromRecordPath, MAX_RECORD_BYTES } from './records.js';
9
9
  import { NO_RUNTIME, loadDescriptors, runtimeDir, namespaces as compiledNamespaces } from './runtime.js';
10
10
  import { parseRef } from './namespace.js';
11
- import { refTargetsOf } from './ref.js';
11
+ import { refTargetsOf, refIsSoft } from './ref.js';
12
12
  import { relationsOf, expectedMirrors } from './relations.js';
13
13
 
14
14
  export function check({ root }) {
@@ -122,9 +122,9 @@ export function check({ root }) {
122
122
  for (const k of unknownFields(d.schema, fields)) {
123
123
  flag(file, `unknown field "${k}" (not in the ${name} schema)`);
124
124
  }
125
- for (const [fieldPath, target] of refFields) {
125
+ for (const [fieldPath, target, soft] of refFields) {
126
126
  for (const value of valuesAt(fields, fieldPath)) {
127
- checkRef(file, fieldPath, value, target, softTargets);
127
+ checkRef(file, fieldPath, value, target, softTargets, soft);
128
128
  }
129
129
  }
130
130
  parsed.get(name).set(id, fields);
@@ -168,7 +168,7 @@ export function check({ root }) {
168
168
  }
169
169
  }
170
170
 
171
- function checkRef(file, fieldPath, value, targets, softTargets) {
171
+ function checkRef(file, fieldPath, value, targets, softTargets, soft = false) {
172
172
  if (typeof value !== 'string') return;
173
173
  if (value.startsWith('@')) return; // runtime tokens (@me, @initiator) are legal
174
174
  // The SAME parser the store writes through (src/namespace.js) — `check` disagreeing with the
@@ -189,9 +189,14 @@ export function check({ root }) {
189
189
  softRefs.set(coll, (softRefs.get(coll) ?? 0) + 1);
190
190
  return;
191
191
  }
192
+ // A SOFT field (`x-reference-soft`) is a declaration, not a resolved link — see ref.js.
193
+ if (soft) return;
192
194
  return flag(file, `${fieldPath.join('.')}: reference "${value}" targets unknown collection "${coll}"`);
193
195
  }
194
- if (!index.get(coll).has(id)) return flag(file, `${fieldPath.join('.')}: dangling reference "${value}" — no such record`);
196
+ if (!index.get(coll).has(id)) {
197
+ if (soft) return; // resolve if present, ignore if absent
198
+ return flag(file, `${fieldPath.join('.')}: dangling reference "${value}" — no such record`);
199
+ }
195
200
  }
196
201
 
197
202
  // ---- report ----------------------------------------------------------------------
@@ -218,16 +223,17 @@ export function check({ root }) {
218
223
  }
219
224
 
220
225
 
221
- // collect [fieldPath, targets] for every x-reference in the schema, where `targets` is '*' or the
222
- // normalized array of declared collections (see refTargetsOf). Relations are NOT read here — they
223
- // are decoded once, from the compiled descriptors, by src/relations.js.
226
+ // collect [fieldPath, targets, soft] for every x-reference in the schema, where `targets` is '*' or
227
+ // the normalized array of declared collections (see refTargetsOf) and `soft` says whether an absent
228
+ // target is a finding (see refIsSoft). Relations are NOT read here — they are decoded once, from the
229
+ // compiled descriptors, by src/relations.js.
224
230
  function collectRefFields(schema, prefix = []) {
225
231
  const out = [];
226
232
  for (const [key, s] of Object.entries(schema.properties ?? {})) {
227
233
  if (!s || typeof s !== 'object') continue;
228
234
  const p = [...prefix, key];
229
235
  const targets = refTargetsOf(s);
230
- if (targets) out.push([p, targets]);
236
+ if (targets) out.push([p, targets, refIsSoft(s)]);
231
237
  if (s.properties) out.push(...collectRefFields(s, p));
232
238
  if (s.items?.properties) out.push(...collectRefFields(s.items, p));
233
239
  }
package/src/cli.js CHANGED
@@ -21,7 +21,7 @@ import { deriveEvents } from './events.js';
21
21
  import { commitPending } from './commit.js';
22
22
  import { Store } from './store.js';
23
23
  import { splitRef } from './ref.js';
24
- import { parseEnvValues, renderTemplate } from './env-vars.js';
24
+ import { envContext, renderTemplate } from './env-vars.js';
25
25
 
26
26
  // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
27
27
  // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
@@ -35,7 +35,8 @@ record verbs (hard validation — invalid writes are rejected before disk).
35
35
  A <target> is either a collection name or a <collection>/<id> reference; the reference splits at
36
36
  the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee is ONE argument:
37
37
  list <collection> [--filter k=v] [--where <json>] [--sort [-]<field>] [--json]
38
- (--filter is ONE condition — a repeat replaces it;
38
+ (--filter is ONE condition — repeat it to AND more
39
+ (--filter a=1 --filter b=2 wants both);
39
40
  anything compound goes in one --where, operator
40
41
  objects e.g. '{"starts":{"_gte":"2026-07-01"}}' —
41
42
  operators: _eq _neq _lt _lte _gt _gte _in _nin
@@ -47,8 +48,12 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
47
48
  add <collection> --<field> <value> … [--id <explicit-id>]
48
49
  (a codec-file collection takes --from <path>
49
50
  instead — the file IS the record, fields derive;
50
- --force replaces an existing file record)
51
- set <collection>/<id> <field>=<value>
51
+ --force replaces an existing file record.
52
+ A repeated --<field> is one ELEMENT of an array
53
+ field — refused on a scalar one; a single value
54
+ still splits on commas)
55
+ set <collection>/<id> <field>=<value> … (repeating a pair adds an element, exactly as a
56
+ repeated --<field> does)
52
57
  rm <collection>/<id> [--force]
53
58
  rename <collection>/<id> <new-id> (rewrites all inbound refs in one WRITE —
54
59
  commit publishes the set together)
@@ -109,7 +114,10 @@ different word in front of it):
109
114
  schema remove-field <collection> --name <field>
110
115
  schema add-view --path </route> --target list --collection collections/<c> --layout <id>
111
116
  [--id <id>] [k.v=…]
112
- schema set-view <id> <key>=<value> … (dotted keys: options.sort=-date, nav.label=Recent)
117
+ schema set-view <id> <key>=<value> … (dotted keys: options.sort=-date, nav.label=Recent.
118
+ A list option takes commas — options.columns=name,status — or JSON.
119
+ An empty value REMOVES the key; quote it to write the empty string
120
+ itself: 'options.sort=""' is the "unsorted" the surface needs.)
113
121
  schema rm-view <id>
114
122
 
115
123
  workspace verbs:
@@ -315,7 +323,9 @@ export function run(argv) {
315
323
  if (repos.length) {
316
324
  const here = repos.filter((r) => r.present).length;
317
325
  console.log(`repos: ${here}/${repos.length} materialized`);
318
- for (const r of repos) if (!r.present) console.log(` absent: ${r.id} ${r.path} (dreamteamer ensure ${r.id})`);
326
+ // an UNRESOLVED path is not the same absence as a repo simply not cloned yet, and
327
+ // `dreamteamer ensure` is not the fix for it — say which one this is.
328
+ for (const r of repos) if (!r.present) console.log(` absent: ${r.id} → ${r.path}${r.unresolved ? ` — ${r.unresolved}` : ` (dreamteamer ensure ${r.id})`}`);
319
329
  }
320
330
  } catch { /* no repos descriptor compiled — nothing to report */ }
321
331
  // Uncommitted records are invisible to `dt changes` (it diffs commits), so the
@@ -395,8 +405,11 @@ function dispatchRecordVerb(ws, verb, args) {
395
405
  // `commands for <c>/<id>` split its own target at the FIRST slash, which cannot name a
396
406
  // namespaced collection. splitRef can, so the id is handed over as `--ids` — the same
397
407
  // `commandsFor(store, collection, ids)` call, reached without re-encoding the reference.
398
- // Ours goes FIRST so an explicit `--ids` from the caller still wins (last flag parsed wins).
399
- return collectionCommand(ws, 'commands', 'for', [collection, '--ids', id, ...rest]);
408
+ // An explicit `--ids` from the caller still wins by NOT injecting ours, not by ordering. It
409
+ // used to rely on "last flag parsed wins", which stopped being true when a repeated flag started
410
+ // promoting to an array instead of overwriting: the pair would now be refused as a double.
411
+ const ours = rest.some((a) => a === '--ids' || a.startsWith('--ids=')) ? [] : ['--ids', id];
412
+ return collectionCommand(ws, 'commands', 'for', [collection, ...ours, ...rest]);
400
413
  }
401
414
 
402
415
  /**
@@ -419,10 +432,7 @@ function resolveVariables(ws, args) {
419
432
  // resolve has no flags, so a flag-shaped target is a mistake — and the one that costs is
420
433
  // `dt resolve --help`, which would otherwise print `--help` back and exit 0.
421
434
  if (target.startsWith('--')) throw new Error(`dt resolve takes a string or a <collection>/<id>, not a flag ("${target}") — see \`dreamteamer help\``);
422
- const declared = ws.pkg.dreamteamer?.vars ?? [];
423
- const envFile = path.join(ws.root, '.env');
424
- const env = parseEnvValues(fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf8') : '');
425
- const ctx = { env, workspaceFolder: ws.root, declared };
435
+ const ctx = envContext(ws);
426
436
 
427
437
  let ref = null;
428
438
  let store = null;