dreamteamer 0.30.0 → 0.32.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 +47 -153
- package/collections/collections.collection.yaml +26 -9
- package/package.json +14 -2
- package/skills/using-dreamteamer/SKILL.md +16 -10
- package/skills/using-dreamteamer/references/before-you-build.md +4 -3
- package/skills/using-dreamteamer/references/collections.md +71 -1
- package/skills/using-dreamteamer/references/commands.md +3 -3
- package/skills/using-dreamteamer/references/data-modeling.md +9 -1
- package/skills/using-dreamteamer/references/extensions.md +60 -0
- package/skills/using-dreamteamer/references/records.md +17 -0
- package/skills/using-dreamteamer/references/sessions.md +4 -5
- package/skills/using-dreamteamer/references/skills.md +3 -5
- package/src/api.d.ts +109 -0
- package/src/api.js +70 -0
- package/src/check.js +57 -2
- package/src/checkout.js +105 -330
- package/src/cli.js +90 -274
- package/src/collections-cli.js +16 -165
- package/src/commit.js +7 -0
- package/src/compile.js +211 -155
- package/src/events.js +7 -2
- package/src/extensions.js +135 -0
- package/src/filter.js +1 -1
- package/src/harnesses.js +82 -135
- package/src/init.js +9 -3
- package/src/placement.js +209 -0
- package/src/records-api.d.ts +103 -0
- package/src/records-api.js +40 -0
- package/src/runtime.js +2 -2
- package/src/schema-ops.js +26 -9
- package/src/store.js +396 -35
- package/collections/containers.collection.yaml +0 -81
- package/collections/images.collection.yaml +0 -46
- package/collections/proofs.collection.yaml +0 -96
- package/skills/using-dreamteamer/references/exporting.md +0 -53
- package/skills/using-dreamteamer/references/proofs.md +0 -435
- package/skills/using-dreamteamer/references/worktrees.md +0 -235
- package/src/container-archive.js +0 -356
- package/src/containers.js +0 -635
- package/src/export-notebooklm.js +0 -502
- package/src/land.js +0 -743
- package/src/prove.js +0 -1922
- package/src/server.js +0 -481
package/src/compile.js
CHANGED
|
@@ -10,28 +10,21 @@ import addFormats from 'ajv-formats';
|
|
|
10
10
|
import { load, dump } from './yaml.js';
|
|
11
11
|
import { slug } from './template.js';
|
|
12
12
|
import { walk, patternRe } from './records.js';
|
|
13
|
+
import { refTargetsOf } from './ref.js';
|
|
14
|
+
import { subpathProblem, placedRecords } from './placement.js';
|
|
13
15
|
import { unknownOperators } from './filter.js';
|
|
14
16
|
import {
|
|
15
17
|
normalizeNamespaces, namespaceProblems, unqualifiedProblems, defaultStoragePath, storageOverlaps,
|
|
16
18
|
baseNameOf, singular, namespaceOf } from './namespace.js';
|
|
17
19
|
// circular on paper in earlier versions — safe: both sides only
|
|
18
20
|
// call at run time, same pattern as store.js ↔ compile.js.
|
|
19
|
-
import { runHarnessAdapters, BEGIN, END, INSTRUCTIONS_BEGIN, INSTRUCTIONS_END } from './harnesses.js';
|
|
21
|
+
import { runHarnessAdapters, renderContributions, BEGIN, END, INSTRUCTIONS_BEGIN, INSTRUCTIONS_END } from './harnesses.js';
|
|
20
22
|
import { ensureEditorRecommendation, ensureEnvExample } from './workspace.js';
|
|
21
23
|
import { satisfies } from './semver.js';
|
|
22
24
|
import { parseEnvValues } from './env-vars.js';
|
|
23
|
-
import { DERIVED_KINDS, readManifest, runtimeDir, engineId, engineVersion } from './runtime.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Every existing caller spells it `from './compile.js'`, and a second reader of the engine's own
|
|
27
|
-
// package.json is exactly the drift this file's comments keep naming.
|
|
28
|
-
export { engineId, engineVersion };
|
|
29
|
-
import { artifactRefs, proofPathFor, validateProofShape, stepWarnings } from './prove.js';
|
|
30
|
-
|
|
31
|
-
// re-exported, not moved: `readManifest` is in the VS Code extension's hand-maintained engine
|
|
32
|
-
// contract as `compileMod.readManifest` (engine.ts), and a removed export is the same cross-repo
|
|
33
|
-
// break as a removed file — decision 139.
|
|
34
|
-
export { readManifest };
|
|
25
|
+
import { DERIVED_KINDS, readManifest, runtimeDir, engineId, engineVersion, loadDescriptors as loadCompiledDescriptors } from './runtime.js';
|
|
26
|
+
import { excludedFromKind, disablesPackage, isPackageEntry } from './extensions.js';
|
|
27
|
+
export { engineId, engineVersion, readManifest };
|
|
35
28
|
|
|
36
29
|
/**
|
|
37
30
|
* Identifier → display label: `finance-accounts` → "Finance Accounts".
|
|
@@ -390,23 +383,110 @@ function stampMirror(byName, ctx, ownerName, field, prop, holder, mirrorName, ta
|
|
|
390
383
|
t.schema.properties = { ...t.schema.properties, [mirrorName]: generated };
|
|
391
384
|
}
|
|
392
385
|
|
|
393
|
-
export const KINDS = ['collections', 'skills', 'agents', 'commands', 'command-bindings', 'ui-views', 'collection-templates', 'proofs'];
|
|
394
|
-
const FOLDER_KINDS = new Set(['skills']); // folder-shape entities: copy the whole record folder
|
|
395
|
-
|
|
396
386
|
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
387
|
+
* `storage.under` — RELATIONSHIP-BASED STORAGE, validated and derived (see src/placement.js for the
|
|
388
|
+
* contract the record layer holds). Authored as `{ field, path }` on the CHILD; compiled with the
|
|
389
|
+
* parent `collection` stamped on, read off the field's `x-reference`, so Store, check and events
|
|
390
|
+
* never open a schema to find the parent.
|
|
400
391
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
392
|
+
* Authored on the child rather than on the parent's inverse field on purpose: the parent's side of
|
|
393
|
+
* the relation is a GENERATED mirror (or absent — no inverse is required), and `storage` is the
|
|
394
|
+
* block that already answers "where do THIS collection's records live". One authored spelling, one
|
|
395
|
+
* compiled spelling, no second copy.
|
|
405
396
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
397
|
+
* Every refusal below is a shape the record layer could not make safe at runtime: a list owner has
|
|
398
|
+
* no single folder, a file-shape parent has no folder at all, an opaque or folder-shape child needs
|
|
399
|
+
* code the store does not carry yet, a second level of nesting has no reader, and two children on
|
|
400
|
+
* one path would index each other's files. Compile is where a descriptor is read, so compile says no.
|
|
401
|
+
*/
|
|
402
|
+
function resolvePlacement(byName) {
|
|
403
|
+
const claims = new Map(); // parent collection -> [{ path, name }]
|
|
404
|
+
for (const [name, d] of byName) {
|
|
405
|
+
const under = d.storage?.under;
|
|
406
|
+
if (under === undefined) continue;
|
|
407
|
+
const where = `collection "${name}": storage.under`;
|
|
408
|
+
if (!under || typeof under !== 'object' || Array.isArray(under)) fail(`${where} must be an object { field: <reference field>, path: <folder inside the parent record> }`);
|
|
409
|
+
const unknown = Object.keys(under).filter((k) => k !== 'field' && k !== 'path');
|
|
410
|
+
if (unknown.length) fail(`${where} has unknown key(s) ${unknown.join(', ')} — it takes \`field\` and \`path\`, nothing else`);
|
|
411
|
+
if (typeof under.field !== 'string' || !under.field) fail(`${where}.field must name the scalar reference field that holds the parent`);
|
|
412
|
+
const bad = subpathProblem(under.path);
|
|
413
|
+
if (bad) fail(`${where}.path ${bad}`);
|
|
414
|
+
// the CHILD's own shape first: an opaque collection has no authored fields at all (compile
|
|
415
|
+
// replaces its schema with the derived ones), so judged later this would read as "no such field"
|
|
416
|
+
if ((d.storage.codec ?? 'md') === 'file') fail(`${where}: this collection is codec: file — an opaque record is not placed under a parent yet; keep it in its own folder`);
|
|
417
|
+
if ((d.storage.shape ?? 'file') === 'folder') fail(`${where}: this collection is shape: folder — a folder record is not placed under a parent yet; keep it in its own folder`);
|
|
418
|
+
const prop = d.schema?.properties?.[under.field];
|
|
419
|
+
if (!prop || typeof prop !== 'object') fail(`${where}.field "${under.field}" — no such field in ${name}'s schema`);
|
|
420
|
+
if (prop.type === 'array' || prop.items) fail(`${where}.field "${under.field}" is a list — a record lives in ONE place, so its owner is a scalar reference`);
|
|
421
|
+
const targets = refTargetsOf(prop);
|
|
422
|
+
if (!targets) fail(`${where}.field "${under.field}" is not a reference — the owner field needs \`x-reference: <parent collection>\``);
|
|
423
|
+
if (targets === '*' || targets.length !== 1) fail(`${where}.field "${under.field}" must reference exactly one collection — a record can live under one kind of parent`);
|
|
424
|
+
const parentName = targets[0];
|
|
425
|
+
const parent = byName.get(parentName);
|
|
426
|
+
if (!parent) fail(`${where}: parent collection "${parentName}" is not installed — a record cannot live inside a folder nothing provides`);
|
|
427
|
+
// nesting before shape: a placed collection is file-shape by the rule two lines up, so judged
|
|
428
|
+
// the other way round every nesting attempt would be told to make its parent a folder
|
|
429
|
+
if (parent.storage?.under !== undefined) fail(`${where}: "${parentName}" is itself stored under another collection — one level is supported; a placed collection cannot be a parent`);
|
|
430
|
+
if ((parent.storage?.shape ?? 'file') !== 'folder') fail(`${where}: "${parentName}" is not shape: folder — a record can only live INSIDE a parent that is a folder (storage: { shape: folder, entry: <file> } on ${parentName})`);
|
|
431
|
+
if ((parent.storage?.repo ?? '.') !== (d.storage?.repo ?? '.')) fail(`${where}: "${parentName}" lives in another git repo (storage.repo) — a record and the folder it sits in must share one`);
|
|
432
|
+
const entry = parent.storage.entry;
|
|
433
|
+
if (entry && under.path.split('/')[0] === entry) fail(`${where}.path "${under.path}" collides with ${parentName}'s entry file "${entry}" — pick a folder name`);
|
|
434
|
+
const siblings = claims.get(parentName) ?? [];
|
|
435
|
+
for (const s of siblings) {
|
|
436
|
+
if (s.path === under.path || s.path.startsWith(under.path + '/') || under.path.startsWith(s.path + '/')) {
|
|
437
|
+
fail(`collections "${s.name}" and "${name}" both store records under ${parentName}/<id>/${s.path === under.path ? s.path : `${s.path} · ${under.path}`} — one would index the other's files; give each its own folder`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
claims.set(parentName, [...siblings, { path: under.path, name }]);
|
|
441
|
+
d.storage.under = { field: under.field, path: under.path, collection: parentName };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* A `storage.under` that is REMOVED or CHANGED while records still sit under the old declaration is
|
|
447
|
+
* refused (R3). The compiled descriptor is the only thing that knows where those records are: the
|
|
448
|
+
* moment it is rewritten, listing, check and relocate all read the new layout, the old child folders
|
|
449
|
+
* fall out of every walk, and a workspace with records on disk reports ✔ 0 violations over fewer
|
|
450
|
+
* records than it holds — the quietest data loss there is. So the runtime about to be replaced is
|
|
451
|
+
* read first, and a transition with records in the way names the two-step that is safe:
|
|
452
|
+
* `relocate --to-root` under the OLD declaration (ids unchanged), then compile, then `relocate`.
|
|
453
|
+
* Adding `under` to a conventional collection moves nothing out of sight and is not refused.
|
|
408
454
|
*/
|
|
409
|
-
|
|
455
|
+
function refusePlacementTransitions(root, byName) {
|
|
456
|
+
const previous = loadCompiledDescriptors(root);
|
|
457
|
+
if (!previous) return;
|
|
458
|
+
for (const [name, d] of byName) {
|
|
459
|
+
const prev = previous.get(name);
|
|
460
|
+
const was = prev?.storage?.under;
|
|
461
|
+
if (!was?.collection || !was.path) continue;
|
|
462
|
+
const now = d.storage?.under ?? null;
|
|
463
|
+
const parent = previous.get(was.collection);
|
|
464
|
+
if (!parent?.storage?.path || !prev.storage?.path) continue;
|
|
465
|
+
// The EFFECTIVE root, not only the annotation: the parent collection's own `storage.path` is
|
|
466
|
+
// part of where every child record is, so moving the parent's folder in its descriptor strands
|
|
467
|
+
// the children exactly as dropping `under` does (R3b).
|
|
468
|
+
const newParentPath = now ? byName.get(now.collection)?.storage?.path : null;
|
|
469
|
+
const same = now && now.collection === was.collection && now.path === was.path && newParentPath === parent.storage.path;
|
|
470
|
+
if (same) continue;
|
|
471
|
+
let n = 0;
|
|
472
|
+
for (const r of placedRecords(prev, path.join(root, prev.storage.path), path.join(root, parent.storage.path))) if (r.parentId !== null) n++;
|
|
473
|
+
if (!n) continue;
|
|
474
|
+
const what = !now ? 'storage.under was removed'
|
|
475
|
+
: now.collection !== was.collection || now.path !== was.path ? `storage.under changed (${was.path} → ${now.path})`
|
|
476
|
+
: `${was.collection}'s storage.path changed (${parent.storage.path} → ${newParentPath})`;
|
|
477
|
+
fail(`collection "${name}": ${what}, but ${n} ${name} record(s) still sit inside ${was.collection} folders (${parent.storage.path}/<id>/${was.path}/) — compiling would stop every reader seeing them. First move them out under the CURRENT declaration: dreamteamer relocate ${name} --to-root (to ${prev.storage.path}, ids unchanged), then compile${now ? `, then dreamteamer relocate ${name} to place them again` : ''}.`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** The source kinds the compiler itself stages. An installed extension may add more
|
|
482
|
+
* (`sourceKinds`, src/extensions.js) — every enumeration below reads `kindsOf(ws)`, never this alone. */
|
|
483
|
+
export const KINDS = ['collections', 'skills', 'agents', 'commands', 'command-bindings', 'ui-views', 'collection-templates'];
|
|
484
|
+
const FOLDER_KINDS = new Set(['skills']); // folder-shape entities: copy the whole record folder
|
|
485
|
+
|
|
486
|
+
/** Every contributed kind of the workspace's loaded extensions, as `{ kind, exclude, extension }`. */
|
|
487
|
+
export const contributedKinds = (ws) => (ws.extensions ?? []).flatMap((e) => e.sourceKinds ?? []);
|
|
488
|
+
/** Built-in kinds, then contributed ones. */
|
|
489
|
+
export const kindsOf = (ws) => [...KINDS, ...contributedKinds(ws).map((k) => k.kind)];
|
|
410
490
|
// DERIVED_KINDS (projected, not staged) lives in runtime.js — the boundary both halves read. Not in
|
|
411
491
|
// KINDS on purpose: a module folder named `modules/` would be nonsense, and `isSystem` below keys
|
|
412
492
|
// off KINDS to decide `storage.base`, so a `modules` collection landing on `base: workspace` would
|
|
@@ -484,9 +564,9 @@ const NON_SOURCE_DIRS = new Set([
|
|
|
484
564
|
* for one to be. Known kinds under `system/` keep compiling — the fallback is deliberate and stays
|
|
485
565
|
* (CLAUDE.md); only UNKNOWN folders become errors, and `dreamteamer.ignore` excuses them in both
|
|
486
566
|
* places with one entry. */
|
|
487
|
-
function strayKindDirs(source, wsRoot, declaredIgnore) {
|
|
567
|
+
function strayKindDirs(source, wsRoot, declaredIgnore, kinds) {
|
|
488
568
|
if (path.resolve(source.root) === path.resolve(wsRoot)) return [];
|
|
489
|
-
const allow = new Set([...
|
|
569
|
+
const allow = new Set([...kinds, ...NON_SOURCE_DIRS, ...declaredIgnore]);
|
|
490
570
|
const dirsIn = (dir, prefix = '') => (fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }) : [])
|
|
491
571
|
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !allow.has(e.name))
|
|
492
572
|
.map((e) => `${prefix}${e.name}`);
|
|
@@ -500,10 +580,6 @@ function strayKindDirs(source, wsRoot, declaredIgnore) {
|
|
|
500
580
|
|
|
501
581
|
const sha256 = (buf) => 'sha256:' + createHash('sha256').update(buf).digest('hex');
|
|
502
582
|
|
|
503
|
-
/** Is this workspace-relative source path inside an installed package? A nudge to write a file
|
|
504
|
-
* there would name a path the next `npm install` erases. (schema-ops has the same one-liner for the
|
|
505
|
-
* same reason; importing it back here would close a cycle for one regex.) */
|
|
506
|
-
const inNodeModules = (p) => /(^|[\\/])node_modules([\\/]|$)/.test(String(p));
|
|
507
583
|
|
|
508
584
|
// channel -> the directory the operator knows it by (used in shadow warnings, and it IS the
|
|
509
585
|
// `location` field's vocabulary — see collections/modules.collection.yaml. Keeping the export name
|
|
@@ -528,10 +604,17 @@ export function discoverModules(root, pkg) {
|
|
|
528
604
|
// is applied per source at compile time. The bare form is what lets a workspace take a PACKAGE of
|
|
529
605
|
// modules and keep only the ones it wants — a disabled module is simply never discovered, so every
|
|
530
606
|
// caller (compile, status, install) sees the same set.
|
|
531
|
-
const
|
|
607
|
+
const disable = pkg?.dreamteamer?.disable ?? [];
|
|
532
608
|
const disabledHits = new Set();
|
|
609
|
+
// the same rule the extension loader applies (full package name, or its scope-stripped id), so a
|
|
610
|
+
// disabled package loses its content AND its code together
|
|
611
|
+
const disabled = (name) => {
|
|
612
|
+
const hit = disable.find((d) => isPackageEntry(d) && disablesPackage([d], name));
|
|
613
|
+
if (hit) disabledHits.add(hit);
|
|
614
|
+
return !!hit;
|
|
615
|
+
};
|
|
533
616
|
const tryAdd = (name, srcRoot, channel) => {
|
|
534
|
-
if (
|
|
617
|
+
if (disabled(name)) return;
|
|
535
618
|
const existing = byName.get(name);
|
|
536
619
|
if (existing) { shadows.push({ name, winner: existing.channel, loser: channel }); return; }
|
|
537
620
|
byName.set(name, { name, root: srcRoot, channel });
|
|
@@ -557,6 +640,9 @@ export function discoverModules(root, pkg) {
|
|
|
557
640
|
}
|
|
558
641
|
};
|
|
559
642
|
const tryAddOrUnpack = (name, srcRoot, channel) => {
|
|
643
|
+
// ⚠ THE PACKAGE FIRST: disabling a bundle disables every module in it. Only the children used to
|
|
644
|
+
// be checked, so `disable: ['@x/bundle']` stopped its code and compiled all of its content.
|
|
645
|
+
if (disabled(name)) return;
|
|
560
646
|
const bundle = path.join(srcRoot, 'modules');
|
|
561
647
|
if (fs.existsSync(bundle) && fs.statSync(bundle).isDirectory()) { scanDir(bundle, channel, false); return; }
|
|
562
648
|
tryAdd(name, srcRoot, channel);
|
|
@@ -617,8 +703,15 @@ export function shadowWarning({ name, winner, loser }) {
|
|
|
617
703
|
return `⚠ module ${name}: ${CHANNEL_LABEL[winner]} copy shadows ${CHANNEL_LABEL[loser]} copy`;
|
|
618
704
|
}
|
|
619
705
|
|
|
620
|
-
export function compile(
|
|
706
|
+
export function compile(ws) {
|
|
707
|
+
const { root, pkg } = ws;
|
|
708
|
+
const extensions = ws.extensions ?? [];
|
|
709
|
+
const contributed = contributedKinds(ws);
|
|
710
|
+
const kinds = kindsOf(ws);
|
|
621
711
|
const RUNTIME = runtimeDir(root);
|
|
712
|
+
// read BEFORE anything below replaces the runtime: an extension's analysis compares against it,
|
|
713
|
+
// and the harness pass prunes what it listed
|
|
714
|
+
const prevManifest = readManifest(root);
|
|
622
715
|
const config = pkg.dreamteamer ?? {};
|
|
623
716
|
const harnesses = config.harnesses ?? ['claude-code'];
|
|
624
717
|
const rel = (p) => path.relative(root, p);
|
|
@@ -660,7 +753,7 @@ export function compile({ root, pkg }) {
|
|
|
660
753
|
} else {
|
|
661
754
|
const strays = [];
|
|
662
755
|
if (fs.existsSync(path.join(root, 'system')) && [...walk(path.join(root, 'system'))].length) strays.push('system/');
|
|
663
|
-
for (const kind of
|
|
756
|
+
for (const kind of kinds) {
|
|
664
757
|
const dir = path.join(root, kind);
|
|
665
758
|
if (fs.existsSync(dir) && [...walk(dir)].length) strays.push(`${kind}/`);
|
|
666
759
|
}
|
|
@@ -874,22 +967,22 @@ export function compile({ root, pkg }) {
|
|
|
874
967
|
}
|
|
875
968
|
|
|
876
969
|
/** module names that actually put something into the compiled runtime — see the warning below */
|
|
877
|
-
const
|
|
970
|
+
const contributedBy = new Set();
|
|
878
971
|
|
|
879
972
|
for (const source of sources) {
|
|
880
973
|
// an unrecognised folder at a module root is a typo'd kind or a kind the engine dropped —
|
|
881
974
|
// both of which used to compile ✔ and contribute nothing (see NON_SOURCE_DIRS)
|
|
882
|
-
const strays = strayKindDirs(source, root, moduleIgnores.get(source.name) ?? []);
|
|
975
|
+
const strays = strayKindDirs(source, root, moduleIgnores.get(source.name) ?? [], kinds);
|
|
883
976
|
if (strays.length) {
|
|
884
977
|
// `ignore` matches a FOLDER NAME, at the root and under `system/` alike, so the remedy has
|
|
885
978
|
// to quote the bare name — suggesting "system/gizmos" would print an entry that matches
|
|
886
979
|
// nothing and send the reader back for a second compile to find that out.
|
|
887
980
|
const ignorable = [...new Set(strays.map((s) => s.replace(/^system\//, '')))];
|
|
888
981
|
fail(`module "${source.name}" (${rel(source.root)}) has folder(s) that are not a known kind: ${strays.join(', ')}
|
|
889
|
-
known kinds: ${
|
|
982
|
+
known kinds: ${kinds.join(', ')}${contributed.length ? '' : `\n a kind an extension adds (proofs/, for one) is known only while that extension is installed or its module is present`}
|
|
890
983
|
if these are not sources, declare them: "dreamteamer": { "ignore": [${ignorable.map((s) => `"${s}"`).join(', ')}] } in ${rel(path.join(source.root, 'package.json'))}`);
|
|
891
984
|
}
|
|
892
|
-
for (const kind of
|
|
985
|
+
for (const kind of kinds) {
|
|
893
986
|
// a half-moved module compiles its flat half and drops the rest — say so rather than
|
|
894
987
|
// reporting ✔ over a silent partial read (the decision-156 failure shape)
|
|
895
988
|
if (bothLayouts(source.root, kind)) {
|
|
@@ -911,7 +1004,7 @@ export function compile({ root, pkg }) {
|
|
|
911
1004
|
: fs.readdirSync(srcDir).sort();
|
|
912
1005
|
for (const name of names) {
|
|
913
1006
|
if (name.startsWith('.')) continue;
|
|
914
|
-
if (
|
|
1007
|
+
if (excludedFromKind(contributed, kind, name)) continue;
|
|
915
1008
|
const entityId = name.replace(/\.[^.]+\.(yaml|md|json)$/, '');
|
|
916
1009
|
if (disabled.has(`${source.name}/${entityId}`)) { disabledHits.add(`${source.name}/${entityId}`); continue; }
|
|
917
1010
|
const srcPath = path.join(srcDir, name);
|
|
@@ -938,27 +1031,27 @@ export function compile({ root, pkg }) {
|
|
|
938
1031
|
if (opaque && Object.keys(doc.schema?.properties ?? {}).length) console.warn(`⚠ collection ${doc.name}: \`schema\` is ignored under \`codec: file\` — an opaque record's fields are derived (ext, bytes)`);
|
|
939
1032
|
if (!descriptorGroups.has(doc.name)) descriptorGroups.set(doc.name, []);
|
|
940
1033
|
descriptorGroups.get(doc.name).push({ src: { path: rel(srcPath), hash: sha256(bytes) }, doc, moduleName: source.name });
|
|
941
|
-
|
|
1034
|
+
contributedBy.add(source.name);
|
|
942
1035
|
} else if (FOLDER_KINDS.has(kind) && isDir) {
|
|
943
1036
|
// the folder's ENTITY file, not its payload — see refuseEmptySource
|
|
944
1037
|
const entityFile = path.join(srcPath, 'SKILL.md');
|
|
945
1038
|
if (fs.existsSync(entityFile)) refuseEmptySource(entityFile);
|
|
946
1039
|
for (const file of walk(srcPath)) {
|
|
947
1040
|
addEntry(path.join(kind, name, path.relative(srcPath, file)), file);
|
|
948
|
-
|
|
1041
|
+
contributedBy.add(source.name);
|
|
949
1042
|
}
|
|
950
1043
|
counts[kind]++;
|
|
951
1044
|
} else if (!isDir) {
|
|
952
1045
|
refuseEmptySource(srcPath);
|
|
953
1046
|
addEntry(path.join(kind, name), srcPath);
|
|
954
|
-
|
|
1047
|
+
contributedBy.add(source.name);
|
|
955
1048
|
counts[kind]++;
|
|
956
1049
|
} else {
|
|
957
1050
|
// nested dirs for file-shape kinds (e.g. date-partitioned) — recurse
|
|
958
1051
|
for (const file of walk(srcPath)) {
|
|
959
1052
|
refuseEmptySource(file);
|
|
960
1053
|
addEntry(path.join(kind, path.relative(srcDir, file)), file);
|
|
961
|
-
|
|
1054
|
+
contributedBy.add(source.name);
|
|
962
1055
|
counts[kind]++;
|
|
963
1056
|
}
|
|
964
1057
|
}
|
|
@@ -996,7 +1089,7 @@ export function compile({ root, pkg }) {
|
|
|
996
1089
|
// A UI bundle IS a contribution. Counting it here is what keeps the warning below honest —
|
|
997
1090
|
// a module whose whole purpose is a layout used to be told it "contributed no recognised
|
|
998
1091
|
// sources" while its layout was rendering in the app.
|
|
999
|
-
|
|
1092
|
+
contributedBy.add(source.name);
|
|
1000
1093
|
}
|
|
1001
1094
|
|
|
1002
1095
|
// A module that ships only folders the engine does not recognise compiles ✔ and contributes
|
|
@@ -1008,10 +1101,11 @@ export function compile({ root, pkg }) {
|
|
|
1008
1101
|
// was written for: a module that ships nothing the engine recognises AT ALL, which is what
|
|
1009
1102
|
// decision 156 cost two days. `kindDir` returns the flat path when neither layout exists, so this
|
|
1010
1103
|
// is a genuine existence test on either spelling.
|
|
1104
|
+
const extensionNames = new Set(extensions.map((e) => e.name));
|
|
1011
1105
|
for (const source of sources) {
|
|
1012
|
-
if (
|
|
1013
|
-
if (
|
|
1014
|
-
console.warn(`⚠ module "${source.name}" (${rel(source.root)}) contributed no recognised sources — its folder names must match a known kind (${
|
|
1106
|
+
if (contributedBy.has(source.name) || extensionNames.has(source.name)) continue;
|
|
1107
|
+
if (kinds.some((k) => fs.existsSync(kindDir(source.root, k)))) continue;
|
|
1108
|
+
console.warn(`⚠ module "${source.name}" (${rel(source.root)}) contributed no recognised sources — its folder names must match a known kind (${kinds.join(', ')}) or it must ship a UI bundle at ui/app.js`);
|
|
1015
1109
|
}
|
|
1016
1110
|
|
|
1017
1111
|
// ---- collection-templates, for `templates:` merging ----------------------------
|
|
@@ -1105,7 +1199,7 @@ export function compile({ root, pkg }) {
|
|
|
1105
1199
|
// verification. ⚠ `users` was in this set until 0.8.0 — a module still declaring
|
|
1106
1200
|
// `x-reference: users` now FAILS here, which is the intended loud outcome rather than a ref
|
|
1107
1201
|
// pointing at a collection nothing provides.
|
|
1108
|
-
const CORE_COLLECTIONS = new Set([...
|
|
1202
|
+
const CORE_COLLECTIONS = new Set([...kinds, ...DERIVED_KINDS, 'repos']);
|
|
1109
1203
|
const wsDir = config['workspace-module'];
|
|
1110
1204
|
const wsModuleName = wsDir
|
|
1111
1205
|
? sources.find((s) => rel(s.root) === path.join('modules', wsDir))?.name
|
|
@@ -1172,6 +1266,9 @@ export function compile({ root, pkg }) {
|
|
|
1172
1266
|
// inverted: every one of the seven would have compiled as `base: workspace`, resolved under
|
|
1173
1267
|
// the workspace root, read as zero records, and become writable through the store.
|
|
1174
1268
|
merged.storage ??= {};
|
|
1269
|
+
// A DRIVER collection (0.24–0.30: `containers`, `images`) was answered by Docker, never by files.
|
|
1270
|
+
// Compiled as an ordinary collection it would become a writable folder of fake host records.
|
|
1271
|
+
if (merged.storage.driver !== undefined) fail(`collection "${name}": storage.driver is gone since 0.31.0 — a container is not a record, and the Docker host left core. Delete the descriptor (${group.map((g) => g.src.path).join(', ')}).`);
|
|
1175
1272
|
const owned = dataOwners.get(storageOwnerOf(group, base));
|
|
1176
1273
|
// A namespaced collection's folder IS its namespace, nested: `health/doctors` →
|
|
1177
1274
|
// `data/health/doctors`. Derived rather than required so a descriptor never has to repeat its
|
|
@@ -1195,7 +1292,7 @@ export function compile({ root, pkg }) {
|
|
|
1195
1292
|
// authors a suffix keeps it, and one that does not was writing `.undefined.` files.
|
|
1196
1293
|
merged.storage.suffix ??= singular(baseNameOf(name, namespaces));
|
|
1197
1294
|
const storagePath = String(merged.storage.path ?? '');
|
|
1198
|
-
const systemKinds = [...
|
|
1295
|
+
const systemKinds = [...kinds, ...DERIVED_KINDS];
|
|
1199
1296
|
const isSystem = systemKinds.includes(storagePath) || systemKinds.includes(storagePath.replace(/^system\//, ''));
|
|
1200
1297
|
merged.storage.base = isSystem ? 'runtime' : 'workspace';
|
|
1201
1298
|
if (owned && !isSystem) {
|
|
@@ -1386,6 +1483,11 @@ export function compile({ root, pkg }) {
|
|
|
1386
1483
|
moduleDeps, wsModuleName,
|
|
1387
1484
|
moduleOf: (n) => collOwner.get(n),
|
|
1388
1485
|
});
|
|
1486
|
+
// ---- placement: a collection stored UNDER another ---------------------------------
|
|
1487
|
+
// Here for the same reason relations are: `storage.under` names a field of this collection AND
|
|
1488
|
+
// the shape of ANOTHER collection, so it can only be judged once every descriptor exists.
|
|
1489
|
+
resolvePlacement(new Map([...mergedGroups].map(([n, g]) => [n, g.merged])));
|
|
1490
|
+
refusePlacementTransitions(root, new Map([...mergedGroups].map(([n, g]) => [n, g.merged])));
|
|
1389
1491
|
|
|
1390
1492
|
// ---- resolved labels, then bytes -------------------------------------------------
|
|
1391
1493
|
// A second loop rather than a tail of the first: generated mirror fields do not exist until the
|
|
@@ -1595,6 +1697,13 @@ export function compile({ root, pkg }) {
|
|
|
1595
1697
|
// ---- unresolved references are compile errors (an agent's declared skills)
|
|
1596
1698
|
const skillIds = new Set([...entries.keys()].filter((k) => k.startsWith('skills/')).map((k) => k.split('/')[1]));
|
|
1597
1699
|
for (const [rt, e] of entries) {
|
|
1700
|
+
// A SKILL whose frontmatter is not YAML compiled ✔ and was refused one gate later by `check`,
|
|
1701
|
+
// naming the compiled copy under .dreamteamer/ that nobody can edit — found when an installed
|
|
1702
|
+
// package shipped a description holding `: `. Parsed here, where the source path is known.
|
|
1703
|
+
if (/^skills\/[^/]+\/SKILL\.md$/.test(rt)) {
|
|
1704
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(e.bytes.toString('utf8'));
|
|
1705
|
+
if (fm) loadSource(fm[1], e.sources[0].path);
|
|
1706
|
+
}
|
|
1598
1707
|
if (rt.startsWith('agents/')) {
|
|
1599
1708
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(e.bytes.toString('utf8'));
|
|
1600
1709
|
const doc = fm ? loadSource(fm[1], e.sources[0].path) : {};
|
|
@@ -1680,46 +1789,45 @@ export function compile({ root, pkg }) {
|
|
|
1680
1789
|
}
|
|
1681
1790
|
}
|
|
1682
1791
|
|
|
1683
|
-
// ----
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
//
|
|
1693
|
-
// ⚠ `artifactRefs` is computed unconditionally, because the coverage line below prints its
|
|
1694
|
-
// denominators on EVERY compile — a workspace with no proofs still gets told what it has.
|
|
1695
|
-
const proofArtifacts = artifactRefs(entries);
|
|
1696
|
-
const proofEntries = [...entries].filter(([rt]) => rt.startsWith('proofs/'));
|
|
1697
|
-
/** every artifact ref named by at least one proof — the coverage numerators, and the nudge's silencer */
|
|
1698
|
-
const provenRefs = new Set();
|
|
1699
|
-
if (proofEntries.length) {
|
|
1700
|
-
// the MERGED descriptors, read back out of the entries this compile is about to write —
|
|
1701
|
-
// the same source of truth the ui-view block reads its own field list from, so a proof is
|
|
1702
|
-
// judged against the schema the workspace will actually run on (`extends` applied).
|
|
1703
|
-
const merged = new Map();
|
|
1792
|
+
// ---- extension analysis ------------------------------------------------------------
|
|
1793
|
+
// After the whole compile is assembled and BEFORE any output is replaced: an extension that owns a
|
|
1794
|
+
// source kind judges its entries against the final merged schemas (proofs name artifacts and filter
|
|
1795
|
+
// on fields, so only the assembled workspace can say whether they mean anything). The draft is data
|
|
1796
|
+
// only — no filesystem writer, no Store, no environment VALUES, key names only. An error fails the
|
|
1797
|
+
// compile exactly like a built-in check; notes print after the summary.
|
|
1798
|
+
const notes = [];
|
|
1799
|
+
if (extensions.some((e) => e.analyze)) {
|
|
1800
|
+
const descriptors = new Map();
|
|
1704
1801
|
for (const [rt, e] of entries) {
|
|
1705
1802
|
if (!rt.startsWith('collections/')) continue;
|
|
1706
1803
|
const doc = load(e.bytes.toString('utf8'));
|
|
1707
|
-
if (doc?.name)
|
|
1804
|
+
if (doc?.name) descriptors.set(doc.name, doc);
|
|
1708
1805
|
}
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1806
|
+
const draft = Object.freeze({
|
|
1807
|
+
entries,
|
|
1808
|
+
descriptors,
|
|
1809
|
+
modules: sources.map((s) => ({ id: moduleId(s.name), name: s.name, root: rel(s.root) || '.', channel: s.channel })),
|
|
1810
|
+
declaredVars: [...declaredVars],
|
|
1811
|
+
declaredEnv: [...declaredEnv.keys()],
|
|
1812
|
+
previousManifest: prevManifest,
|
|
1813
|
+
parse: (rt) => loadSource(entries.get(rt).bytes.toString('utf8'), entries.get(rt).sources[0].path),
|
|
1814
|
+
});
|
|
1815
|
+
for (const ext of extensions) {
|
|
1816
|
+
if (!ext.analyze) continue;
|
|
1817
|
+
const out = ext.analyze(draft) ?? {};
|
|
1818
|
+
if (out.errors?.length) fail(out.errors.join('\n '));
|
|
1819
|
+
for (const w of out.warnings ?? []) console.warn(`⚠ ${w}`);
|
|
1820
|
+
notes.push(...(out.notes ?? []));
|
|
1720
1821
|
}
|
|
1721
1822
|
}
|
|
1722
1823
|
|
|
1824
|
+
// ---- extension code for the harness pass, run BEFORE anything is replaced ------------
|
|
1825
|
+
// A contributed renderer that throws must leave the last good runtime and every harness file
|
|
1826
|
+
// byte-for-byte as they were — so it runs here, on data, and the write pass below only writes.
|
|
1827
|
+
let contributions;
|
|
1828
|
+
try { contributions = renderContributions({ entries, harnesses, version: engineVer, extensions }); }
|
|
1829
|
+
catch (e) { fail(e.message); }
|
|
1830
|
+
|
|
1723
1831
|
// ---- materialize .dreamteamer ------------------------------------------------
|
|
1724
1832
|
// mkdir the runtime ROOT unconditionally: with zero entries nothing below created it, so the
|
|
1725
1833
|
// manifest write at the end failed ENOENT — `init` followed by `compile` in a fresh workspace
|
|
@@ -1735,7 +1843,10 @@ export function compile({ root, pkg }) {
|
|
|
1735
1843
|
// (the workspace module's own record after it was renamed, and a domain module's after it was
|
|
1736
1844
|
// folded into another). The
|
|
1737
1845
|
// runtime is build output; stale build output is the compiler's problem, not the reader's.
|
|
1738
|
-
|
|
1846
|
+
// ⚠ AND every kind the PREVIOUS compile staged: an extension uninstalled since then leaves its
|
|
1847
|
+
// compiled folder behind otherwise, read by nothing and listed by `dt list` for ever.
|
|
1848
|
+
const prevKinds = (prevManifest?.['source-kinds'] ?? []).map((k) => k.kind).filter((k) => typeof k === 'string' && /^[a-z][a-z0-9-]*$/.test(k));
|
|
1849
|
+
for (const kind of new Set([...kinds, ...DERIVED_KINDS, ...prevKinds])) fs.rmSync(path.join(RUNTIME, kind), { recursive: true, force: true });
|
|
1739
1850
|
fs.rmSync(path.join(RUNTIME, 'system'), { recursive: true, force: true });
|
|
1740
1851
|
fs.rmSync(path.join(RUNTIME, 'ui'), { recursive: true, force: true });
|
|
1741
1852
|
for (const [rt, e] of entries) {
|
|
@@ -1745,14 +1856,13 @@ export function compile({ root, pkg }) {
|
|
|
1745
1856
|
}
|
|
1746
1857
|
|
|
1747
1858
|
// ---- harness adapters (dispatch table lives in harnesses.js) -------------------
|
|
1748
|
-
const prevManifest = readManifest(root);
|
|
1749
1859
|
// What the harness blocks should TELL an agent about where sources live — measured, not assumed.
|
|
1750
1860
|
// A workspace still on the nested layout was being handed prose naming the flat one, and that
|
|
1751
1861
|
// block is the first thing a session reads.
|
|
1752
|
-
const anyFlat = sources.some((s) =>
|
|
1753
|
-
const anyNested = sources.some((s) =>
|
|
1862
|
+
const anyFlat = sources.some((s) => kinds.some((k) => fs.existsSync(path.join(s.root, k))));
|
|
1863
|
+
const anyNested = sources.some((s) => kinds.some((k) => fs.existsSync(path.join(s.root, 'system', k))));
|
|
1754
1864
|
const sourceLayout = anyFlat && anyNested ? 'mixed' : anyNested ? 'nested' : 'flat';
|
|
1755
|
-
const { outputs: adapterOutputs, blocks: adapterBlocks, summary: harnessSummary } = runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout, namespaces, version: engineVer, workspaceModule: config['workspace-module'] ?? '' });
|
|
1865
|
+
const { outputs: adapterOutputs, blocks: adapterBlocks, summary: harnessSummary } = runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout, namespaces, version: engineVer, workspaceModule: config['workspace-module'] ?? '', extensions, kinds: contributed.map((k) => k.kind), contributions });
|
|
1756
1866
|
|
|
1757
1867
|
// ---- provenance manifest ------------------------------------------------------
|
|
1758
1868
|
const manifest = {
|
|
@@ -1776,6 +1886,11 @@ export function compile({ root, pkg }) {
|
|
|
1776
1886
|
// `storage.base` is a field instead of a path test. An older runtime has no key here, which
|
|
1777
1887
|
// reads as "no namespaces", which is exactly right for a workspace that never declared any.
|
|
1778
1888
|
namespaces,
|
|
1889
|
+
// The extensions this runtime was compiled WITH, and the source kinds they staged — so the next
|
|
1890
|
+
// compile can prune a kind whose extension is gone, and `staleness` can walk a contributed kind
|
|
1891
|
+
// without loading any extension code.
|
|
1892
|
+
...(extensions.length ? { extensions: extensions.map((e) => ({ name: e.name, version: e.version })) } : {}),
|
|
1893
|
+
...(contributed.length ? { 'source-kinds': contributed.map((k) => ({ kind: k.kind, exclude: k.exclude, extension: k.extension })) } : {}),
|
|
1779
1894
|
// ⚠ BOTH KEYS FOR ONE RELEASE. `location` is the new spelling; `channel` stays so a reader
|
|
1780
1895
|
// that has not moved — `runtime.sourceRoots`'s npm filter, and the extension — keeps working
|
|
1781
1896
|
// against a runtime this engine compiled. Removed in 0.20.0.
|
|
@@ -1796,75 +1911,14 @@ export function compile({ root, pkg }) {
|
|
|
1796
1911
|
};
|
|
1797
1912
|
fs.writeFileSync(path.join(RUNTIME, 'manifest.yaml'), dump(manifest));
|
|
1798
1913
|
|
|
1799
|
-
const summary =
|
|
1914
|
+
const summary = kinds.filter((k) => counts[k]).map((k) => `${counts[k]} ${k}${k === 'collections' && mergedCount ? ` (${mergedCount} merged)` : ''}`).join(', ');
|
|
1800
1915
|
const sourceLabel = config['workspace-module']
|
|
1801
1916
|
? `${sources.length} module(s) (workspace-module: ${config['workspace-module']})`
|
|
1802
1917
|
: `${sources.length - 1} module(s) + workspace`;
|
|
1803
1918
|
console.log(`✔ compiled ${summary || 'nothing'} from ${sourceLabel} → .dreamteamer`);
|
|
1804
1919
|
for (const line of harnessSummary) console.log(`✔ harness ${line}`);
|
|
1805
1920
|
|
|
1806
|
-
|
|
1807
|
-
// One line, unconditional, even at zero: coverage that is only reported when someone asks is
|
|
1808
|
-
// coverage nobody knows the number of. The denominators are what this compile actually produced
|
|
1809
|
-
// (`artifactRefs`, the one enumeration `prove --missing` also reads), the numerators what a
|
|
1810
|
-
// proof names.
|
|
1811
|
-
const covered = (list) => `${list.filter((a) => provenRefs.has(a)).length}/${list.length}`;
|
|
1812
|
-
console.log(`proofs: ${proofEntries.length} declared · commands ${covered(proofArtifacts.commands)} · skills ${covered(proofArtifacts.skills)} · scripts ${covered(proofArtifacts.scripts)} · bindings ${covered(proofArtifacts.bindings)}`);
|
|
1813
|
-
|
|
1814
|
-
// ---- the nudge: ONCE, per NEW command or script with no proof -----------------
|
|
1815
|
-
// module id → the module's workspace-relative ROOT, exactly the value the module projection
|
|
1816
|
-
// stores in `path`. Sorted longest-first so a nested module wins over its parent, and the root
|
|
1817
|
-
// layout's `.` (the empty prefix) matches last.
|
|
1818
|
-
const moduleRoots = sources.map((s) => ({ id: moduleId(s.name), root: rel(s.root) || '.' }))
|
|
1819
|
-
.sort((a, b) => b.root.length - a.root.length);
|
|
1820
|
-
const ownerRoot = (srcPath) => moduleRoots.find(({ root: r }) => r === '.' || srcPath === r || srcPath.startsWith(`${r}/`))?.root ?? '.';
|
|
1821
|
-
// ⚠ NEW, not merely uncovered. A workspace adopting proofs has forty-odd artifacts and none of
|
|
1822
|
-
// them proven; forty-four warnings on day one is the noise that teaches an operator to skip
|
|
1823
|
-
// every line this compile prints. So the nudge fires only for an artifact absent from the
|
|
1824
|
-
// PREVIOUS manifest, and a first-ever compile (no previous manifest at all) nudges nothing.
|
|
1825
|
-
//
|
|
1826
|
-
// Commands and scripts only — they are the artifacts that RUN, and a skill gets its nudge at the
|
|
1827
|
-
// moment `dt add skills` writes it. An artifact shipped from node_modules is skipped: a proof
|
|
1828
|
-
// written there is erased by the next `npm install`.
|
|
1829
|
-
//
|
|
1830
|
-
// ⚠ A KNOWN GAP, stated rather than hidden: a script added to an EXISTING module does not nudge.
|
|
1831
|
-
// The manifest records a module record's SOURCE hash (its package.json), and adding a file under
|
|
1832
|
-
// `bin/` changes neither the entry key nor that hash, so there is nothing to compare. The
|
|
1833
|
-
// coverage line still counts it.
|
|
1834
|
-
if (prevManifest?.entries) {
|
|
1835
|
-
for (const ref of [...proofArtifacts.commands, ...proofArtifacts.scripts]) {
|
|
1836
|
-
if (provenRefs.has(ref)) continue;
|
|
1837
|
-
const known = artifactSource(entries, ref);
|
|
1838
|
-
if (!known || known.new === false || inNodeModules(known.source)) continue;
|
|
1839
|
-
console.log(`no proof yet for ${ref} — ${proofPathFor(ref, known.moduleRoot)} (see using-dreamteamer › proofs)`);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
/** Where an artifact's source lives, and whether the PREVIOUS manifest already knew it.
|
|
1844
|
-
*
|
|
1845
|
-
* ⚠ THE MODULE ROOT COMES OFF THE SOURCE, NEVER OFF THE FILE PATH. Slicing it out of the
|
|
1846
|
-
* artifact's source path (`lastIndexOf('/commands/')`, `replace(/\/package\.json$/)`) is
|
|
1847
|
-
* correct only in the `workspace-module` layout. In the ROOT layout (no `workspace-module`, see
|
|
1848
|
-
* :619) a command's source is `commands/hello.command.md` — no module segment at all — so the
|
|
1849
|
-
* slice returned -1 and the nudge named `commands/hello.command.m/proofs/…`, and a module
|
|
1850
|
-
* script named `package.json/proofs/…`. `rel(source.root)` is the same value the module record's
|
|
1851
|
-
* `path` field carries (see the projection near :1459), which is the value that is always right. */
|
|
1852
|
-
function artifactSource(all, ref) {
|
|
1853
|
-
if (ref.startsWith('commands/')) {
|
|
1854
|
-
const key = `${ref}.command.md`;
|
|
1855
|
-
const source = all.get(key)?.sources?.[0]?.path ?? '';
|
|
1856
|
-
if (!source) return null;
|
|
1857
|
-
return { source, moduleRoot: ownerRoot(source), new: !(key in prevManifest.entries) };
|
|
1858
|
-
}
|
|
1859
|
-
// a module script: `<module-id>/bin/<file>` — the id is in the ref, so the root is a lookup
|
|
1860
|
-
// rather than a guess. Its "entry" is the module record compile projected, so a whole NEW
|
|
1861
|
-
// module's scripts nudge and an existing module's do not (above).
|
|
1862
|
-
const mod = ref.slice(0, ref.indexOf('/'));
|
|
1863
|
-
const key = `modules/${mod}.module.yaml`;
|
|
1864
|
-
const entry = all.get(key);
|
|
1865
|
-
if (!entry) return null;
|
|
1866
|
-
return { source: entry.sources?.[0]?.path ?? '', moduleRoot: moduleRoots.find((m) => m.id === mod)?.root ?? '.', new: !(key in prevManifest.entries) };
|
|
1867
|
-
}
|
|
1921
|
+
for (const n of notes) console.log(n);
|
|
1868
1922
|
return 0;
|
|
1869
1923
|
}
|
|
1870
1924
|
|
|
@@ -1913,15 +1967,17 @@ export function staleness(root) {
|
|
|
1913
1967
|
//
|
|
1914
1968
|
// The root itself, when a workspace declares no `workspace-module`, is not a named module, so no
|
|
1915
1969
|
// `<module>/<entity>` entry can address its sources — it is walked unfiltered, as before.
|
|
1916
|
-
const disabledEntities = new Set((pkg.dreamteamer?.disable ?? []).filter((d) => typeof d === 'string' && d
|
|
1970
|
+
const disabledEntities = new Set((pkg.dreamteamer?.disable ?? []).filter((d) => typeof d === 'string' && !isPackageEntry(d)));
|
|
1917
1971
|
const roots = [...(wm ? [] : [{ name: null, root }]), ...found.modules.map((m) => ({ name: m.name, root: m.root }))];
|
|
1972
|
+
// the kinds the last compile STAGED, read off its manifest — staleness loads no extension code
|
|
1973
|
+
const contributedKinds = (manifest['source-kinds'] ?? []).map((k) => ({ kind: k.kind, exclude: k.exclude ?? [] }));
|
|
1918
1974
|
for (const { name: moduleName, root: r } of roots) {
|
|
1919
|
-
for (const kind of KINDS) {
|
|
1975
|
+
for (const kind of [...KINDS, ...contributedKinds.map((k) => k.kind)]) {
|
|
1920
1976
|
const dir = kindDir(r, kind);
|
|
1921
1977
|
if (!fs.existsSync(dir)) continue;
|
|
1922
1978
|
for (const f of walk(dir)) {
|
|
1923
1979
|
const rel = path.relative(dir, f).split(path.sep).join('/');
|
|
1924
|
-
if (
|
|
1980
|
+
if (excludedFromKind(contributedKinds, kind, rel)) continue;
|
|
1925
1981
|
// The SAME id derivation `compile` uses, so the two can never disagree about which
|
|
1926
1982
|
// file a disable entry names.
|
|
1927
1983
|
//
|
package/src/events.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import { idFromRecordPath } from './records.js';
|
|
7
|
+
import { placedChildAt } from './placement.js';
|
|
7
8
|
|
|
8
9
|
/** Record events between two points, across EVERY repo that holds records. `from` is a sha or a
|
|
9
10
|
* date — a sha is meaningless in another repo, so it is resolved to its commit DATE and each
|
|
@@ -107,8 +108,12 @@ export function pathToRecord(descriptors, relPath) {
|
|
|
107
108
|
const rest = relPath.slice(best.storage.path.length + 1);
|
|
108
109
|
if (best.storage.shape === 'folder') {
|
|
109
110
|
const entry = best.storage.entry ?? 'SKILL.md';
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
// `<id>/<entry>` is the parent's own record; anything deeper may be a record of a collection
|
|
112
|
+
// stored UNDER it (`<id>/meetings/2026/10/kickoff.meeting.md`), which the longest-prefix match
|
|
113
|
+
// above can never see because its own storage.path is elsewhere. One place decides, so a
|
|
114
|
+
// commit, an event and a surface agree on whose record a path is.
|
|
115
|
+
if (rest.endsWith('/' + entry) && rest.indexOf('/') === rest.length - entry.length - 1) return { collection: best.name, id: rest.slice(0, -(entry.length + 1)) };
|
|
116
|
+
return placedChildAt(descriptors, best.name, rest);
|
|
112
117
|
}
|
|
113
118
|
const id = idFromRecordPath(best, rest);
|
|
114
119
|
return id === null ? null : { collection: best.name, id };
|