@gitdocket/core 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,18 +1,39 @@
1
1
  {
2
2
  "name": "@gitdocket/core",
3
- "version": "0.0.0",
4
- "description": "Bootstrap reservation for GitDocket core; stable releases begin at 0.1.0.",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "The typed file-and-graph engine behind GitDocket.",
5
6
  "license": "Apache-2.0",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/GitDocket/gitdocket.git",
9
10
  "directory": "packages/core"
10
11
  },
11
- "homepage": "https://github.com/GitDocket/gitdocket#readme",
12
- "files": ["README.md"],
12
+ "homepage": "https://gitdocket.com",
13
+ "engines": {
14
+ "bun": ">=1.3.14"
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.ts",
18
+ "./cache": "./src/cache.ts",
19
+ "./orientation": "./src/orientation.ts",
20
+ "./overview": "./src/overview.ts"
21
+ },
22
+ "files": [
23
+ "src/**/*.ts",
24
+ "src/**/*.json",
25
+ "!src/**/*.test.ts"
26
+ ],
13
27
  "publishConfig": {
14
28
  "access": "public",
15
- "registry": "https://registry.npmjs.org/",
16
- "tag": "bootstrap"
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "dependencies": {
32
+ "remark-frontmatter": "^5.0.0",
33
+ "remark-parse": "^11.0.0",
34
+ "unified": "^11.0.5",
35
+ "unist-util-visit": "^5.1.0",
36
+ "yaml": "^2.9.0",
37
+ "zod": "^4.5.4"
17
38
  }
18
39
  }
package/src/bundle.ts ADDED
@@ -0,0 +1,127 @@
1
+ // Bundle loading: FileStore → parsed concept graph with ID resolution
2
+ // (aliases included), duplicate detection, and derived readiness.
3
+
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { CONFIG_FILENAME, type DocketConfig, parseConfig } from "./config";
7
+ import { type FileStore, LocalFileStore } from "./filestore";
8
+ import {
9
+ type Concept,
10
+ type Decision,
11
+ type Diagnostic,
12
+ parseConcept,
13
+ type WorkItem,
14
+ } from "./parse";
15
+ import { buildSchemas } from "./schema";
16
+ import { byManualOrder, isReady, isStatus, type Status } from "./states";
17
+
18
+ export interface Bundle {
19
+ config: DocketConfig;
20
+ concepts: Concept[];
21
+ workItems: WorkItem[];
22
+ decisions: Decision[];
23
+ diagnostics: Diagnostic[];
24
+ /** Resolve a work item or decision by id — aliases included. */
25
+ byId(id: string): WorkItem | Decision | undefined;
26
+ statusById: ReadonlyMap<string, Status>;
27
+ /** Tasks that are `todo` with every dependency `done`. Derived, never stored. */
28
+ readyIds(): string[];
29
+ }
30
+
31
+ /**
32
+ * The canonical ready queue used by every surface, including bare
33
+ * `docket task start`. Readiness comes from the bundle; manual rank and
34
+ * priority supply the user-controlled order, with task ID as the stable
35
+ * fallback.
36
+ */
37
+ export function readyWorkItems(bundle: Bundle): WorkItem[] {
38
+ return bundle
39
+ .readyIds()
40
+ .map((id) => bundle.byId(id))
41
+ .filter((item): item is WorkItem => item?.kind === "work")
42
+ .sort((a, z) => byManualOrder(a.fm, z.fm));
43
+ }
44
+
45
+ export async function loadBundle(
46
+ store: FileStore,
47
+ config: DocketConfig,
48
+ ): Promise<Bundle> {
49
+ const schemas = buildSchemas(config);
50
+ const concepts: Concept[] = [];
51
+ const diagnostics: Diagnostic[] = [];
52
+
53
+ for (const path of await store.list()) {
54
+ const parsed = parseConcept(path, await store.read(path), schemas);
55
+ diagnostics.push(...parsed.diagnostics);
56
+ if (parsed.concept) concepts.push(parsed.concept);
57
+ }
58
+
59
+ const workItems = concepts.filter((c): c is WorkItem => c.kind === "work");
60
+ const decisions = concepts.filter(
61
+ (c): c is Decision => c.kind === "decision",
62
+ );
63
+
64
+ const index = new Map<string, WorkItem | Decision>();
65
+ for (const item of [...workItems, ...decisions]) {
66
+ for (const id of [item.fm.id, ...item.fm.aliases]) {
67
+ const existing = index.get(id);
68
+ if (existing) {
69
+ diagnostics.push({
70
+ path: item.path,
71
+ message: `duplicate id ${id} (also in ${existing.path})`,
72
+ severity: "error",
73
+ });
74
+ } else {
75
+ index.set(id, item);
76
+ }
77
+ }
78
+ }
79
+
80
+ const statusById = new Map<string, Status>();
81
+ for (const item of workItems) {
82
+ if (isStatus(item.fm.status)) {
83
+ for (const id of [item.fm.id, ...item.fm.aliases])
84
+ statusById.set(id, item.fm.status);
85
+ }
86
+ }
87
+
88
+ return {
89
+ config,
90
+ concepts,
91
+ workItems,
92
+ decisions,
93
+ diagnostics,
94
+ byId: (id) => index.get(id),
95
+ statusById,
96
+ readyIds: () =>
97
+ workItems
98
+ .filter((w) => w.fm.type === "Task")
99
+ .filter((w) => isReady(w.fm.status, w.fm.depends_on, statusById))
100
+ .map((w) => w.fm.id),
101
+ };
102
+ }
103
+
104
+ /** Walk upward from `start` to the nearest directory containing docket.yaml. */
105
+ export async function findRepoRoot(start: string): Promise<string | undefined> {
106
+ let dir = start;
107
+ for (;;) {
108
+ const found = await readFile(join(dir, CONFIG_FILENAME), "utf8").then(
109
+ () => true,
110
+ () => false,
111
+ );
112
+ if (found) return dir;
113
+ const parent = join(dir, "..");
114
+ if (parent === dir) return undefined;
115
+ dir = parent;
116
+ }
117
+ }
118
+
119
+ /** Convenience: load the bundle of a repo checkout from its docket.yaml. */
120
+ export async function loadRepo(repoRoot: string): Promise<Bundle> {
121
+ const configSource = await readFile(
122
+ join(repoRoot, CONFIG_FILENAME),
123
+ "utf8",
124
+ ).catch(() => undefined);
125
+ const config = parseConfig(configSource);
126
+ return loadBundle(new LocalFileStore(join(repoRoot, config.bundle)), config);
127
+ }