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.
Files changed (43) hide show
  1. package/README.md +47 -153
  2. package/collections/collections.collection.yaml +26 -9
  3. package/package.json +14 -2
  4. package/skills/using-dreamteamer/SKILL.md +16 -10
  5. package/skills/using-dreamteamer/references/before-you-build.md +4 -3
  6. package/skills/using-dreamteamer/references/collections.md +71 -1
  7. package/skills/using-dreamteamer/references/commands.md +3 -3
  8. package/skills/using-dreamteamer/references/data-modeling.md +9 -1
  9. package/skills/using-dreamteamer/references/extensions.md +60 -0
  10. package/skills/using-dreamteamer/references/records.md +17 -0
  11. package/skills/using-dreamteamer/references/sessions.md +4 -5
  12. package/skills/using-dreamteamer/references/skills.md +3 -5
  13. package/src/api.d.ts +109 -0
  14. package/src/api.js +70 -0
  15. package/src/check.js +57 -2
  16. package/src/checkout.js +105 -330
  17. package/src/cli.js +90 -274
  18. package/src/collections-cli.js +16 -165
  19. package/src/commit.js +7 -0
  20. package/src/compile.js +211 -155
  21. package/src/events.js +7 -2
  22. package/src/extensions.js +135 -0
  23. package/src/filter.js +1 -1
  24. package/src/harnesses.js +82 -135
  25. package/src/init.js +9 -3
  26. package/src/placement.js +209 -0
  27. package/src/records-api.d.ts +103 -0
  28. package/src/records-api.js +40 -0
  29. package/src/runtime.js +2 -2
  30. package/src/schema-ops.js +26 -9
  31. package/src/store.js +396 -35
  32. package/collections/containers.collection.yaml +0 -81
  33. package/collections/images.collection.yaml +0 -46
  34. package/collections/proofs.collection.yaml +0 -96
  35. package/skills/using-dreamteamer/references/exporting.md +0 -53
  36. package/skills/using-dreamteamer/references/proofs.md +0 -435
  37. package/skills/using-dreamteamer/references/worktrees.md +0 -235
  38. package/src/container-archive.js +0 -356
  39. package/src/containers.js +0 -635
  40. package/src/export-notebooklm.js +0 -502
  41. package/src/land.js +0 -743
  42. package/src/prove.js +0 -1922
  43. 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
- // ⚠ RE-EXPORTED, NOT RE-IMPLEMENTED. `engineVersion` moved to the boundary layer so `prove` can
25
- // stamp a ledger row without importing the compiler (that edge was a real, if latent, cycle).
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
- * ⚠ `proofs/fixtures/` IS RECORDS, NOT PROOFS. It holds the store a `writes` proof runs against —
398
- * laid onto a throwaway worktree by `dt prove`, mirroring the workspace root — so it is not a
399
- * compiled source at all.
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
- * ONE predicate, because BOTH enumerations of a kind directory have to agree about it and they are
402
- * 900 lines apart. The stager alone made compile read `data/notes/x.note.md` as a proof and refuse
403
- * the whole module; the staleness scan alone then reported every fixture file "(new, uncompiled)"
404
- * on every single command, for ever, with no compile able to clear it.
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
- * @param {string} kind the source kind being enumerated
407
- * @param {string} relFromKind the entry's path RELATIVE to the kind directory, '/'-separated
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
- const isProofFixture = (kind, relFromKind) => kind === 'proofs' && (relFromKind === 'fixtures' || relFromKind.startsWith('fixtures/'));
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([...KINDS, ...NON_SOURCE_DIRS, ...declaredIgnore]);
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 disabledModules = new Set((pkg?.dreamteamer?.disable ?? []).filter((d) => typeof d === 'string' && !d.includes('/')));
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 (disabledModules.has(name)) { disabledHits.add(name); return; }
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({ root, pkg }) {
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 KINDS) {
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 contributed = new Set();
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: ${KINDS.join(', ')}
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 KINDS) {
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 (isProofFixture(kind, name)) continue;
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
- contributed.add(source.name);
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
- contributed.add(source.name);
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
- contributed.add(source.name);
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
- contributed.add(source.name);
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
- contributed.add(source.name);
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 (contributed.has(source.name)) continue;
1013
- if (KINDS.some((k) => fs.existsSync(kindDir(source.root, k)))) continue;
1014
- 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`);
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([...KINDS, ...DERIVED_KINDS, 'repos']);
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 = [...KINDS, ...DERIVED_KINDS];
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
- // ---- proof validation --------------------------------------------------------------
1684
- // A proof declares behaviour the ENGINE judges, so every key in one is a value the engine
1685
- // interprets — which by the rule stated above the ui-view block makes all of them compile's to
1686
- // validate. The two silent failures this refuses, both measured in the spike:
1687
- // `about: skills/greter` — names no artifact, so the proof proves nothing and says so nowhere.
1688
- // `where: { statuz: … }` — an unknown key fails CLOSED in `matchesFilter`, so a `count`
1689
- // expectation over it passes or fails FOREVER for a reason no output names.
1690
- // The judgement itself lives in `prove.js` (unit-tested against a hand-built descriptor set);
1691
- // this block is the wiring, and it is deliberately paid for only when a proof exists.
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) merged.set(doc.name, doc);
1804
+ if (doc?.name) descriptors.set(doc.name, doc);
1708
1805
  }
1709
- const proofCtx = { descriptors: merged, declaredVars, moduleEnv: new Set(declaredEnv.keys()), artifacts: proofArtifacts.all };
1710
- for (const [rt, e] of proofEntries) {
1711
- const proof = loadSource(e.bytes.toString('utf8'), e.sources[0].path);
1712
- const errs = validateProofShape(proof, proofCtx);
1713
- if (errs.length) fail(errs.map((m) => `${rt}: ${m}`).join('\n '));
1714
- // ⚠ WARNINGS, not errors (R17). A step is a shell string, so a brace nobody substitutes is
1715
- // as likely to be `awk '{print}'` as a typo'd `{recrod}` — and `substitute` refusing both
1716
- // at run time was measured to kill four correct steps. Naming the token here is the whole
1717
- // net that remains: it costs a line, and the proof still compiles and still runs.
1718
- for (const w of stepWarnings(proof)) console.warn(`⚠ ${rt}: ${w}`);
1719
- for (const ref of proof?.about ?? []) provenRefs.add(String(ref));
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
- for (const kind of [...KINDS, ...DERIVED_KINDS]) fs.rmSync(path.join(RUNTIME, kind), { recursive: true, force: true });
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) => KINDS.some((k) => fs.existsSync(path.join(s.root, k))));
1753
- const anyNested = sources.some((s) => KINDS.some((k) => fs.existsSync(path.join(s.root, 'system', k))));
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 = KINDS.filter((k) => counts[k]).map((k) => `${counts[k]} ${k}${k === 'collections' && mergedCount ? ` (${mergedCount} merged)` : ''}`).join(', ');
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
- // ---- proof coverage, on EVERY compile ----------------------------------------
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.includes('/')));
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 (isProofFixture(kind, rel)) continue;
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
- if (!rest.endsWith('/' + entry)) return null;
111
- return { collection: best.name, id: rest.slice(0, -(entry.length + 1)) };
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 };