dreamteamer 0.13.2 → 0.13.4
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/package.json +1 -1
- package/src/record-commands.js +14 -4
- package/src/schema-ops.js +76 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamteamer",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
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>",
|
package/src/record-commands.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// ref traversal (tier 1). serves `dreamteamer commands for`, GET /api/commands/:name,
|
|
6
6
|
// and (through the extension's api.ts port) the studio's Commands tab.
|
|
7
7
|
import { matchesFilter } from './filter.js';
|
|
8
|
+
import { parseRef } from './namespace.js';
|
|
8
9
|
|
|
9
10
|
// memoized `<collection>/<id>` → parsed fields (or null) for ONE evaluation pass:
|
|
10
11
|
// overlapping refs across a 50-record selection parse once, and filter.js stays
|
|
@@ -14,11 +15,20 @@ export function recordResolver(store) {
|
|
|
14
15
|
return (ref) => {
|
|
15
16
|
if (memo.has(ref)) return memo.get(ref);
|
|
16
17
|
let target = null;
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// ⚠ `parseRef`, NEVER `ref.indexOf('/')`. This split at the first slash until 0.13.3, which
|
|
19
|
+
// made `family/people/gilad` the collection `family` (a NAMESPACE, not a collection) holding
|
|
20
|
+
// the id `people/gilad`. `store.read` threw, the catch below swallowed it, and the caller got
|
|
21
|
+
// null — which filter.js is documented to treat as NARROWING. So every one-hop relational
|
|
22
|
+
// filter over a namespaced collection matched ZERO records, with no error anywhere, while the
|
|
23
|
+
// identical filter over a default-namespace ref worked. Measured on a real vault: 151 rows via
|
|
24
|
+
// `companies/<id>`, 0 rows via `family/people/<id>`, 273 for the same records addressed flat.
|
|
25
|
+
// The blast radius was not only filtering — this resolver also evaluates `can-enter`/`can-exit`
|
|
26
|
+
// below, so a binding predicate hopping a namespaced ref reported "not available".
|
|
27
|
+
const parsed = parseRef(ref, store.namespaces);
|
|
28
|
+
if (parsed) {
|
|
19
29
|
try {
|
|
20
|
-
const { fields } = store.read(
|
|
21
|
-
target = { ...fields, id:
|
|
30
|
+
const { fields } = store.read(parsed.collection, parsed.id);
|
|
31
|
+
target = { ...fields, id: parsed.id };
|
|
22
32
|
} catch { /* dangling ref or unknown collection — narrows, never widens */ }
|
|
23
33
|
}
|
|
24
34
|
memo.set(ref, target);
|
package/src/schema-ops.js
CHANGED
|
@@ -597,23 +597,95 @@ function upsertField(ws, store, collection, fieldName, prop, required, verb) {
|
|
|
597
597
|
return { collection, field: fieldName, file: dest, extends: doc.extends };
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
/**
|
|
601
|
+
* WHERE A UI-VIEW'S SOURCE ACTUALLY LIVES — asked of the manifest, exactly as `descriptorSourceDir`
|
|
602
|
+
* asks it for a collection, and for the same reason: the guard that matters is "will `npm install`
|
|
603
|
+
* erase this write", not "which module owns it".
|
|
604
|
+
*
|
|
605
|
+
* ⚠ This used to be `workspaceSystemDir` unconditionally, which silently meant a view could only be
|
|
606
|
+
* saved if the WORKSPACE MODULE happened to ship it. Saving one shipped by any other inline module
|
|
607
|
+
* wrote a SECOND file carrying the same id, and compile refuses that by name — so the whole write
|
|
608
|
+
* rolled back and the surface reported `name collision on ui-view "…"` instead of saving. Measured
|
|
609
|
+
* on gk-brain 2026-08-28: every one of its module-shipped views (`modules/family`, `modules/rnd`,
|
|
610
|
+
* `modules/services`) was unsaveable, and the failure said nothing about why.
|
|
611
|
+
*
|
|
612
|
+
* Returns `{ file, shipped }` — where to write, and the workspace-relative source that already
|
|
613
|
+
* exists (null for a new view, which lands in the workspace module as before).
|
|
614
|
+
*/
|
|
615
|
+
function uiViewSourceFile(ws, id) {
|
|
616
|
+
const src = readManifest(ws.root)?.entries?.[`ui-views/${id}.ui-view.yaml`]?.sources?.[0];
|
|
617
|
+
// sources are `{path, hash}`; tolerate the pre-0.10 string form, same as compile's staleness check
|
|
618
|
+
const shipped = typeof src === 'string' ? src : src?.path;
|
|
619
|
+
if (!shipped) return { file: path.join(workspaceSystemDir(ws, 'ui-views'), `${id}.ui-view.yaml`), shipped: null };
|
|
620
|
+
return { file: path.join(ws.root, shipped), shipped };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Re-attach a rewritten YAML source's COMMENTS — the part `dump` cannot round-trip.
|
|
625
|
+
*
|
|
626
|
+
* js-yaml drops every comment on `load` → `dump`. That is fine for a generated artifact and wrong
|
|
627
|
+
* for a module SOURCE, which is where this project writes down why something exists (`setScalar`
|
|
628
|
+
* above exists for the same reason). A real round-trip needs a different YAML library and core is
|
|
629
|
+
* not taking one on for this, so this does the narrow thing that is actually safe: a comment block
|
|
630
|
+
* sitting directly above a TOP-LEVEL key is carried back above that same key, if the key survived.
|
|
631
|
+
* The file header comes along for free — it is the block above the first key.
|
|
632
|
+
*
|
|
633
|
+
* ⚠ Deliberately top-level only. A comment above a NESTED key cannot be re-placed without knowing
|
|
634
|
+
* where that key ended up, and a misplaced comment is worse than an absent one: it would attach an
|
|
635
|
+
* explanation to something it does not explain. Those are still lost. Measured against
|
|
636
|
+
* `modules/family/ui-views/health-labs-abnormal.ui-view.yaml`, whose two blocks — the file header
|
|
637
|
+
* and the ⚠ above `filter:` — are both top-level and both survive.
|
|
638
|
+
*/
|
|
639
|
+
function reattachComments(oldText, newText) {
|
|
640
|
+
const TOP_KEY = /^([A-Za-z_][\w-]*):/;
|
|
641
|
+
const blocks = new Map(); // surviving key -> the comment lines that sat above it
|
|
642
|
+
let pending = [];
|
|
643
|
+
for (const line of oldText.split('\n')) {
|
|
644
|
+
if (line.startsWith('#') || line.trim() === '') { pending.push(line); continue; }
|
|
645
|
+
const key = TOP_KEY.exec(line)?.[1];
|
|
646
|
+
if (key && pending.some((l) => l.startsWith('#'))) {
|
|
647
|
+
while (pending.length && pending[pending.length - 1].trim() === '') pending.pop();
|
|
648
|
+
blocks.set(key, pending);
|
|
649
|
+
}
|
|
650
|
+
pending = [];
|
|
651
|
+
}
|
|
652
|
+
if (!blocks.size) return newText;
|
|
653
|
+
|
|
654
|
+
const out = [];
|
|
655
|
+
for (const line of newText.split('\n')) {
|
|
656
|
+
const block = blocks.get(TOP_KEY.exec(line)?.[1]);
|
|
657
|
+
if (block) out.push(...block);
|
|
658
|
+
out.push(line);
|
|
659
|
+
}
|
|
660
|
+
return out.join('\n');
|
|
661
|
+
}
|
|
662
|
+
|
|
600
663
|
// saved views (M3): a studio-saved view IS a ui-view record — but ui-views are
|
|
601
664
|
// system-stored (sources + compile), so the write goes through the same gate as any
|
|
602
665
|
// other schema op. the studio "save view" button lands here.
|
|
603
666
|
export function saveUiView(ws, store, { id, view }) {
|
|
604
667
|
if (!id || !/^[a-z0-9][a-z0-9-/]*$/.test(id)) throw new Error(`invalid ui-view id "${id}" — lowercase slug required`);
|
|
605
|
-
const dest =
|
|
668
|
+
const { file: dest, shipped } = uiViewSourceFile(ws, id);
|
|
669
|
+
if (shipped && /(^|\/)node_modules\//.test(shipped))
|
|
670
|
+
throw new Error(`ui-view "${id}" is shipped by an installed package (${shipped}) — a write there is erased by the next npm install.\n save it under a different name, or disable it (dreamteamer.disable) and re-create it.`);
|
|
606
671
|
const existed = fs.existsSync(dest);
|
|
672
|
+
// A module source is where this project writes down WHY a view exists; `dump` cannot keep that.
|
|
673
|
+
const previous = existed ? fs.readFileSync(dest, 'utf8') : null;
|
|
607
674
|
writeGated(ws, store, [dest], `dreamteamer: ui-views ${existed ? 'update' : 'add'} ${id}`, () => {
|
|
608
675
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
609
|
-
fs.writeFileSync(dest, dump(view));
|
|
676
|
+
fs.writeFileSync(dest, previous === null ? dump(view) : reattachComments(previous, dump(view)));
|
|
610
677
|
});
|
|
611
678
|
return { id, file: dest, updated: existed };
|
|
612
679
|
}
|
|
613
680
|
|
|
614
681
|
export function removeUiView(ws, store, id) {
|
|
615
|
-
|
|
616
|
-
|
|
682
|
+
// Same source resolution as the save above — an inline module's view is under this repo's git
|
|
683
|
+
// history like everything else, so deleting it is one revertable commit. Refusing it while
|
|
684
|
+
// ALLOWING a save to the same file would be an asymmetry with nothing behind it.
|
|
685
|
+
const { file: dest, shipped } = uiViewSourceFile(ws, id);
|
|
686
|
+
if (shipped && /(^|\/)node_modules\//.test(shipped))
|
|
687
|
+
throw new Error(`ui-view "${id}" is shipped by an installed package (${shipped}) — removing the file would be undone by the next npm install.\n disable it instead: add "<module>/${id}" to dreamteamer.disable in package.json.`);
|
|
688
|
+
if (!fs.existsSync(dest)) throw new Error(`ui-view "${id}" does not exist`);
|
|
617
689
|
writeGated(ws, store, [dest], `dreamteamer: ui-views rm ${id}`, () => fs.rmSync(dest));
|
|
618
690
|
return { removed: id };
|
|
619
691
|
}
|