dreamteamer 0.12.0 → 0.12.1

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/README.md CHANGED
@@ -73,8 +73,8 @@ source_file: ${env:FILES_FOLDER}/2026/q3.pdf
73
73
  ```
74
74
 
75
75
  Three variables, borrowing VS Code's grammar: `${env:NAME}` — declared in `dreamteamer.vars` in
76
- `package.json`, valued in the gitignored `.env` plus `${workspaceFolder}` and `${userHome}`.
77
- One verb renders them:
76
+ `package.json`, valued in the gitignored `.env` (an empty or whitespace-only value counts as no
77
+ value at all) — plus `${workspaceFolder}` and `${userHome}`. One verb renders them:
78
78
 
79
79
  ```bash
80
80
  npx dreamteamer resolve '${env:FILES_FOLDER}/x' # → /Volumes/annex/x
@@ -93,7 +93,7 @@ schema:
93
93
  description: >-
94
94
  The module that OWNS this concept — DERIVED by compile from the base source, never authored.
95
95
  An overlay adds fields to somebody else's collection and does not take it over, so `meetings`
96
- stays owned by crm even though hq3 overlays it. This is the workspace's real partition, and
96
+ stays owned by crm even though the workspace module overlays it. This is the workspace's real partition, and
97
97
  what the nav groups by.
98
98
  group:
99
99
  type: string
@@ -30,7 +30,7 @@ schema:
30
30
  properties:
31
31
  name:
32
32
  type: string
33
- description: The module's package name, verbatim — `@dreamteamer/crm`, `hq3-workspace`.
33
+ description: The module's package name, verbatim — `@dreamteamer/crm`, `acme-workspace`.
34
34
  title:
35
35
  type: string
36
36
  description: >-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
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>",
@@ -57,6 +57,7 @@
57
57
  "test": "node scripts/test.mjs",
58
58
  "test:unit": "node scripts/test.mjs --unit",
59
59
  "test:watch": "node --watch scripts/test.mjs --unit",
60
+ "perf": "node test/perf/run.mjs",
60
61
  "metrics": "node scripts/metrics.mjs",
61
62
  "metrics:check": "node scripts/metrics.mjs --check",
62
63
  "layers": "node scripts/layers.mjs",
@@ -100,7 +100,7 @@ source_file: ${env:FILES_FOLDER}/2026/q3.pdf
100
100
 
101
101
  | variable | renders to |
102
102
  |---|---|
103
- | `${env:NAME}` | `NAME`'s value in the workspace's `.env` — and only if `NAME` is listed in `dreamteamer.vars` in `package.json` |
103
+ | `${env:NAME}` | `NAME`'s value in the workspace's `.env` — and only if `NAME` is listed in `dreamteamer.vars` in `package.json` AND has a non-empty value there (an empty or whitespace-only value fails exactly like an unset key) |
104
104
  | `${workspaceFolder}` | the workspace root, absolute |
105
105
  | `${userHome}` | the current user's home directory |
106
106
 
package/src/compile.js CHANGED
@@ -49,7 +49,7 @@ export function titleCase(id) {
49
49
  * Every `x-display` left in a schema, as `[fieldPath, template, referenceTarget|null]`.
50
50
  *
51
51
  * The keyword was renamed to `x-title-template` and mostly DELETED — its value is inherited from
52
- * the target collection's `title_template`. There is deliberately no alias: recipes and dt-hq pin
52
+ * the target collection's `title_template`. There is deliberately no alias: real workspaces pin
53
53
  * this engine by SHA, so nothing breaks until someone bumps a pin, and that person needs a message
54
54
  * rather than silence. JSON Schema IGNORES unknown keywords, so the alternative to failing here is
55
55
  * a label that quietly stops working and regresses to a raw id.
@@ -318,7 +318,11 @@ export function compile({ root, pkg }) {
318
318
  // pairing there is: compile says nothing and then `dt resolve` answers "no value in
319
319
  // .env" about a line the operator is looking straight at. Values are read here and
320
320
  // never printed — the warnings below name keys only.
321
- const present = new Set(parseEnvValues(fs.readFileSync(envPath, 'utf8')).keys());
321
+ // A key present with an EMPTY (or whitespace-only) value is treated as absent, same as
322
+ // resolve's renderTemplate — `FILES_FOLDER=` must warn here exactly as `FILES_FOLDER`
323
+ // missing entirely would, or compile says nothing and resolve fails on the same line.
324
+ const parsedEnv = parseEnvValues(fs.readFileSync(envPath, 'utf8'));
325
+ const present = new Set([...parsedEnv].filter(([, v]) => v.trim() !== '').map(([k]) => k));
322
326
  for (const [k, mods] of declaredEnv) {
323
327
  if (present.has(k)) continue;
324
328
  for (const mod of mods) console.warn(`⚠ module ${mod} declares env key ${k} — missing from .env (see .env.example)`);
@@ -662,7 +666,8 @@ export function compile({ root, pkg }) {
662
666
  // judged against the BASE module's declarations, which it never wrote.
663
667
  const groupModules = [...new Set(group.map((g) => g.moduleName))];
664
668
  // WHO OWNS the concept — the module whose source is the base, not the ones overlaying it.
665
- // An overlay adds fields to somebody else's collection (hq3 adds `tags` to crm's contacts);
669
+ // An overlay adds fields to somebody else's collection (a workspace module adding its own
670
+ // `tags` to the `crm` module's `contacts`);
666
671
  // it does not take the concept over. Measured 2026-08-11: letting the overlay win moves
667
672
  // `contacts` and `meetings` out of CRM, and a CRM without contacts reads as broken.
668
673
  //
@@ -672,7 +677,8 @@ export function compile({ root, pkg }) {
672
677
  // IS the answer, and it exists to REPLACE `group:` as the workspace's partition rather than
673
678
  // to sit beside it.
674
679
  merged.owner = `modules/${moduleId(base?.moduleName ?? groupModules[0])}`;
675
- // EVERY contributing module, not just the base — a collection merged from crm + hq3 belongs
680
+ // EVERY contributing module, not just the base — a collection merged from `crm` and the
681
+ // workspace module that overlays it belongs
676
682
  // to both, and saying otherwise is what made a flat "which module owns this" field wrong.
677
683
  for (const m of groupModules) {
678
684
  if (!moduleColls.has(m)) moduleColls.set(m, new Set());
@@ -881,7 +887,8 @@ export function compile({ root, pkg }) {
881
887
  // in this loop and never got cleared — a module that was RENAMED or REMOVED left its old record
882
888
  // behind forever, listing collections that no longer exist. `check` reads those records like any
883
889
  // other, so it surfaced as a dangling reference in a file nobody had touched, twice in one day
884
- // (`hq3-workspace` after the workspace-module rename, and again after `crm` was folded in). The
890
+ // (the workspace module's own record after it was renamed, and a domain module's after it was
891
+ // folded into another). The
885
892
  // runtime is build output; stale build output is the compiler's problem, not the reader's.
886
893
  for (const kind of [...KINDS, ...DERIVED_KINDS]) fs.rmSync(path.join(RUNTIME, kind), { recursive: true, force: true });
887
894
  fs.rmSync(path.join(RUNTIME, 'system'), { recursive: true, force: true });
package/src/env-vars.js CHANGED
@@ -34,7 +34,11 @@ export function renderTemplate(str, { env, workspaceFolder, declared }) {
34
34
  if (ns !== 'env') throw new Error(`\${${ns}:${arg}} is not a dreamteamer variable — ${SUPPORTED}`);
35
35
  if (!arg) throw new Error(`\${env:} needs a key name — ${SUPPORTED}`);
36
36
  if (!declared.includes(arg)) throw new Error(`\${env:${arg}}: "${arg}" is not declared in dreamteamer.vars (workspace package.json) — declared: ${declared.join(', ') || '(none)'}`);
37
- if (!env.has(arg)) throw new Error(`\${env:${arg}} is declared but has no value in .env on this machine`);
37
+ // An empty or whitespace-only value is indistinguishable from unset to anyone reading the
38
+ // rendered output — `FILES_FOLDER=` passes `env.has()` and silently renders to '', producing
39
+ // a plausible-looking but wrong path. Same failure, same message: the operator can't tell
40
+ // the two states apart from outside and doesn't care which one it is.
41
+ if (!env.has(arg) || env.get(arg).trim() === '') throw new Error(`\${env:${arg}} is declared but has no value in .env on this machine`);
38
42
  return env.get(arg);
39
43
  });
40
44
  }
package/src/init.js CHANGED
@@ -75,10 +75,10 @@ export function init({ flags = {} } = {}) {
75
75
  harnesses,
76
76
  'gitignore-runtime-folder': true,
77
77
  // The workspace's own sources live in `modules/default/`, and the folder is named for its ROLE,
78
- // not for the vault. It used to be named after the workspace, and that name went stale twice in
79
- // one repo (`hq3` → `gk`, decision 213 reversed by 224) — each rename rewriting every path that
80
- // RESOLVES while the historical documents deliberately kept the old spelling, so a stale-looking
81
- // `modules/hq3` was correct in prose and a bug in a path. A role name cannot go stale.
78
+ // not for the vault. It used to be named after the workspace, and that name went stale TWICE in
79
+ // one repo (decision 213, reversed by 224) — each rename rewriting every path that RESOLVES
80
+ // while the historical documents deliberately kept the old spelling, so the old `modules/<vault>`
81
+ // was correct in prose and a bug in a path. A role name cannot go stale.
82
82
  //
83
83
  // `default` is deliberately the same word `RESERVED_NAMESPACES` holds (namespace.js): this module
84
84
  // owns the DEFAULT-namespace collections, and the default namespace is the empty prefix. The one
package/src/schema-ops.js CHANGED
@@ -219,14 +219,23 @@ export function removeCollection(ws, store, name, { force = false } = {}) {
219
219
  * relearn both, and would corrupt `data/tasks/` in a path or a URL on its first outing. N passes over
220
220
  * the record files is the price, and at human scale it is worth paying for reusing the correct code.
221
221
  *
222
- * ⚠ MEASURED 2026-08-17, so the cost is a number rather than a hope: a 2,291-record collection in a
223
- * 3,391-file workspace — gk-brain's `finance-transactions` — takes **3 minutes**, of which 142s is
224
- * system time. That is 7.7M file reads to rewrite ZERO references, because the pass runs per id
225
- * whether or not anything points at the collection. Tolerable for a one-time migration and left
226
- * alone on that basis; it is O(records x files), so a workspace 3x larger pays 27 minutes. The fix
227
- * when it is needed is a batch entry point on the store that reads each file ONCE and loops the ref
228
- * set in memory, with `text.includes(oldName + '/')` as a cheap NEGATIVE filter only — never as the
229
- * matcher, for the reason above.
222
+ * ⚠ MEASURED 2026-08-17, so the cost is a number rather than a hope: a real 2,291-record collection
223
+ * in a 3,391-file workspace takes **3 minutes**, of which 142s is system time — 7.7M file reads to
224
+ * rewrite ZERO references, because the pass runs per id whether or not anything points at the
225
+ * collection. Tolerable for a one-time migration and left alone on that basis; it is
226
+ * O(records x files), so a workspace 3x larger pays 27 minutes.
227
+ *
228
+ * REPRODUCED 2026-08-22 by `npm run perf -- --records=2291 --filler=1100`, which generates a
229
+ * workspace that shape — 271s wall, 203s of it system, and **15.6M reads, not 7.7M**. The original
230
+ * figure counted ONE pass per id; there are TWO, because `captureRefs` walks every record file for
231
+ * the rollback snapshot before `rewriteRefs` walks them all again. `7.7M` is the per-pass number.
232
+ * That is what a generated fixture is for: the finding was right about the shape and off by 2x on
233
+ * the count, and no comment could have told you.
234
+ *
235
+ * The fix when it is needed is a batch entry point on the store that reads each file ONCE and loops
236
+ * the ref set in memory, with `text.includes(oldName + '/')` as a cheap NEGATIVE filter only — never
237
+ * as the matcher, for the reason above. Halving it is cheaper still: the snapshot pass and the
238
+ * rewrite pass read the same bytes.
230
239
  */
231
240
  export function renameCollection(ws, store, oldName, newName) {
232
241
  const d = store.descriptor(oldName); // throws with the known-collection list if absent