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 +123 -51
- package/collections/collections.collection.yaml +13 -1
- package/collections/modules.collection.yaml +81 -0
- package/package.json +10 -4
- package/skills/building-dreamteamer/references/collections.md +32 -2
- package/skills/using-dreamteamer/references/records.md +17 -0
- package/src/check.js +32 -9
- package/src/cli.js +7 -1
- package/src/collections-cli.js +28 -2
- package/src/compile.js +264 -5
- package/src/harnesses.js +29 -8
- package/src/namespace.js +181 -0
- package/src/presentation.js +12 -1
- package/src/runtime.js +50 -3
- package/src/schema-ops.js +260 -11
- package/src/server.js +9 -0
- package/src/store.js +63 -21
package/README.md
CHANGED
|
@@ -1,82 +1,154 @@
|
|
|
1
1
|
# dreamteamer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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 #
|
|
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
|
-
|
|
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
|
-
##
|
|
23
|
+
## Structured
|
|
22
24
|
|
|
23
|
-
A
|
|
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
|
-
|
|
27
|
+
```
|
|
28
|
+
data/meetings/2026/07/kickoff.meeting.md
|
|
29
|
+
```
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
##
|
|
114
|
+
## The editor
|
|
69
115
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
4
|
-
"description": "A workspace compiler for coding agents
|
|
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
|
-
|
|
24
|
-
|
|
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 =
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
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))
|
|
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
|
package/src/collections-cli.js
CHANGED
|
@@ -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) {
|