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
|
@@ -6,7 +6,7 @@ refusals that keep that from looping, lying, or carrying private data somewhere
|
|
|
6
6
|
|
|
7
7
|
The whole design turns on one asymmetry. Two sessions in the same tree are conflict-BLIND, and so are
|
|
8
8
|
two sessions in one conversation: **the second write wins and nobody is told.** Sessions are
|
|
9
|
-
|
|
9
|
+
git worktrees' twin — observed, never stored, and the registry is the authority rather than anyone's
|
|
10
10
|
memory of it.
|
|
11
11
|
|
|
12
12
|
| the question | read |
|
|
@@ -175,10 +175,9 @@ A coordinator spans repos by construction, and one of them may publish.
|
|
|
175
175
|
records it may not message a publishing session at all, whatever it means to say. This is what
|
|
176
176
|
keeps rule 1 safe after compaction, when it can no longer recall precisely what it read.
|
|
177
177
|
6. ⚠ **`cwd` is not the repo, and one repo is not one tree.** Harnesses put worktrees *inside* the
|
|
178
|
-
repo or *under the home directory* depending on the harness
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
worktrees` is the instrument.
|
|
178
|
+
repo or *under the home directory* depending on the harness. A `cwd` under a harness's own
|
|
179
|
+
worktree root is still that repo and carries its full boundary — so a path-prefix test against
|
|
180
|
+
the primary root gets it wrong in the dangerous direction. `git worktree list` is the instrument.
|
|
182
181
|
|
|
183
182
|
## not stepping on your own toes
|
|
184
183
|
|
|
@@ -160,12 +160,10 @@ Skills ship with modules and are read by any operator on any machine:
|
|
|
160
160
|
renamed verb or a changed limit in a skill sends every future session down the old path
|
|
161
161
|
confidently. When you catch a skill lying, fixing it is part of the task you are on, not a
|
|
162
162
|
follow-up.
|
|
163
|
-
- **Give it a
|
|
163
|
+
- **Give it a mechanical check, and know what one cannot cover.** Something must pin the
|
|
164
164
|
mechanical half: the script the skill names runs, the record it promises appears, the path it
|
|
165
|
-
files to exists
|
|
166
|
-
|
|
167
|
-
`dt list proofs --missing` names every artifact nobody claimed anything about. ⚠ **A
|
|
168
|
-
green proof is not evidence the skill TEACHES.** Whether a fresh session finds it, loads it and
|
|
165
|
+
files to exists — in a workspace with a proofs extension, that is a `proofs` record, written
|
|
166
|
+
in the same commit. ⚠ **A green check is not evidence the skill TEACHES.** Whether a fresh session finds it, loads it and
|
|
169
167
|
does the job right is the eval layer: real tasks, blind sessions, a scoring sheet — a procedure,
|
|
170
168
|
never something the engine runs.
|
|
171
169
|
- **Retire what nothing loads.** A skill nobody uses still costs its line in every session's index.
|
package/src/api.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The typed contract of `import … from 'dreamteamer'` (src/api.js). It re-exports the record half
|
|
2
|
+
// (`dreamteamer/records`, src/records-api.d.ts) and adds the workspace half. A test pins each runtime
|
|
3
|
+
// export list to its declaration, so a name added to one and not the other fails the suite.
|
|
4
|
+
|
|
5
|
+
export * from './records-api.js';
|
|
6
|
+
import type { Fields, Descriptor, Descriptors, Manifest, Store } from './records-api.js';
|
|
7
|
+
|
|
8
|
+
// ---- the workspace and extensions ------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface Workspace {
|
|
11
|
+
/** absolute path of the workspace root */
|
|
12
|
+
root: string;
|
|
13
|
+
/** the workspace package.json */
|
|
14
|
+
pkg: { name?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string>; dreamteamer?: Record<string, any>; [k: string]: unknown };
|
|
15
|
+
/** the ACTIVATED extensions, present on a handle from `openWorkspace` */
|
|
16
|
+
extensions?: LoadedExtension[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What an extension's `activate(dt)` returns. Every key is optional. */
|
|
20
|
+
export interface Contribution {
|
|
21
|
+
commands?: Record<string, { usage?: string; run(ws: Workspace, argv: string[]): number | void | Promise<number | void> }>;
|
|
22
|
+
sourceKinds?: (string | { kind: string; exclude?: string[] })[];
|
|
23
|
+
analyze?(draft: CompileDraft): { errors?: string[]; warnings?: string[]; notes?: string[] } | void;
|
|
24
|
+
harnesses?: Record<string, (ctx: HarnessContext) => { blocks?: Record<string, string | null>; summary?: string } | void>;
|
|
25
|
+
orientation?: string | ((ctx: { entries: Map<string, Entry> }) => string);
|
|
26
|
+
hooks?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export type Activate = (dt: typeof import('./api.js')) => Contribution | Promise<Contribution>;
|
|
29
|
+
export interface LoadedExtension {
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
commands: NonNullable<Contribution['commands']>;
|
|
33
|
+
sourceKinds: { kind: string; exclude: string[]; extension: string }[];
|
|
34
|
+
analyze: Contribution['analyze'] | null;
|
|
35
|
+
harnesses: NonNullable<Contribution['harnesses']>;
|
|
36
|
+
orientation: Contribution['orientation'] | null;
|
|
37
|
+
hooks: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
export type Entry = { sources: { path: string; hash: string }[]; bytes: Buffer };
|
|
40
|
+
export interface CompileDraft {
|
|
41
|
+
/** runtime-relative path → staged entry; read-only */
|
|
42
|
+
readonly entries: Map<string, Entry>;
|
|
43
|
+
/** the FINAL merged descriptors */
|
|
44
|
+
readonly descriptors: Descriptors;
|
|
45
|
+
readonly modules: { id: string; name: string; root: string; channel: 'inline' | 'git' | 'npm' }[];
|
|
46
|
+
/** names only — never values */
|
|
47
|
+
readonly declaredVars: string[];
|
|
48
|
+
readonly declaredEnv: string[];
|
|
49
|
+
readonly previousManifest: Manifest | null;
|
|
50
|
+
/** parse one staged YAML entry, with its source path in any error */
|
|
51
|
+
parse(runtimePath: string): any;
|
|
52
|
+
}
|
|
53
|
+
export interface HarnessContext {
|
|
54
|
+
entries: Map<string, Entry>;
|
|
55
|
+
version: string;
|
|
56
|
+
collections: { name: string; generated: boolean; systemGroup: boolean; description: string; module: string; sensitive: boolean; sensitiveFields: string[] }[];
|
|
57
|
+
modules: { id: string; title: string; description: string; path: string }[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const EXTENSION_API: 1;
|
|
61
|
+
export function openWorkspace(start?: string): Promise<Workspace & { extensions: LoadedExtension[] }>;
|
|
62
|
+
export function findWorkspace(start?: string): Workspace;
|
|
63
|
+
export function declaredExtensions(ws: Workspace): { name: string; version: string; dir: string; entry: string }[];
|
|
64
|
+
export const engineBin: string;
|
|
65
|
+
export const engineRoot: string;
|
|
66
|
+
|
|
67
|
+
export function envContext(ws: Workspace): unknown;
|
|
68
|
+
export function renderTemplate(template: string, ctx: unknown): string;
|
|
69
|
+
export function parseEnvValues(text: string): Map<string, string>;
|
|
70
|
+
export function satisfies(version: string, range: string): boolean | null;
|
|
71
|
+
|
|
72
|
+
// ---- the compiler, schema and module operations --------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/** throws CompileError on a bad source; prints its summary; returns 0 */
|
|
75
|
+
export function compile(ws: Workspace): 0;
|
|
76
|
+
export function staleness(root: string): { compiled: boolean; stale: string[]; manifest?: Manifest; message?: string };
|
|
77
|
+
export function warnIfStale(root: string): ReturnType<typeof staleness>;
|
|
78
|
+
export function discoverModules(root: string, pkg: Workspace['pkg']): { modules: { name: string; root: string; channel: string }[]; shadows: unknown[]; disabledModules: string[] };
|
|
79
|
+
export class CompileError extends Error {}
|
|
80
|
+
export const KINDS: readonly string[];
|
|
81
|
+
export const MANAGED_BLOCKS: readonly { id: 'orientation' | 'instructions'; begin: string; end: string }[];
|
|
82
|
+
type SchemaOp = (ws: Workspace, store: Store, ...args: any[]) => any;
|
|
83
|
+
export const createCollection: SchemaOp, removeCollection: SchemaOp, renameCollection: SchemaOp, moveCollection: SchemaOp, setCollectionScalars: SchemaOp;
|
|
84
|
+
export const addField: SchemaOp, updateField: SchemaOp, removeField: SchemaOp, removeFieldPlan: SchemaOp, renameField: SchemaOp, renameFieldPlan: SchemaOp;
|
|
85
|
+
export function fieldDef(store: Store, flags: Record<string, unknown>, collection: string): any;
|
|
86
|
+
export function statedKeywords(flags: Record<string, unknown>): any;
|
|
87
|
+
export const saveUiView: SchemaOp, removeUiView: SchemaOp;
|
|
88
|
+
export const createModule: SchemaOp, setModule: SchemaOp, renameModule: SchemaOp, removeModule: SchemaOp;
|
|
89
|
+
export const createSkill: SchemaOp, refuseHandAuthored: SchemaOp, removeEntity: SchemaOp, renameEntity: SchemaOp, setEntityFrontmatter: SchemaOp;
|
|
90
|
+
export function init(opts?: { flags?: Record<string, string> }): 0;
|
|
91
|
+
export function ensureRepo(ws: Workspace, id: string): { path: string; cloned: boolean };
|
|
92
|
+
export function ensureAllRepos(ws: Workspace): { path: string; cloned: boolean }[];
|
|
93
|
+
export function listRepos(ws: Workspace): { id: string; path: string; present: boolean; unresolved?: string }[];
|
|
94
|
+
export function installClone(ws: Workspace, url: string, name?: string): number;
|
|
95
|
+
|
|
96
|
+
// ---- the checkout -------------------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export function describeCheckout(root: string, git?: (args: string[], cwd: string) => string): { root: string; kind: 'primary' | 'linked'; gitDir: string; commonDir: string; primary: string; insideRoot: boolean };
|
|
99
|
+
export function installCommand(ws: Workspace, argv: string[], opts?: { open?: (at: string) => Promise<Workspace> }): Promise<number>;
|
|
100
|
+
export function resolveNpm(execPath?: string, env?: NodeJS.ProcessEnv): string | null;
|
|
101
|
+
export function childEnv(): NodeJS.ProcessEnv;
|
|
102
|
+
export function readHookInput(stdinText: string): { cwd: string | null; name: string | null; raw: Record<string, unknown> };
|
|
103
|
+
export function readStdin(isTTY?: boolean): string;
|
|
104
|
+
|
|
105
|
+
// ---- CLI helpers ------------------------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/** write `text` plus a trailing newline, synchronously, looping on short writes — safe before process.exit */
|
|
108
|
+
export function emit(text: string, fd?: number): void;
|
|
109
|
+
export function parseArgs(argv: string[]): { flags: Record<string, string | boolean | string[]>; pos: string[] };
|
package/src/api.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The PUBLIC API — `import … from 'dreamteamer'`. Everything a surface (the VS Code extension, the
|
|
2
|
+
// mobile app) or an extension (http, workflows, notebooklm) may call, and nothing else: package.json
|
|
3
|
+
// `exports` hides `src/*`, so an internal file can be split, renamed or deleted without a cross-repo
|
|
4
|
+
// break. That break used to be the rule — the extension imported fifteen internal files by path, and
|
|
5
|
+
// deleting one took `activate()` down before the tree view existed.
|
|
6
|
+
//
|
|
7
|
+
// Grouped by task. Each name is the ONE implementation of its operation; nothing here wraps or
|
|
8
|
+
// re-implements. `api.d.ts` beside this file is the typed contract, and a test pins the two together.
|
|
9
|
+
//
|
|
10
|
+
// Importing this module prints nothing, writes nothing, binds no port and touches no network.
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import * as self from './api.js';
|
|
14
|
+
import { findWorkspace } from './workspace.js';
|
|
15
|
+
import { loadExtensions } from './extensions.js';
|
|
16
|
+
import { KINDS } from './compile.js';
|
|
17
|
+
import { DERIVED_KINDS } from './runtime.js';
|
|
18
|
+
import { KNOWN_HARNESSES } from './harnesses.js';
|
|
19
|
+
import { CORE_VERBS } from './cli.js';
|
|
20
|
+
import { installCommand as installCheckout } from './checkout.js';
|
|
21
|
+
|
|
22
|
+
// Everything the record half exports (the browser-safe entry, `dreamteamer/records`), then the rest.
|
|
23
|
+
export * from './records-api.js';
|
|
24
|
+
|
|
25
|
+
// ---- the workspace ------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The workspace at (or above) `start`, with its declared extensions ACTIVATED against this engine.
|
|
29
|
+
* The handle every other call takes: `{ root, pkg, extensions }`. Performs no compile, install,
|
|
30
|
+
* network call or write, and never changes the process cwd.
|
|
31
|
+
*/
|
|
32
|
+
export async function openWorkspace(start = process.cwd()) {
|
|
33
|
+
const ws = findWorkspace(start);
|
|
34
|
+
ws.extensions = await loadExtensions(ws, self, { verbs: CORE_VERBS, kinds: [...KINDS, ...DERIVED_KINDS], harnesses: KNOWN_HARNESSES });
|
|
35
|
+
return ws;
|
|
36
|
+
}
|
|
37
|
+
export { findWorkspace };
|
|
38
|
+
export { EXTENSION_API, declaredExtensions } from './extensions.js';
|
|
39
|
+
|
|
40
|
+
/** The engine's own CLI entry — what a tool spawns to run `dt` in another checkout. */
|
|
41
|
+
export const engineBin = fileURLToPath(new URL('../bin/dreamteamer.js', import.meta.url));
|
|
42
|
+
export const engineRoot = path.dirname(path.dirname(engineBin));
|
|
43
|
+
|
|
44
|
+
// ---- values the workspace half adds ---------------------------------------------------------------
|
|
45
|
+
export { envContext, renderTemplate, parseEnvValues } from './env-vars.js';
|
|
46
|
+
export { satisfies } from './semver.js';
|
|
47
|
+
|
|
48
|
+
// ---- the compiler, schema and module operations ---------------------------------------------------
|
|
49
|
+
export { compile, staleness, warnIfStale, discoverModules, CompileError, KINDS } from './compile.js';
|
|
50
|
+
export { MANAGED_BLOCKS } from './harnesses.js';
|
|
51
|
+
export {
|
|
52
|
+
createCollection, removeCollection, renameCollection, moveCollection, setCollectionScalars,
|
|
53
|
+
addField, updateField, removeField, removeFieldPlan, renameField, renameFieldPlan, fieldDef, statedKeywords,
|
|
54
|
+
saveUiView, removeUiView,
|
|
55
|
+
createModule, setModule, renameModule, removeModule,
|
|
56
|
+
createSkill, refuseHandAuthored, removeEntity, renameEntity, setEntityFrontmatter,
|
|
57
|
+
} from './schema-ops.js';
|
|
58
|
+
export { init, ensureRepo, ensureAllRepos, listRepos, installClone } from './init.js';
|
|
59
|
+
|
|
60
|
+
// ---- the checkout: which one this is, and how it becomes ready ---------------------------------
|
|
61
|
+
export { describeCheckout, resolveNpm, childEnv, readHookInput, readStdin } from './checkout.js';
|
|
62
|
+
|
|
63
|
+
/** `dt install` on the checkout `ws`, as the CLI runs it. The compile step reopens the workspace with
|
|
64
|
+
* `opts.open` — `openWorkspace` unless a caller injects another — so the extensions npm just
|
|
65
|
+
* installed take part in that compile. The one name here that supplies a default rather than
|
|
66
|
+
* re-exporting: the checkout layer cannot import the opener that activates extensions. */
|
|
67
|
+
export const installCommand = (ws, argv, opts = {}) => installCheckout(ws, argv, { open: openWorkspace, ...opts });
|
|
68
|
+
|
|
69
|
+
// ---- CLI helpers an extension command reuses -----------------------------------------------------
|
|
70
|
+
export { emit, parseArgs } from './collections-cli.js';
|
package/src/check.js
CHANGED
|
@@ -10,6 +10,7 @@ import { NO_RUNTIME, loadDescriptors, runtimeDir, namespaces as compiledNamespac
|
|
|
10
10
|
import { parseRef } from './namespace.js';
|
|
11
11
|
import { refTargetsOf, refIsSoft } from './ref.js';
|
|
12
12
|
import { relationsOf, expectedMirrors } from './relations.js';
|
|
13
|
+
import { placementOf, placedRecords, ownerIdOf, symlinkedChildRoots } from './placement.js';
|
|
13
14
|
|
|
14
15
|
export function check({ root }) {
|
|
15
16
|
const RUNTIME = runtimeDir(root);
|
|
@@ -34,16 +35,19 @@ export function check({ root }) {
|
|
|
34
35
|
|
|
35
36
|
// ---- index all records: collection -> Map<id, filePath> ------------------------
|
|
36
37
|
const index = new Map();
|
|
38
|
+
// placed collections only: id -> the parent id its file was FOUND under (null = the fallback root)
|
|
39
|
+
const observed = new Map();
|
|
37
40
|
const strays = [];
|
|
38
41
|
// declared here rather than beside the validation pass: indexing can itself produce a
|
|
39
42
|
// finding (an unreachable data root, below) before a single record is read.
|
|
40
43
|
const violations = [];
|
|
44
|
+
const dirOf = (d) => path.join(d.storage.base === 'runtime' ? RUNTIME : root, d.storage.path);
|
|
41
45
|
for (const [name, d] of descriptors) {
|
|
42
46
|
const ids = new Map();
|
|
43
47
|
index.set(name, ids);
|
|
44
48
|
// runtime-based (knowhow/meta) collections are read from the COMPILED runtime —
|
|
45
49
|
// their sources may live in any module; .dreamteamer is the merged read surface
|
|
46
|
-
const dir =
|
|
50
|
+
const dir = dirOf(d);
|
|
47
51
|
// An unreachable data ROOT is a finding, not a skip: a collection whose module clone is
|
|
48
52
|
// missing otherwise reports zero records and a clean check — a silent success. An EMPTY
|
|
49
53
|
// directory stays fine (a module with no records yet is normal); only a missing owning
|
|
@@ -53,13 +57,46 @@ export function check({ root }) {
|
|
|
53
57
|
violations.push({ file: d.storage.path, msg: `collection "${name}" is owned by ${d.storage.repo}, which is not present — every record in it is unreadable` });
|
|
54
58
|
continue;
|
|
55
59
|
}
|
|
60
|
+
const under = placementOf(d);
|
|
61
|
+
if (under) {
|
|
62
|
+
// ONE logical collection across the fallback root and every parent folder — the same walk
|
|
63
|
+
// the store indexes with (src/placement.js), so the two cannot disagree about which files
|
|
64
|
+
// are records. The first file to claim an id keeps it; every later one is a violation,
|
|
65
|
+
// because a `get` that silently answered from whichever folder sorted later is the
|
|
66
|
+
// failure this report exists to make visible.
|
|
67
|
+
const seen = new Map();
|
|
68
|
+
observed.set(name, seen);
|
|
69
|
+
const parentDir = dirOf(descriptors.get(under.collection));
|
|
70
|
+
// a child root that is (or sits behind) a symlink is not read — whatever it points at is not
|
|
71
|
+
// this parent's folder — and it is named here rather than silently skipped
|
|
72
|
+
for (const link of symlinkedChildRoots(under, parentDir)) {
|
|
73
|
+
violations.push({ file: rel(link), msg: `is a symlink — ${name} records are read only from real folders inside ${under.collection} records; whatever this points at is not indexed. Replace it with a real folder.` });
|
|
74
|
+
}
|
|
75
|
+
const onLink = (p) => violations.push({ file: rel(p), msg: `is a symlink inside a ${under.collection} record's folder — nothing behind it is read as a ${name} record, and nothing is written through it. Replace it with a real folder or file.` });
|
|
76
|
+
for (const r of placedRecords(d, dir, parentDir, onLink)) {
|
|
77
|
+
if (ids.has(r.id)) {
|
|
78
|
+
violations.push({ file: rel(r.file), msg: `collection "${name}" holds the id "${r.id}" twice — ${rel(ids.get(r.id))} and ${rel(r.file)}. Remove one.` });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
ids.set(r.id, r.file);
|
|
82
|
+
seen.set(r.id, r.parentId);
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
56
86
|
if (!fs.existsSync(dir)) continue;
|
|
57
87
|
const shape = d.storage.shape ?? 'file';
|
|
58
88
|
if (shape === 'folder') {
|
|
89
|
+
// a FILE record at the root of a folder-shape collection is the state a collection is in
|
|
90
|
+
// right after its shape changed — named as such, with the verb that finishes the change
|
|
91
|
+
const asFile = { ...d, storage: { ...d.storage, shape: 'file' } };
|
|
59
92
|
for (const entry of fs.readdirSync(dir).sort()) {
|
|
60
93
|
if (entry.startsWith('.')) continue;
|
|
61
94
|
const p = path.join(dir, entry);
|
|
62
|
-
if (!fs.statSync(p).isDirectory()) {
|
|
95
|
+
if (!fs.statSync(p).isDirectory()) {
|
|
96
|
+
const legacy = idFromRecordPath(asFile, entry) !== null;
|
|
97
|
+
strays.push({ collection: name, file: rel(p), note: legacy ? `a file-shape record in a folder-shape collection — dreamteamer relocate ${name} moves it to ${entry.split('.')[0]}/${d.storage.entry}` : undefined });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
63
100
|
const main = path.join(p, d.storage.entry ?? 'SKILL.md');
|
|
64
101
|
if (fs.existsSync(main)) ids.set(entry, main);
|
|
65
102
|
else strays.push({ collection: name, file: rel(p), note: `missing entry file ${d.storage.entry}` });
|
|
@@ -127,6 +164,24 @@ export function check({ root }) {
|
|
|
127
164
|
checkRef(file, fieldPath, value, target, softTargets, soft);
|
|
128
165
|
}
|
|
129
166
|
}
|
|
167
|
+
// ---- placement: the FIELD is the intended owner, the FOLDER is observed placement ---
|
|
168
|
+
// Reported, never repaired: a record found under the wrong company is either a hand move
|
|
169
|
+
// (the field is right, run relocate) or a hand edit of the field (the folder is right, set
|
|
170
|
+
// it back) and only a person knows which. A malformed owner value is skipped here — the
|
|
171
|
+
// reference check above has already named it, and "placed under X but owner is empty"
|
|
172
|
+
// on top of that would be a second report of one typo.
|
|
173
|
+
const under = placementOf(d);
|
|
174
|
+
if (under) {
|
|
175
|
+
const raw = fields[under.field];
|
|
176
|
+
const wellFormed = raw == null || raw === '' || parseRef(raw, namespaces);
|
|
177
|
+
const want = ownerIdOf(fields, under, (v) => parseRef(v, namespaces));
|
|
178
|
+
const got = observed.get(name).get(id);
|
|
179
|
+
if (wellFormed && want !== got) {
|
|
180
|
+
const where = got ? `under ${under.collection}/${got}` : `in its own root (${d.storage.path})`;
|
|
181
|
+
const should = want ? `${under.field} is ${under.collection}/${want}` : `${under.field} is empty`;
|
|
182
|
+
flag(file, `placed ${where} but ${should} — the file is not where its owner puts it. Run: dreamteamer relocate ${name}/${id}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
130
185
|
parsed.get(name).set(id, fields);
|
|
131
186
|
}
|
|
132
187
|
}
|