dreamteamer 0.6.3 → 0.7.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/README.md CHANGED
@@ -1,82 +1,154 @@
1
1
  # dreamteamer
2
2
 
3
- A workspace compiler for coding agents. Module sources compile **explicitly** into `.dreamteamer/`,
4
- the single runtime read surface, and from there into per-harness adapters — Claude Code, Codex, Pi,
5
- Gemini CLI, Cursor. Data is plain files: records are `<id>.<suffix>.<ext>`, ids are paths,
6
- references are `<collection>/<id>`. A write lands on disk; `dreamteamer commit` publishes it, one
7
- commit per repo. Hard validation before disk; nothing hidden.
3
+ **Structured, modular memory for coding agents.**
4
+
5
+ Your agent already has a memory. You just can't see it it's prose, in a black box somewhere,
6
+ untracked and unshared. And memory *is* context, which is the single biggest lever on what your agent
7
+ decides and how well it does it. So the most consequential thing in your setup is the one you have the
8
+ least access to.
9
+
10
+ dreamteamer makes it **files with a schema**: plain markdown in your git repo that your agent reads
11
+ natively, and that you can browse as tables, boards and forms.
8
12
 
9
13
  ```bash
10
- npm i dreamteamer # engine + the `dreamteamer` bin
14
+ npm i dreamteamer
11
15
  npx dreamteamer init # scaffold a workspace
12
16
  npx dreamteamer compile # sources → .dreamteamer (+ harness adapters)
13
- npx dreamteamer check # validate every record
17
+ npx dreamteamer check # prove every record and every link is intact
14
18
  npx dreamteamer help # the full command surface
15
19
  ```
16
20
 
17
- There is a VS Code extension — [dreamteamer-vscode](https://github.com/dreamteamer/dreamteamer-vscode) —
18
- which loads the engine **your workspace pins**, so the editor, the CLI and any agent session are
19
- provably running the same code.
21
+ Apache-2.0. No server, no account, no telemetry.
20
22
 
21
- ## Sources, and where they come from
23
+ ## Structured
22
24
 
23
- A module contributes collections, skills, agents, commands, command-bindings, UI views and
24
- collection templates. Modules are discovered over three channels, in precedence order: inline
25
- `modules/*`, then `git_modules/*`, then npm dependencies. Sources live **flat at a module root** —
26
- `modules/crm/skills/`, beside `package.json` — and an unknown folder at a module root is a compile
27
- error rather than a silent skip.
25
+ A record is a file. That's the whole trick.
28
26
 
29
- ## Attached repos (`repos`) vs modules (`git-modules`)
27
+ ```
28
+ data/meetings/2026/07/kickoff.meeting.md
29
+ ```
30
30
 
31
- Two different lifecycles, deliberately two different homes.
31
+ ```yaml
32
+ ---
33
+ title: Kickoff
34
+ date: 2026-07-14
35
+ attendees: [contacts/ada, contacts/lin]
36
+ project: projects/apollo
37
+ ---
38
+ Ada walked through the constraints. Lin owns the spec by Friday.
39
+ ```
32
40
 
33
- **Modules** are declared in the workspace's `package.json` under `dreamteamer.git-modules` and
34
- restored by `dreamteamer install`. They MUST live in config rather than in records because of a
35
- genuine bootstrap ordering: a fresh clone has no `.dreamteamer`, therefore no compiled schemas,
36
- therefore no readable records — so module clones have to be restorable before anything can be read.
41
+ Your agent opens that file the way it opens any file. Nothing is intercepted, nothing is proxied,
42
+ there is no API to learn.
37
43
 
38
- **Attached repos** are `repos` records under `data/repos/`. They contribute NOTHING to the
39
- workspace no schema, no skills, no UI. A repo record says only where a related git repo lives and
40
- how to get it, and exists so that domain collections can reference `repos/<id>` instead of each
41
- inventing its own url/ref/identity fields. Because they are not needed at compile time, they get to
42
- be data — which buys hard validation, the record CLI verbs, and history for free.
44
+ But `attendees` isn't a string — it's a link. `dreamteamer check` proves every one of them resolves,
45
+ and renaming `contacts/ada` updates everything pointing at it. A write with an unknown field, a wrong
46
+ type, or a reference to a record that doesn't exist is **rejected before it touches disk**.
43
47
 
44
- Working trees are materialized **on demand**:
48
+ **A schema is an agreement about what things are called.** Shared terminology with guardrails — not a
49
+ cage, because it stays negotiable. You change it by saying so:
45
50
 
46
- ```bash
47
- dreamteamer repos ensure <id> # clone if missing, print the path; idempotent
48
- dreamteamer repos ensure --all # explicit opt-in, e.g. before going offline
51
+ > *"From here on a client has a renewal date, and it's a date."*
52
+
53
+ That's a schema update and a data migration, and it's an **explicit, reviewable event** rather than
54
+ silent drift. Once it exists you get the column in a table, the field in a form, validation, sorting
55
+ and aggregation — all of it falling out of having said what the thing is.
56
+
57
+ The shape of a record is deliberately dull, because dull is what survives:
58
+
59
+ - records are `<id>.<suffix>.<ext>` files; **the id is the path** inside the collection folder
60
+ - references are `<collection>/<id>` — always qualified, greppable, never a bare name
61
+ - a collection may be scoped under a **declared namespace** — `health/doctors/dana-levi`, stored in
62
+ `data/health/doctors/`. The default namespace is the empty prefix, so `tasks/kickoff` is unchanged
63
+ - a write lands on disk; `dreamteamer commit` publishes it, one commit per repo
64
+ - schemas are JSON Schema in a YAML file, one per collection
65
+
66
+ ## Modular
67
+
68
+ **Data and skills are the new app structure.** A coding agent with the right skills over the right
69
+ data is arbitrary functionality — but composing that with no module system is where most setups stall.
70
+
71
+ So dreamteamer doesn't invent one. **It uses npm.**
72
+
73
+ `node_modules` is battle-tested, universally adopted, and already sitting in nearly every
74
+ coding-agent setup. A module contributes collections, skills, agents, commands, command-bindings and
75
+ UI views — and skills and agents are treated as exactly what they are: **memory that loads into
76
+ context**, living in the same module structure as everything else, in a standard your tooling already
77
+ understands.
78
+
79
+ Three channels, one shape:
80
+
81
+ ```
82
+ modules/<name>/ # lives in this repo
83
+ git_modules/<name>/ # lives in its own repo
84
+ node_modules/<name>/ # published package
49
85
  ```
50
86
 
51
- `install` deliberately does not do this. The record count only grows while the fraction any given
52
- session needs only shrinks, so eager restore would make every fresh clone slow, would require every
53
- identity's credentials to be present at install time, and would let one unreachable remote fail the
54
- whole install. Lazy materialization fails only the action you asked for, at the moment you asked.
87
+ Precedence runs top to bottom, so a local copy shadows a published one which is how you develop a
88
+ module and use it in the same workspace at the same time.
89
+
90
+ Sources live **flat at a module root** `modules/crm/skills/`, beside `package.json` and a folder
91
+ at a module root that isn't a known kind is a compile error rather than a silent skip.
92
+
93
+ ### Modules are not rigid
94
+
95
+ This is the part that differs from npm on purpose.
96
+
97
+ Installing a module into a workspace that already has opinions — its own idea of what a `contact` is —
98
+ is a **negotiation, not an overwrite**. Four workspaces wanted a CRM and all four wanted a different
99
+ `contacts`. A hard import would force one answer and make every divergence a fork.
100
+
101
+ Two same-name collections is a compile error that names both descriptors and tells you the move:
102
+ declare `extends: <module>/<collection>` and overlay only what differs. Because every schema is one
103
+ small YAML file, adapting is cheap — read it, change what doesn't fit, and the diff shows exactly what
104
+ you agreed to.
55
105
 
56
- Path resolution is `<repos-path>/<identity>/<name>`, where `repos-path` is a `package.json`
57
- `dreamteamer` key defaulting to `projects`, and `identity` is optional (omit it for
58
- `<repos-path>/<name>`). A record's `path` field overrides the derivation entirely.
106
+ So domain modules are **recipes you copy and adapt, not packages you install**, and divergence is the
107
+ normal case rather than a failure.
59
108
 
60
- **`identity` is an opaque path segment to the engine.** A workspace may use it to select a git
61
- identity — via `~/.gitconfig` `includeIf` rules keyed on the path, for example — but that resolution
62
- happens outside the engine, which only joins it into a path.
109
+ ## Every harness, one source
63
110
 
64
- **A missing working tree is not a violation.** `dreamteamer check` does not and must not flag it: a
65
- reference to `repos/<id>` resolves because the RECORD exists, and whether the clone is on disk is
66
- irrelevant to referential integrity. Presence is reported by `dreamteamer status`.
111
+ `compile` writes `.dreamteamer/` the single runtime read surface and from there into per-harness
112
+ adapters: Claude Code, Codex, Pi, Gemini CLI, Cursor. Author a skill once; every agent you run sees it.
67
113
 
68
- ## Domain modules are recipes, not packages
114
+ ## The editor
69
115
 
70
- Anything domain-shaped people, meetings, products, content — belongs in a module, and a generic
71
- version belongs in a recipe a workspace **copies and adapts**. They are deliberately not installable
72
- packages: four workspaces wanted a CRM and all four wanted a different `contacts`, so an import
73
- would force one answer and make every divergence a fork.
116
+ [dreamteamer-vscode](https://github.com/dreamteamer/dreamteamer-vscode) gives you tables, boards,
117
+ calendars, maps, forms and a data-model designer over the same files — and it loads **the engine your
118
+ workspace pins**, so the editor, the CLI and any agent session are provably running the same code.
119
+
120
+ ## Docs
121
+
122
+ This is an agent-native tool, so its documentation is shipped as skills the agent loads on demand —
123
+ and you can read them like any other file:
124
+
125
+ - [`skills/using-dreamteamer`](skills/using-dreamteamer) — the map: collections, conventions, the CLI,
126
+ how records work
127
+ - [`skills/building-dreamteamer`](skills/building-dreamteamer) — authoring: collections, skills,
128
+ agents, commands, UI views, and which of those a given request should become
129
+ - [`docs/repos-and-modules.md`](docs/repos-and-modules.md) — attached repos vs modules, and why they
130
+ have different homes
131
+ - [`docs/namespaces-blast-radius.md`](docs/namespaces-blast-radius.md) — scoping collections under a
132
+ namespace (`health/doctors`), what it costs consumers, and why the default namespace is transparent
133
+ - [`UPDATING.md`](UPDATING.md) — what to do when upgrading, one section per release
134
+
135
+ ## What it isn't
136
+
137
+ Not a database — records are files and git is the history. Not a cloud service — there is no server
138
+ and no account. Not a note-taking app — it's the layer underneath one.
139
+
140
+ And it is **not** for data that needs row-level access control, field-level encryption, or provable
141
+ erasure. Git cannot do those, and pretending otherwise is how people get hurt. This is for
142
+ human-scale structured knowledge: thousands of records, not millions.
74
143
 
75
144
  ## Contributing
76
145
 
77
146
  Issues are welcome. For anything larger than a typo, please open a discussion before a pull request —
78
- this is a small, deliberately lean codebase (`npm run metrics` enforces size budgets), and it's
79
- better to agree on the shape first.
147
+ this is a small, deliberately lean codebase (`npm run metrics` enforces size budgets), and it's better
148
+ to agree on the shape first.
149
+
150
+ `npm run verify` is the gate: import-layer direction, size budgets, and the test suite (tiers 1+2,
151
+ zero dependencies, a few seconds). See [CONTRIBUTING.md](CONTRIBUTING.md).
80
152
 
81
153
  ## License
82
154
 
@@ -69,9 +69,21 @@ schema:
69
69
  icon:
70
70
  type: string
71
71
  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.
72
+ owner:
73
+ type: string
74
+ x-reference: modules
75
+ description: >-
76
+ The module that OWNS this concept — DERIVED by compile from the base source, never authored.
77
+ An overlay adds fields to somebody else's collection and does not take it over, so `meetings`
78
+ stays owned by crm even though hq3 overlays it. This is the workspace's real partition, and
79
+ what the nav groups by.
72
80
  group:
73
81
  type: string
74
- description: Which nav folder this collection sits in. Ungrouped collections list at the top level; `system` is the plumbing group behind the gear.
82
+ description: >-
83
+ DEPRECATED as a nav axis since 2026-08-11 — the nav groups by `owner` (a module, which has a
84
+ title of its own) instead of by this string with a display-name map maintained in a surface.
85
+ Still read by nothing; kept so the change is a code revert rather than a data migration, and
86
+ because a workspace may yet want a partition that deliberately DIFFERS from its modules.
75
87
  order: 10
76
88
  list_fields: [name, last-modified]
77
89
  icon: schema
@@ -0,0 +1,81 @@
1
+ name: modules
2
+ # The modules this workspace compiled, PROJECTED by compile from what it discovered — the first
3
+ # derived collection in core, and the shape is deliberate enough to name.
4
+ #
5
+ # Every other runtime-stored collection (skills, commands, ui-views…) is STAGED from a module's
6
+ # source folder and read back by the engine. This one is neither: `package.json` stays the source of
7
+ # truth and compile keeps reading it, so a record here is a projection, never an input. Editing one
8
+ # would be editing a photograph.
9
+ #
10
+ # Why it earns a place anyway, against the three questions:
11
+ # 1. Does the ENGINE read it? It reads the DATA, which is the test that matters. `dependencies`
12
+ # names modules and is checked for cycles; `peerDependencies` names collections and is what
13
+ # lets a cross-module reference compile at all (compile.js — "cyclic module dependencies",
14
+ # "an overlay cannot compile without its base"). This is enforced structure, not annotation.
15
+ # 2. Recipe creeping into core? No. A module is the engine's own concept.
16
+ # 3. Could a module do it instead? No — nothing inside a module can enumerate the module set.
17
+ #
18
+ # What it buys is the thing a `group:` label cannot express: EDGES. A group is a partition; a
19
+ # dependency is a relation, and it has two kinds that mean different things. Once it is a
20
+ # collection, the browse, the diagram, `dt modules list` and the nav all work with no code written
21
+ # for any of them.
22
+ storage: { path: modules, codec: yaml, shape: file, suffix: module }
23
+ id:
24
+ generate: "{{ name | slug }}"
25
+ pattern: "^[a-z0-9-]+$"
26
+ title_template: "{{ name }}"
27
+ schema:
28
+ type: object
29
+ required: [name, channel]
30
+ properties:
31
+ name:
32
+ type: string
33
+ description: The module's package name, verbatim — `@dreamteamer/crm`, `hq3-workspace`.
34
+ title:
35
+ type: string
36
+ description: >-
37
+ What to CALL this module — authored as `dreamteamer.title` in its package.json, else derived
38
+ from the id. Authored because deriving cannot know an acronym: titleCase("crm") is "Crm".
39
+ A module names itself, which is what replaces a display-name map maintained in a surface.
40
+ channel:
41
+ type: string
42
+ enum: [path, git, npm, inline]
43
+ description: How this module reached the workspace. `inline` is the workspace's own sources.
44
+ path:
45
+ type: string
46
+ description: Workspace-relative root of the module's sources.
47
+ owns_data:
48
+ type: boolean
49
+ description: >-
50
+ The module keeps its records in its OWN clone rather than this workspace's data/ — so a
51
+ write there commits in that repo, and a rename spanning both is unavoidably two commits.
52
+ dependencies:
53
+ type: array
54
+ description: >-
55
+ Modules this one cannot compile without — an overlay needs its base. HARD and acyclic;
56
+ compile fails on a ring and names the peer escape hatch.
57
+ items:
58
+ type: string
59
+ x-reference: modules
60
+ peer_dependencies:
61
+ type: array
62
+ description: >-
63
+ Collections this module REFERENCES but does not own. Soft on purpose: naming a concept
64
+ rather than a module is what keeps two modules from forming a ring, and what lets a module
65
+ ship an unused reference without dragging in a whole CRM.
66
+ items:
67
+ type: string
68
+ x-reference: collections
69
+ collections:
70
+ type: array
71
+ description: >-
72
+ Collections this module contributed a source for. A collection merged from several modules
73
+ appears under EVERY one of them — which is the honest answer the flat "which module owns
74
+ this" provenance could not give (it took the first source and dropped the overlay).
75
+ items:
76
+ type: string
77
+ x-reference: collections
78
+ order: 140
79
+ list_fields: [title, name, channel, collections]
80
+ icon: deployed_code
81
+ group: system
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.6.3",
4
- "description": "A workspace compiler for coding agents schema-validated records as plain files over git, compiled into every harness",
3
+ "version": "0.7.0",
4
+ "description": "A workspace compiler for coding agents \u2014 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>",
7
7
  "homepage": "https://github.com/dreamteamer/dreamteamer#readme",
@@ -49,10 +49,16 @@
49
49
  "express": "^5.2.1",
50
50
  "js-yaml": "^4.1.0"
51
51
  },
52
- "dreamteamer": {},
52
+ "dreamteamer": {
53
+ "title": "System"
54
+ },
53
55
  "scripts": {
56
+ "test": "node scripts/test.mjs",
57
+ "test:unit": "node scripts/test.mjs --unit",
58
+ "test:watch": "node --watch scripts/test.mjs --unit",
54
59
  "metrics": "node scripts/metrics.mjs",
55
60
  "metrics:check": "node scripts/metrics.mjs --check",
56
- "layers": "node scripts/layers.mjs"
61
+ "layers": "node scripts/layers.mjs",
62
+ "verify": "node scripts/layers.mjs && node scripts/metrics.mjs --check && node scripts/test.mjs"
57
63
  }
58
64
  }
@@ -8,6 +8,7 @@ One descriptor file: `modules/<module>/collections/<name>.collection.yaml`. The
8
8
  | goal | how |
9
9
  |---|---|
10
10
  | new collection from a template | `dt collections add --name research-docs --template docs` |
11
+ | move one into a namespace | `dt collections rename doctors health/doctors` (or `doctors --namespace health`) |
11
12
  | templateless | `dt collections add --name <n>` — emits a minimal compilable schema |
12
13
  | add a field | `dt <collection> add-field --name urgent --type boolean --default-value false` |
13
14
  | change / drop a field | `dt <collection> update-field …` · `remove-field --name <f>` |
@@ -20,8 +21,37 @@ or a bare collection name for a reference into it. `--required true` widens `req
20
21
 
21
22
  ⚠ **The meta verbs write the WORKSPACE module only.** To change a field on a collection another
22
23
  module owns, either edit that module's descriptor by hand or add an `extends:` overlay.
23
- **There is no `collections rename`** it refuses system sources. A rename is `git mv` of the
24
- descriptor + edit `name`/`storage.path`/`suffix` + re-suffix every record, all in one commit.
24
+ **`dt collections rename <old> <new>`** moves the descriptor, the records, the record filenames and
25
+ every inbound reference in ONE commit including `x-reference` targets in other descriptors and any
26
+ ui-view pointing at it. `<old> --namespace <ns>` is sugar for moving it into a namespace under the same
27
+ bare name. It refuses a compiled source, a module-owned collection, a taken name, and an undeclared
28
+ target namespace; a refusal leaves nothing half-moved. Two things it deliberately does NOT overrule,
29
+ because both are authored choices: a `storage.path` you set by hand (the records stay put, and it says
30
+ so) and a `storage.suffix` that is not the singular of the old name.
31
+
32
+ ## namespaces — scoping a collection under a folder
33
+
34
+ A collection name may carry a slash-delimited namespace, and it becomes real directory nesting:
35
+
36
+ | declare in the workspace `package.json` | create it | lands in | referenced as |
37
+ |---|---|---|---|
38
+ | `"namespaces": ["health"]` | `dt collections add --namespace health --name doctors` | `data/health/doctors/` | `health/doctors/dana-levi` |
39
+
40
+ - **The default namespace is the empty prefix.** `tasks` stays `data/tasks/` and `tasks/kickoff`, so
41
+ common entities need no prefix and adopting namespaces migrates nothing. `default` is RESERVED —
42
+ there is never a second spelling for one collection.
43
+ - ⚠ **The namespace MUST be declared before the collection compiles.** An id is also a slash path
44
+ (`meetings/2026/07/kickoff`), so `a/b/c` is ambiguous without the declared set; an undeclared prefix
45
+ is a compile error rather than a reference that silently names a different collection.
46
+ - `--namespace health --name doctors` and `--name health/doctors` are the same thing. The descriptor
47
+ lands at `collections/health/doctors.collection.yaml`, mirroring the runtime; the `suffix` comes off
48
+ the bare name (`<id>.doctor.md`).
49
+ - `x-reference: health/doctors`, `disable: "<module>/health/doctors"` and every record verb all take the
50
+ QUALIFIED name — it is the collection's identity everywhere.
51
+ - Nested namespaces work (`work/clients`), longest declared prefix wins.
52
+ - ⚠ **No collection may store records inside another's folder** — a namespace folder cannot itself be a
53
+ collection root. compile refuses it, because the outer collection would index the inner one's records
54
+ as its own.
25
55
 
26
56
  ## `templates:` — a live shared field set
27
57
 
@@ -74,6 +74,23 @@ due: '2026-07-28'
74
74
  Users report the login button does nothing on mobile.
75
75
  ```
76
76
 
77
+ ## namespaced collections
78
+
79
+ A collection may be scoped under a namespace declared in the workspace `package.json`
80
+ (`dreamteamer.namespaces`). Everything about working with its records is unchanged except that the
81
+ QUALIFIED name is the collection's name everywhere:
82
+
83
+ ```bash
84
+ dt health/doctors add --name "Dana Levi" # → data/health/doctors/dana-levi.doctor.md
85
+ dt health/visits add --name Checkup --date 2026-03-04 --doctor health/doctors/dana-levi
86
+ ```
87
+
88
+ - a reference is still `<collection>/<id>` — `health/doctors/dana-levi` is the collection
89
+ `health/doctors` and the id `dana-levi`.
90
+ - the **default namespace has no prefix**: `tasks/kickoff` in `data/tasks/`, exactly as always.
91
+ - ⚠ a namespace only exists if it is DECLARED. Without the declaration the same string reads as the
92
+ collection `health` with a nested id, so it dangles — `dt check` says so.
93
+
77
94
  ## the hard rules
78
95
 
79
96
  **never hand-rename or `mv` a record file** — the id IS the path, so a rename silently dangles
package/src/check.js CHANGED
@@ -6,7 +6,8 @@ import path from 'node:path';
6
6
  import Ajv from 'ajv';
7
7
  import addFormats from 'ajv-formats';
8
8
  import { parseRecord, patternRe, fmtAjvError, unknownFields, walk, EXT } from './records.js';
9
- import { NO_RUNTIME, loadDescriptors, runtimeDir } from './runtime.js';
9
+ import { NO_RUNTIME, loadDescriptors, runtimeDir, namespaces as compiledNamespaces } from './runtime.js';
10
+ import { parseRef } from './namespace.js';
10
11
 
11
12
  export function check({ root }) {
12
13
  const RUNTIME = runtimeDir(root);
@@ -25,6 +26,9 @@ export function check({ root }) {
25
26
  console.error(`✖ ${NO_RUNTIME}`);
26
27
  return 2;
27
28
  }
29
+ // Off the manifest, like the descriptors themselves — `check` is in the record layer and must not
30
+ // learn what a workspace package.json is (see the split in CLAUDE.md).
31
+ const namespaces = compiledNamespaces(root);
28
32
 
29
33
  // ---- index all records: collection -> Map<id, filePath> ------------------------
30
34
  const index = new Map();
@@ -74,10 +78,12 @@ export function check({ root }) {
74
78
  // parsed fields, kept for the symmetric-ref pass below (parse each record exactly once)
75
79
  const parsed = new Map();
76
80
  const inverseRules = []; // [collection, fieldPath, targetCollection, inverseField]
81
+ const softRefs = new Map(); // absent-but-declared peer collection -> how many refs point at it
77
82
 
78
83
  for (const [name, d] of descriptors) {
79
84
  const validate = ajv.compile(d.schema);
80
85
  const refFields = collectRefFields(d.schema);
86
+ const softTargets = d.unresolved_peers ? new Set(d.unresolved_peers) : null;
81
87
  const bodyField = Object.entries(d.schema.properties ?? {}).find(([, s]) => s?.['x-body'])?.[0];
82
88
  parsed.set(name, new Map());
83
89
  for (const [fieldPath, target, inverse] of refFields) {
@@ -103,7 +109,7 @@ export function check({ root }) {
103
109
  }
104
110
  for (const [fieldPath, target] of refFields) {
105
111
  for (const value of valuesAt(fields, fieldPath)) {
106
- checkRef(file, fieldPath, value, target);
112
+ checkRef(file, fieldPath, value, target, softTargets);
107
113
  }
108
114
  }
109
115
  parsed.get(name).set(id, fields);
@@ -120,7 +126,8 @@ export function check({ root }) {
120
126
  const self = `${name}/${id}`;
121
127
  for (const value of valuesAt(fields, fieldPath)) {
122
128
  if (typeof value !== 'string' || value.startsWith('@')) continue;
123
- const targetId = value.slice(value.indexOf('/') + 1);
129
+ const targetId = parseRef(value, namespaces)?.id;
130
+ if (targetId === undefined) continue; // already flagged as malformed
124
131
  const targetFields = parsed.get(target)?.get(targetId);
125
132
  if (!targetFields) continue; // already flagged as dangling
126
133
  const back = [...valuesAt(targetFields, [inverse])];
@@ -132,17 +139,28 @@ export function check({ root }) {
132
139
  }
133
140
  }
134
141
 
135
- function checkRef(file, fieldPath, value, target) {
142
+ function checkRef(file, fieldPath, value, target, softTargets) {
136
143
  if (typeof value !== 'string') return;
137
144
  if (value.startsWith('@')) return; // runtime tokens (@me, @initiator) are legal
138
- const slash = value.indexOf('/');
139
- if (slash < 1) return flag(file, `${fieldPath.join('.')}: reference "${value}" is not <collection>/<id>`);
140
- const coll = value.slice(0, slash);
141
- const id = value.slice(slash + 1);
145
+ // The SAME parser the store writes through (src/namespace.js) — `check` disagreeing with the
146
+ // write path about where a namespace ends would flag valid records and pass invalid ones.
147
+ const ref = parseRef(value, namespaces);
148
+ if (!ref) return flag(file, `${fieldPath.join('.')}: reference "${value}" is not <collection>/<id>`);
149
+ const { collection: coll, id } = ref;
142
150
  if (target !== '*' && coll !== target) {
143
151
  return flag(file, `${fieldPath.join('.')}: reference "${value}" should target collection "${target}"`);
144
152
  }
145
- if (!descriptors.has(coll)) return flag(file, `${fieldPath.join('.')}: reference "${value}" targets unknown collection "${coll}"`);
153
+ if (!descriptors.has(coll)) {
154
+ // A collection the owning module DECLARED as a peer and nothing installed provides is the
155
+ // normal state of a module opened on its own — the reference is unresolvable, not wrong.
156
+ // `unresolved_peers` is stamped by compile so this layer never has to know what a module
157
+ // is (see the record/workspace split in CLAUDE.md).
158
+ if (softTargets?.has(coll)) {
159
+ softRefs.set(coll, (softRefs.get(coll) ?? 0) + 1);
160
+ return;
161
+ }
162
+ return flag(file, `${fieldPath.join('.')}: reference "${value}" targets unknown collection "${coll}"`);
163
+ }
146
164
  if (!index.get(coll).has(id)) return flag(file, `${fieldPath.join('.')}: dangling reference "${value}" — no such record`);
147
165
  }
148
166
 
@@ -150,6 +168,11 @@ export function check({ root }) {
150
168
  for (const s of strays) {
151
169
  console.log(`⚠ ${s.file} — unrecognized file in ${s.collection} folder${s.note ? ` (${s.note})` : ''}`);
152
170
  }
171
+ // Warned, never silent: the references are real and currently resolve to nothing. This is the
172
+ // expected reading when a module is opened without the workspace that provides the concept.
173
+ for (const [coll, n] of [...softRefs].sort()) {
174
+ console.log(`⚠ peer collection "${coll}" is declared but not installed — ${n} reference${n === 1 ? '' : 's'} unresolvable`);
175
+ }
153
176
  if (violations.length === 0) {
154
177
  console.log(`✔ 0 violations (${[...index.values()].reduce((n, m) => n + m.size, 0)} records across ${descriptors.size} collections)`);
155
178
  return 0;
package/src/cli.js CHANGED
@@ -55,8 +55,14 @@ repo attachment (working trees are materialized ON DEMAND, never at install):
55
55
  repos ensure --all [--json] (explicit opt-in: everything, e.g. before going offline)
56
56
 
57
57
  meta verbs (schema operations — write SOURCES through a compile gate, never the runtime):
58
- collections add --name <name> [--template docs|entity]
58
+ collections add --name <name> [--namespace <ns>] [--template docs|entity]
59
+ (--namespace health --name doctors === --name health/doctors; the
60
+ namespace must already be declared in dreamteamer.namespaces, and
61
+ records land in data/<ns>/<name>/)
59
62
  collections rm <name> [--force] (--force required if it still has records)
63
+ collections rename <old> <new> (or <old> --namespace <ns> to move it into one)
64
+ moves the descriptor AND the records, re-suffixes files when the
65
+ suffix was derived, rewrites every inbound reference, ONE commit
60
66
  <collection> add-field --name <field> --type <type> [--options a,b] [--default-value v] [--required true]
61
67
  [--description "what this field means"]
62
68
  types: string text markdown boolean number integer date datetime
@@ -8,7 +8,7 @@ import { Store, bodyField } from './store.js';
8
8
  import { load, dump } from './yaml.js';
9
9
  import { slug } from './template.js';
10
10
  import {
11
- createCollection, removeCollection, addField, updateField, removeField, fieldDef, saveUiView, removeUiView,
11
+ createCollection, removeCollection, renameCollection, addField, updateField, removeField, fieldDef, saveUiView, removeUiView,
12
12
  // was copy-pasted here, and the copy went stale the moment the source layout gained a second
13
13
  // spelling — one implementation, two callers
14
14
  workspaceSystemDir,
@@ -17,6 +17,7 @@ import { history, historyDiff } from './history.js';
17
17
  import { commandsFor, recordResolver } from './record-commands.js';
18
18
  import { distinctValues } from './field-values.js';
19
19
  import { matchesFilter } from './filter.js';
20
+ import { baseNameOf, normalizeNamespaces } from './namespace.js';
20
21
  import { sortRows } from './temporal.js';
21
22
  import { ensureRepo, ensureAllRepos } from './init.js';
22
23
 
@@ -69,6 +70,7 @@ export function collectionCommand(ws, collection, verb, args) {
69
70
  // ordinary record path refuses them ("… are system sources") and always would.
70
71
  if (collection === 'collections' && verb === 'add') return metaCollectionsAdd(ws, store, flags);
71
72
  if (collection === 'collections' && verb === 'rm') return metaCollectionsRm(ws, store, flags, pos);
73
+ if (collection === 'collections' && verb === 'rename') return metaCollectionsRename(ws, store, flags, pos);
72
74
  if (collection === 'commands' && verb === 'for') return metaCommandsFor(ws, store, flags, pos);
73
75
  if (collection === 'ui-views' && ['add', 'set', 'rm'].includes(verb)) return metaUiView(ws, store, verb, flags, pos);
74
76
  if (collection === 'repos' && verb === 'ensure') return metaReposEnsure(ws, flags, pos);
@@ -228,12 +230,36 @@ function metaReposEnsure(ws, flags, pos) {
228
230
 
229
231
  // `dreamteamer collections add --name research-docs --template docs`
230
232
  function metaCollectionsAdd(ws, store, flags) {
231
- const { file } = createCollection(ws, store, { name: flags.name, template: flags.template });
233
+ const { file } = createCollection(ws, store, { name: flags.name, template: flags.template, namespace: flags.namespace });
232
234
  console.log(`✔ ${rel(ws.root, file)}`);
233
235
  console.log('✔ compiled — the collection is live (schema ops prove sources with a real compile)');
234
236
  return 0;
235
237
  }
236
238
 
239
+ // `dreamteamer collections rename doctors health/doctors`, or `… doctors --namespace health`.
240
+ // The whole point is that namespacing EXISTING data is one command instead of a six-step hand
241
+ // migration whose last step (rewriting references) dangles everything when forgotten.
242
+ function metaCollectionsRename(ws, store, flags, pos) {
243
+ const [oldName, explicitNew] = pos;
244
+ if (!oldName) throw new Error('usage: collections rename <old-name> <new-name> | <old-name> --namespace <ns>');
245
+ // `--namespace health` on its own moves the collection INTO that namespace keeping its bare name,
246
+ // which is the common case and saves retyping it.
247
+ const newName = explicitNew
248
+ ?? (flags.namespace ? `${String(flags.namespace).replace(/^\/+|\/+$/g, '')}/${baseNameOf(oldName, normalizeNamespaces(ws.pkg.dreamteamer?.namespaces))}` : null);
249
+ if (!newName) throw new Error('missing new name — give it positionally or with --namespace <ns>');
250
+
251
+ const out = renameCollection(ws, store, oldName, newName);
252
+ if (flags.json) { emit(JSON.stringify(out)); return 0; }
253
+ if (!out.renamed) { console.log(`✔ ${oldName} — already named that, nothing to do`); return 0; }
254
+ console.log(`✔ ${oldName} → ${out.name}`);
255
+ if (out.from !== out.to) console.log(` records ${out.from} → ${out.to} (${out.records})`);
256
+ if (out.suffix) console.log(` suffix .${out.suffix.from}.md → .${out.suffix.to}.md`);
257
+ if (out.pathKept) console.log(` ⚠ storage.path kept as "${out.pathKept}" — it was authored, so the rename did not overrule it`);
258
+ console.log(` refs ${out.rewrites} rewritten`);
259
+ console.log('✔ compiled — the rename is live, in ONE commit');
260
+ return 0;
261
+ }
262
+
237
263
  // `dreamteamer collections rm widgets [--force]` — --force is required to drop a collection
238
264
  // that still has records (removeCollection refuses otherwise, and says so).
239
265
  function metaCollectionsRm(ws, store, flags, pos) {