jg-plans 0.1.0 → 0.2.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/CHANGELOG.md +20 -0
- package/README.md +8 -8
- package/api/README.md +75 -12
- package/cli/README.md +24 -6
- package/dist/cli/arguments.js +18 -3
- package/dist/cli/commands.js +76 -17
- package/dist/cli/types.d.ts +3 -1
- package/dist/documents/fingerprints.d.ts +19 -0
- package/dist/documents/fingerprints.js +56 -0
- package/dist/documents/index.d.ts +2 -1
- package/dist/documents/index.js +1 -0
- package/dist/documents/milestones.js +44 -33
- package/dist/documents/plan.js +33 -68
- package/dist/documents/types.d.ts +22 -0
- package/dist/documents/utilities.d.ts +16 -2
- package/dist/documents/utilities.js +105 -4
- package/dist/edits/index.d.ts +2 -0
- package/dist/edits/index.js +2 -1
- package/dist/edits/preparations.d.ts +30 -0
- package/dist/edits/preparations.js +159 -0
- package/dist/edits/types.d.ts +14 -0
- package/dist/edits/utilities.d.ts +1 -0
- package/dist/edits/utilities.js +9 -2
- package/dist/errors/constants.d.ts +3 -1
- package/dist/errors/constants.js +4 -0
- package/dist/files/documents.d.ts +46 -0
- package/dist/files/documents.js +176 -0
- package/dist/files/index.d.ts +3 -1
- package/dist/files/index.js +2 -0
- package/dist/files/locks.d.ts +16 -0
- package/dist/files/locks.js +127 -0
- package/dist/files/mutations.d.ts +7 -3
- package/dist/files/mutations.js +32 -83
- package/dist/files/types.d.ts +19 -5
- package/dist/files/utilities.d.ts +4 -4
- package/dist/files/utilities.js +24 -7
- package/dist/index.d.ts +10 -8
- package/dist/index.js +5 -4
- package/dist/protocol/index.d.ts +4 -4
- package/dist/protocol/index.js +2 -2
- package/dist/protocol/types.d.ts +63 -0
- package/dist/protocol/types.js +53 -1
- package/docs/getting-started.md +4 -4
- package/docs/integrations.md +14 -14
- package/errors/README.md +6 -4
- package/examples/library.mjs +34 -2
- package/package.json +4 -3
- package/protocol/0.1/README.md +8 -2
package/dist/files/mutations.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { lstat, mkdir,
|
|
1
|
+
import { lstat, mkdir, rmdir, unlink } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { getErrorCode } from 'jg-exception';
|
|
4
|
-
import {
|
|
4
|
+
import { readBoundedFile } from 'jg-fs';
|
|
5
|
+
import { hashValue } from 'jg-crypto';
|
|
5
6
|
import { stringify } from 'yaml';
|
|
6
|
-
import { z } from 'zod';
|
|
7
7
|
import { parseMilestones, parsePlan } from "../documents/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { decodeSource, encodeSource, preparePlanUpdate } from "../edits/index.js";
|
|
9
9
|
import { planErrors } from "../errors/index.js";
|
|
10
|
-
import { CreatePlanOptionsSchema,
|
|
10
|
+
import { CreatePlanOptionsSchema, PlanUpdateSchema, } from "../protocol/index.js";
|
|
11
|
+
import { persistPrepared, readMutationSource, validateMutationOptions, writeExclusive, } from "./documents.js";
|
|
12
|
+
import { withPlanDocumentLocks } from "./locks.js";
|
|
11
13
|
const validateNewText = (value) => {
|
|
12
14
|
if (typeof value === 'string')
|
|
13
15
|
encodeSource(value);
|
|
@@ -20,19 +22,6 @@ const validateNewText = (value) => {
|
|
|
20
22
|
validateNewText(item);
|
|
21
23
|
}
|
|
22
24
|
};
|
|
23
|
-
const writeExclusive = async (root, path, bytes, owned) => {
|
|
24
|
-
await assertNoLinksBelow(root, path);
|
|
25
|
-
const handle = await open(path, 'wx');
|
|
26
|
-
try {
|
|
27
|
-
const stat = await handle.stat({ bigint: true });
|
|
28
|
-
owned.push({ path, dev: stat.dev, ino: stat.ino, bytes });
|
|
29
|
-
await handle.writeFile(bytes);
|
|
30
|
-
await handle.sync();
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
await handle.close();
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
25
|
/** Remove only files this call created and still recognizes, followed by the empty owned directory. */
|
|
37
26
|
const cleanupCreation = async (directory, identity, owned) => {
|
|
38
27
|
try {
|
|
@@ -81,6 +70,8 @@ const cleanupCreation = async (directory, identity, owned) => {
|
|
|
81
70
|
* - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
|
|
82
71
|
* - JG_PLANS_UNSUPPORTED_ENCODING: Document text is not valid UTF-8
|
|
83
72
|
* - JG_PLANS_DESTINATION_EXISTS: The plan destination already exists
|
|
73
|
+
* - JG_PLANS_LOCK_BUSY: The plan document is locked
|
|
74
|
+
* - JG_PLANS_LOCK_FAILED: The plan document lock operation failed
|
|
84
75
|
* - JG_PLANS_WRITE_FAILED: The plan document could not be written
|
|
85
76
|
*/
|
|
86
77
|
export const createPlan = async (options) => {
|
|
@@ -146,11 +137,17 @@ export const createPlan = async (options) => {
|
|
|
146
137
|
}
|
|
147
138
|
const owned = [];
|
|
148
139
|
try {
|
|
149
|
-
await
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
await withPlanDocumentLocks(directory, milestoneBytes === null ? ['plan'] : ['plan', 'milestones'], async (selected) => {
|
|
141
|
+
await writeExclusive(selected, join(selected, 'plan.md'), planBytes, owned);
|
|
142
|
+
if (milestoneBytes !== null)
|
|
143
|
+
await writeExclusive(selected, join(selected, 'milestones.md'), milestoneBytes, owned);
|
|
144
|
+
});
|
|
152
145
|
}
|
|
153
146
|
catch (cause) {
|
|
147
|
+
// Release failure can follow a durable write. Preserve its files and uncertainty.
|
|
148
|
+
if (getErrorCode(cause) === 'JG_PLANS_LOCK_FAILED' ||
|
|
149
|
+
getErrorCode(cause) === 'JG_PLANS_LOCK_BUSY')
|
|
150
|
+
throw cause;
|
|
154
151
|
const cleanupFailedPaths = await cleanupCreation(directory, identity, owned);
|
|
155
152
|
throw planErrors.create('writeFailed', {
|
|
156
153
|
cause,
|
|
@@ -161,15 +158,15 @@ export const createPlan = async (options) => {
|
|
|
161
158
|
const milestones = input.milestones === undefined ? null : parseMilestones(input.milestones);
|
|
162
159
|
return {
|
|
163
160
|
directory,
|
|
164
|
-
plan,
|
|
165
|
-
milestones,
|
|
161
|
+
plan: { ...plan, sha256: hashValue(planBytes) },
|
|
162
|
+
milestones: milestones === null ? null : { ...milestones, sha256: hashValue(milestoneBytes) },
|
|
166
163
|
diagnostics: [...plan.diagnostics, ...(milestones?.diagnostics ?? [])],
|
|
167
164
|
};
|
|
168
165
|
};
|
|
169
166
|
/**
|
|
170
|
-
* Apply
|
|
171
|
-
*
|
|
172
|
-
*
|
|
167
|
+
* Apply an explicit narrow patch under the shared selected-document lock.
|
|
168
|
+
* Guard optional observed bytes before preparation; unchanged output is never replaced.
|
|
169
|
+
* Nonparticipating editors retain a final comparison-to-rename race.
|
|
173
170
|
* @throws
|
|
174
171
|
* - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
|
|
175
172
|
* - JG_PLANS_READ_FAILED: The selected document could not be read
|
|
@@ -177,66 +174,18 @@ export const createPlan = async (options) => {
|
|
|
177
174
|
* - JG_PLANS_UNSAFE_EDIT: The requested edit is unsupported or ambiguous
|
|
178
175
|
* - JG_PLANS_SOURCE_CHANGED: The document changed before replacement
|
|
179
176
|
* - JG_PLANS_WRITE_FAILED: The plan document could not be written
|
|
177
|
+
* - JG_PLANS_LOCK_BUSY: The plan document is locked
|
|
178
|
+
* - JG_PLANS_LOCK_FAILED: The plan document lock operation failed
|
|
180
179
|
*/
|
|
181
180
|
export const updatePlan = async (directory, patch, options = {}) => {
|
|
181
|
+
validateMutationOptions(directory, options);
|
|
182
182
|
const validated = PlanUpdateSchema.safeParse(patch);
|
|
183
|
-
if (!validated.success
|
|
184
|
-
!z
|
|
185
|
-
.string()
|
|
186
|
-
.min(1)
|
|
187
|
-
.refine((value) => !value.includes('\0'))
|
|
188
|
-
.safeParse(directory).success ||
|
|
189
|
-
!ReadOptionsSchema.safeParse(options).success)
|
|
183
|
+
if (!validated.success)
|
|
190
184
|
throw planErrors.create('invalidInput');
|
|
191
185
|
const request = validated.data;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
before = await readBoundedFile(path, options.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
198
|
-
}
|
|
199
|
-
catch (cause) {
|
|
200
|
-
const reason = getErrorCode(cause);
|
|
201
|
-
throw planErrors.create('readFailed', { cause, ...(reason ? { data: { reason } } : {}) });
|
|
202
|
-
}
|
|
203
|
-
let source;
|
|
204
|
-
try {
|
|
205
|
-
source = decodeSource(before);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
throw planErrors.create('unsupportedEncoding');
|
|
209
|
-
}
|
|
210
|
-
const edits = request.document === 'plan' ? planEdits(source, request) : milestoneEdits(source, request);
|
|
211
|
-
const after = applyEdits(before, source, edits);
|
|
212
|
-
const text = decodeSource(after);
|
|
213
|
-
const observation = request.document === 'plan' ? parsePlan(text) : parseMilestones(text);
|
|
214
|
-
if (request.document === 'plan') {
|
|
215
|
-
const updated = parsePlan(text);
|
|
216
|
-
if (updated.diagnostics.some((item) => item.code === 'INVALID_YAML' || item.code === 'UNTERMINATED_FRONTMATTER'))
|
|
217
|
-
throw planErrors.create('unsafeEdit', { data: { reason: 'invalid-serialized-frontmatter' } });
|
|
218
|
-
for (const [key, value] of Object.entries(request.set))
|
|
219
|
-
if (!Object.is(updated.metadata[key], value))
|
|
220
|
-
throw planErrors.create('unsafeEdit', {
|
|
221
|
-
data: { reason: 'unsupported-serialized-value', field: key },
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
const changed = !before.equals(after);
|
|
225
|
-
if (changed) {
|
|
226
|
-
try {
|
|
227
|
-
await assertNoLinksBelow(selected, path);
|
|
228
|
-
await writeFileAtomicIfUnchanged(path, after, before);
|
|
229
|
-
}
|
|
230
|
-
catch (cause) {
|
|
231
|
-
if (getErrorCode(cause) === 'FILE_CHANGED')
|
|
232
|
-
throw planErrors.create('sourceChanged', { cause });
|
|
233
|
-
throw planErrors.create('writeFailed', { cause });
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
directory: selected,
|
|
238
|
-
document: request.document,
|
|
239
|
-
changed,
|
|
240
|
-
diagnostics: observation.diagnostics,
|
|
241
|
-
};
|
|
186
|
+
return withPlanDocumentLocks(directory, [request.document], async (selected) => {
|
|
187
|
+
const before = await readMutationSource(selected, request.document, options, request.expectedSha256);
|
|
188
|
+
const prepared = preparePlanUpdate(decodeSource(before), request);
|
|
189
|
+
return persistPrepared(selected, request.document, before, prepared);
|
|
190
|
+
});
|
|
242
191
|
};
|
package/dist/files/types.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { IParsedMilestones, IParsedPlan } from '../documents/index.ts';
|
|
1
|
+
import type { IParsedMilestones, IParsedPlan, IPlanFingerprint } from '../documents/index.ts';
|
|
2
|
+
import type { IPreparedPlanEdit } from '../edits/index.ts';
|
|
2
3
|
import type { IDiagnostic } from '../protocol/index.ts';
|
|
3
4
|
/** Availability is independent per document; absence never implies approval or completion. */
|
|
4
5
|
export type IDocumentRead<TDocument> = {
|
|
5
6
|
state: 'readable';
|
|
6
7
|
path: string;
|
|
8
|
+
sha256: string;
|
|
7
9
|
value: TDocument;
|
|
8
10
|
} | {
|
|
9
11
|
state: 'missing';
|
|
@@ -17,19 +19,31 @@ export type IPlanInspection = {
|
|
|
17
19
|
directory: string;
|
|
18
20
|
plan: IDocumentRead<IParsedPlan>;
|
|
19
21
|
milestones: IDocumentRead<IParsedMilestones>;
|
|
22
|
+
fingerprint?: IPlanFingerprint;
|
|
20
23
|
diagnostics: IDiagnostic[];
|
|
21
24
|
};
|
|
22
25
|
/** Creation returns original generated/supplied sources and their useful observations. */
|
|
23
26
|
export type IPlanCreation = {
|
|
24
27
|
directory: string;
|
|
25
|
-
plan: IParsedPlan
|
|
26
|
-
|
|
28
|
+
plan: IParsedPlan & {
|
|
29
|
+
sha256: string;
|
|
30
|
+
};
|
|
31
|
+
milestones: (IParsedMilestones & {
|
|
32
|
+
sha256: string;
|
|
33
|
+
}) | null;
|
|
27
34
|
diagnostics: IDiagnostic[];
|
|
28
35
|
};
|
|
29
36
|
/** An update reports only its selected document; it does not inspect unrelated files. */
|
|
30
|
-
export type IPlanUpdateResult = {
|
|
37
|
+
export type IPlanUpdateResult = Omit<IPreparedPlanEdit, 'source'> & {
|
|
31
38
|
directory: string;
|
|
32
39
|
document: 'plan' | 'milestones';
|
|
33
|
-
|
|
40
|
+
};
|
|
41
|
+
/** Guarded source writes have the same digest/assignment result as narrow updates. */
|
|
42
|
+
export type IPlanDocumentWriteResult = IPlanUpdateResult;
|
|
43
|
+
export type IRemoveMilestonesResult = {
|
|
44
|
+
directory: string;
|
|
45
|
+
document: 'milestones';
|
|
46
|
+
changed: true;
|
|
47
|
+
sha256: null;
|
|
34
48
|
diagnostics: IDiagnostic[];
|
|
35
49
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type IInspectPlanOptions } from '../protocol/index.ts';
|
|
2
2
|
import type { IPlanInspection } from './types.ts';
|
|
3
3
|
/**
|
|
4
4
|
* Read only plan.md and milestones.md beneath the explicitly selected directory.
|
|
5
5
|
* Missing, oversized, unreadable, and undecodable files are observations; other
|
|
6
6
|
* readable documents remain available. No history is discovered or restored.
|
|
7
|
-
* @param directory Caller-selected path, including custom
|
|
8
|
-
* @param options Per-file byte bound, defaulting to 1 MiB.
|
|
7
|
+
* @param directory Caller-selected path, including custom locations.
|
|
8
|
+
* @param options Per-file byte bound, defaulting to 1 MiB, and optional fingerprint exclusions.
|
|
9
9
|
* @returns Per-document availability, partial parsed data, and diagnostics.
|
|
10
10
|
* @throws
|
|
11
11
|
* - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
|
|
12
12
|
*/
|
|
13
|
-
export declare const inspectPlan: (directory: string, options?:
|
|
13
|
+
export declare const inspectPlan: (directory: string, options?: IInspectPlanOptions) => Promise<IPlanInspection>;
|
package/dist/files/utilities.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
2
|
import { getErrorCode } from 'jg-exception';
|
|
3
|
+
import { hashValue } from 'jg-crypto';
|
|
3
4
|
import { readBoundedFile } from 'jg-fs';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { parseMilestones, parsePlan } from "../documents/index.js";
|
|
7
|
+
import { fingerprintObservation } from "../documents/fingerprints.js";
|
|
6
8
|
import { decodeSource } from "../edits/index.js";
|
|
7
9
|
import { planErrors } from "../errors/index.js";
|
|
8
|
-
import { DEFAULT_MAX_BYTES,
|
|
10
|
+
import { DEFAULT_MAX_BYTES, InspectPlanOptionsSchema, } from "../protocol/index.js";
|
|
9
11
|
const observeFile = async (directory, name, maxBytes, parse) => {
|
|
10
12
|
const path = join(directory, `${name}.md`);
|
|
11
13
|
let bytes;
|
|
@@ -49,28 +51,30 @@ const observeFile = async (directory, name, maxBytes, parse) => {
|
|
|
49
51
|
],
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
|
-
return { state: 'readable', path, value: parse(source) };
|
|
54
|
+
return { state: 'readable', path, sha256: hashValue(bytes), value: parse(source) };
|
|
53
55
|
};
|
|
54
56
|
/**
|
|
55
57
|
* Read only plan.md and milestones.md beneath the explicitly selected directory.
|
|
56
58
|
* Missing, oversized, unreadable, and undecodable files are observations; other
|
|
57
59
|
* readable documents remain available. No history is discovered or restored.
|
|
58
|
-
* @param directory Caller-selected path, including custom
|
|
59
|
-
* @param options Per-file byte bound, defaulting to 1 MiB.
|
|
60
|
+
* @param directory Caller-selected path, including custom locations.
|
|
61
|
+
* @param options Per-file byte bound, defaulting to 1 MiB, and optional fingerprint exclusions.
|
|
60
62
|
* @returns Per-document availability, partial parsed data, and diagnostics.
|
|
61
63
|
* @throws
|
|
62
64
|
* - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
|
|
63
65
|
*/
|
|
64
66
|
export const inspectPlan = async (directory, options = {}) => {
|
|
67
|
+
const parsed = InspectPlanOptionsSchema.safeParse(options);
|
|
65
68
|
if (!z
|
|
66
69
|
.string()
|
|
67
70
|
.min(1)
|
|
68
71
|
.refine((value) => !value.includes('\0'))
|
|
69
72
|
.safeParse(directory).success ||
|
|
70
|
-
!
|
|
73
|
+
!parsed.success)
|
|
71
74
|
throw planErrors.create('invalidInput');
|
|
72
75
|
const selected = resolve(directory);
|
|
73
|
-
const
|
|
76
|
+
const input = parsed.data;
|
|
77
|
+
const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
74
78
|
const [plan, milestones] = await Promise.all([
|
|
75
79
|
observeFile(selected, 'plan', maxBytes, parsePlan),
|
|
76
80
|
observeFile(selected, 'milestones', maxBytes, parseMilestones),
|
|
@@ -99,5 +103,18 @@ export const inspectPlan = async (directory, options = {}) => {
|
|
|
99
103
|
severity: 'warning',
|
|
100
104
|
message: 'Milestones remain without a plan; no plan is restored.',
|
|
101
105
|
});
|
|
102
|
-
|
|
106
|
+
const fingerprint = input.fingerprint === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: fingerprintObservation(plan.state === 'readable' ? plan.value : null, input.fingerprint);
|
|
109
|
+
if (fingerprint) {
|
|
110
|
+
const known = new Set(diagnostics);
|
|
111
|
+
diagnostics.push(...fingerprint.diagnostics.filter((item) => !known.has(item)));
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
directory: selected,
|
|
115
|
+
plan,
|
|
116
|
+
milestones,
|
|
117
|
+
diagnostics,
|
|
118
|
+
...(fingerprint ? { fingerprint } : {}),
|
|
119
|
+
};
|
|
103
120
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
export { checkPlan, parseMilestones, parsePlan } from './documents/index.ts';
|
|
2
|
-
export type { ICheckResult, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, IParsedPlan, } from './documents/index.ts';
|
|
1
|
+
export { checkPlan, parseMilestones, parsePlan, fingerprintPlan } from './documents/index.ts';
|
|
2
|
+
export type { ICheckResult, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, IParsedPlan, ISourceSpan, IHeadingSection, IPlanFingerprint, } from './documents/index.ts';
|
|
3
3
|
export { ERRORS } from './errors/index.ts';
|
|
4
4
|
export { inspectPlan } from './files/index.ts';
|
|
5
|
-
export { createPlan, updatePlan } from './files/index.ts';
|
|
5
|
+
export { createPlan, updatePlan, writePlanDocument, removeMilestones, withPlanDocumentLocks, } from './files/index.ts';
|
|
6
|
+
export { preparePlanUpdate, preparePlanDocumentWrite } from './edits/index.ts';
|
|
7
|
+
export type { IPreparedPlanEdit, IAssignedMilestoneId } from './edits/index.ts';
|
|
6
8
|
export type { IDocumentRead, IPlanInspection } from './files/index.ts';
|
|
7
|
-
export type { IPlanCreation, IPlanUpdateResult } from './files/index.ts';
|
|
8
|
-
export { PROTOCOL_VERSION, DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, SourceLocationSchema, } from './protocol/index.ts';
|
|
9
|
-
export type { IDiagnostic, IPlanDocuments, IPlanMetadata, IReadOptions, ISourceLocation, } from './protocol/index.ts';
|
|
10
|
-
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, } from './protocol/index.ts';
|
|
11
|
-
export type { ICreatePlanOptions, IPlanUpdate } from './protocol/index.ts';
|
|
9
|
+
export type { IPlanCreation, IPlanUpdateResult, IPlanDocumentWriteResult, IRemoveMilestonesResult, } from './files/index.ts';
|
|
10
|
+
export { PROTOCOL_VERSION, DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, PlanFingerprintOptionsSchema, PlanFingerprintSchema, InspectPlanOptionsSchema, SourceLocationSchema, } from './protocol/index.ts';
|
|
11
|
+
export type { IDiagnostic, IPlanDocuments, IPlanMetadata, IReadOptions, IPlanFingerprintOptions, IInspectPlanOptions, ISourceLocation, } from './protocol/index.ts';
|
|
12
|
+
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, PlanDocumentWriteSchema, RemoveMilestonesSchema, } from './protocol/index.ts';
|
|
13
|
+
export type { ICreatePlanOptions, IPlanUpdate, IPlanDocumentWrite, IRemoveMilestones, } from './protocol/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { checkPlan, parseMilestones, parsePlan } from "./documents/index.js";
|
|
1
|
+
export { checkPlan, parseMilestones, parsePlan, fingerprintPlan } from "./documents/index.js";
|
|
2
2
|
export { ERRORS } from "./errors/index.js";
|
|
3
3
|
export { inspectPlan } from "./files/index.js";
|
|
4
|
-
export { createPlan, updatePlan } from "./files/index.js";
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
4
|
+
export { createPlan, updatePlan, writePlanDocument, removeMilestones, withPlanDocumentLocks, } from "./files/index.js";
|
|
5
|
+
export { preparePlanUpdate, preparePlanDocumentWrite } from "./edits/index.js";
|
|
6
|
+
export { PROTOCOL_VERSION, DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, PlanFingerprintOptionsSchema, PlanFingerprintSchema, InspectPlanOptionsSchema, SourceLocationSchema, } from "./protocol/index.js";
|
|
7
|
+
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, PlanDocumentWriteSchema, RemoveMilestonesSchema, } from "./protocol/index.js";
|
package/dist/protocol/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { DEFAULT_MAX_BYTES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
|
-
export { DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, SourceLocationSchema, } from './types.ts';
|
|
3
|
-
export type { IDiagnostic, IPlanDocuments, IPlanMetadata, IReadOptions, ISourceLocation, } from './types.ts';
|
|
4
|
-
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, } from './types.ts';
|
|
5
|
-
export type { ICreatePlanOptions, IPlanUpdate } from './types.ts';
|
|
2
|
+
export { DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, PlanFingerprintOptionsSchema, PlanFingerprintSchema, InspectPlanOptionsSchema, SourceLocationSchema, } from './types.ts';
|
|
3
|
+
export type { IDiagnostic, IPlanDocuments, IPlanMetadata, IReadOptions, IPlanFingerprintOptions, IPlanFingerprint, IInspectPlanOptions, ISourceLocation, } from './types.ts';
|
|
4
|
+
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, PlanDocumentWriteSchema, RemoveMilestonesSchema, } from './types.ts';
|
|
5
|
+
export type { ICreatePlanOptions, IPlanUpdate, IPlanDocumentWrite, IRemoveMilestones, } from './types.ts';
|
package/dist/protocol/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { DEFAULT_MAX_BYTES, PROTOCOL_VERSION } from "./constants.js";
|
|
2
|
-
export { DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, SourceLocationSchema, } from "./types.js";
|
|
3
|
-
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, } from "./types.js";
|
|
2
|
+
export { DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, PlanFingerprintOptionsSchema, PlanFingerprintSchema, InspectPlanOptionsSchema, SourceLocationSchema, } from "./types.js";
|
|
3
|
+
export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, PlanDocumentWriteSchema, RemoveMilestonesSchema, } from "./types.js";
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -45,6 +45,43 @@ export declare const ReadOptionsSchema: z.ZodObject<{
|
|
|
45
45
|
maxBytes: z.ZodOptional<z.ZodNumber>;
|
|
46
46
|
}, z.core.$strict>;
|
|
47
47
|
export type IReadOptions = z.infer<typeof ReadOptionsSchema>;
|
|
48
|
+
/** Exclusions are caller policy; no metadata key is excluded implicitly. */
|
|
49
|
+
export declare const PlanFingerprintOptionsSchema: z.ZodObject<{
|
|
50
|
+
excludeMetadataKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
export type IPlanFingerprintOptions = z.infer<typeof PlanFingerprintOptionsSchema>;
|
|
53
|
+
/** Closed JSON result for library and CLI consumers; unavailable identity has diagnostics. */
|
|
54
|
+
export declare const PlanFingerprintSchema: z.ZodObject<{
|
|
55
|
+
sha256: z.ZodNullable<z.ZodString>;
|
|
56
|
+
excludeMetadataKeys: z.ZodArray<z.ZodString>;
|
|
57
|
+
diagnostics: z.ZodArray<z.ZodObject<{
|
|
58
|
+
code: z.ZodString;
|
|
59
|
+
severity: z.ZodEnum<{
|
|
60
|
+
error: "error";
|
|
61
|
+
info: "info";
|
|
62
|
+
warning: "warning";
|
|
63
|
+
}>;
|
|
64
|
+
document: z.ZodEnum<{
|
|
65
|
+
plan: "plan";
|
|
66
|
+
milestones: "milestones";
|
|
67
|
+
}>;
|
|
68
|
+
message: z.ZodString;
|
|
69
|
+
location: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
offset: z.ZodNumber;
|
|
71
|
+
line: z.ZodNumber;
|
|
72
|
+
column: z.ZodNumber;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
}, z.core.$strict>;
|
|
76
|
+
export type IPlanFingerprint = z.infer<typeof PlanFingerprintSchema>;
|
|
77
|
+
/** Request a structural fingerprint without another document read or parse. */
|
|
78
|
+
export declare const InspectPlanOptionsSchema: z.ZodObject<{
|
|
79
|
+
maxBytes: z.ZodOptional<z.ZodNumber>;
|
|
80
|
+
fingerprint: z.ZodOptional<z.ZodObject<{
|
|
81
|
+
excludeMetadataKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
82
|
+
}, z.core.$strict>>;
|
|
83
|
+
}, z.core.$strict>;
|
|
84
|
+
export type IInspectPlanOptions = z.infer<typeof InspectPlanOptionsSchema>;
|
|
48
85
|
/** Creation accepts JSON metadata, including project extensions and nonconforming known values. */
|
|
49
86
|
export declare const CreatePlanOptionsSchema: z.ZodObject<{
|
|
50
87
|
slug: z.ZodString;
|
|
@@ -82,8 +119,10 @@ export declare const PlanUpdateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
82
119
|
revision: z.ZodExactOptional<z.ZodNumber>;
|
|
83
120
|
status: z.ZodExactOptional<z.ZodString>;
|
|
84
121
|
}, z.core.$strict>;
|
|
122
|
+
expectedSha256: z.ZodOptional<z.ZodString>;
|
|
85
123
|
}, z.core.$strict>, z.ZodObject<{
|
|
86
124
|
document: z.ZodLiteral<"milestones">;
|
|
125
|
+
expectedSha256: z.ZodOptional<z.ZodString>;
|
|
87
126
|
changes: z.ZodArray<z.ZodObject<{
|
|
88
127
|
select: z.ZodXor<readonly [z.ZodObject<{
|
|
89
128
|
id: z.ZodUUID;
|
|
@@ -97,3 +136,27 @@ export declare const PlanUpdateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
97
136
|
}, z.core.$strict>>;
|
|
98
137
|
}, z.core.$strict>], "document">;
|
|
99
138
|
export type IPlanUpdate = z.infer<typeof PlanUpdateSchema>;
|
|
139
|
+
/** Guarded document replacement; null expectations permit only absent milestone creation. */
|
|
140
|
+
export declare const PlanDocumentWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
141
|
+
document: z.ZodLiteral<"plan">;
|
|
142
|
+
expectedSha256: z.ZodString;
|
|
143
|
+
body: z.ZodString;
|
|
144
|
+
set: z.ZodOptional<z.ZodObject<{
|
|
145
|
+
id: z.ZodExactOptional<z.ZodString>;
|
|
146
|
+
created_at: z.ZodExactOptional<z.ZodISODateTime>;
|
|
147
|
+
updated_at: z.ZodExactOptional<z.ZodISODateTime>;
|
|
148
|
+
revision: z.ZodExactOptional<z.ZodNumber>;
|
|
149
|
+
status: z.ZodExactOptional<z.ZodString>;
|
|
150
|
+
}, z.core.$strict>>;
|
|
151
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
152
|
+
document: z.ZodLiteral<"milestones">;
|
|
153
|
+
expectedSha256: z.ZodNullable<z.ZodString>;
|
|
154
|
+
source: z.ZodString;
|
|
155
|
+
assignMissingIds: z.ZodOptional<z.ZodBoolean>;
|
|
156
|
+
}, z.core.$strict>], "document">;
|
|
157
|
+
export type IPlanDocumentWrite = z.infer<typeof PlanDocumentWriteSchema>;
|
|
158
|
+
/** Removing a selected regular milestone document always requires its observed hash. */
|
|
159
|
+
export declare const RemoveMilestonesSchema: z.ZodObject<{
|
|
160
|
+
expectedSha256: z.ZodString;
|
|
161
|
+
}, z.core.$strict>;
|
|
162
|
+
export type IRemoveMilestones = z.infer<typeof RemoveMilestonesSchema>;
|
package/dist/protocol/types.js
CHANGED
|
@@ -30,6 +30,25 @@ export const PlanDocumentsSchema = z.strictObject({
|
|
|
30
30
|
export const ReadOptionsSchema = z.strictObject({
|
|
31
31
|
maxBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
32
32
|
});
|
|
33
|
+
/** Exclusions are caller policy; no metadata key is excluded implicitly. */
|
|
34
|
+
export const PlanFingerprintOptionsSchema = z.strictObject({
|
|
35
|
+
excludeMetadataKeys: z.array(z.string()).optional(),
|
|
36
|
+
});
|
|
37
|
+
/** Closed JSON result for library and CLI consumers; unavailable identity has diagnostics. */
|
|
38
|
+
export const PlanFingerprintSchema = z
|
|
39
|
+
.strictObject({
|
|
40
|
+
sha256: z
|
|
41
|
+
.string()
|
|
42
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
43
|
+
.nullable(),
|
|
44
|
+
excludeMetadataKeys: z.array(z.string()).refine((keys) => new Set(keys).size === keys.length),
|
|
45
|
+
diagnostics: z.array(DiagnosticSchema),
|
|
46
|
+
})
|
|
47
|
+
.refine((value) => value.sha256 !== null || value.diagnostics.length > 0);
|
|
48
|
+
/** Request a structural fingerprint without another document read or parse. */
|
|
49
|
+
export const InspectPlanOptionsSchema = ReadOptionsSchema.extend({
|
|
50
|
+
fingerprint: PlanFingerprintOptionsSchema.optional(),
|
|
51
|
+
});
|
|
33
52
|
/** Creation accepts JSON metadata, including project extensions and nonconforming known values. */
|
|
34
53
|
export const CreatePlanOptionsSchema = z.strictObject({
|
|
35
54
|
slug: z
|
|
@@ -68,11 +87,44 @@ export const MilestoneSetSchema = z
|
|
|
68
87
|
.refine((value) => Object.keys(value).length > 0);
|
|
69
88
|
/** A call edits one document with one closed patch, never arbitrary prose. */
|
|
70
89
|
export const PlanUpdateSchema = z.discriminatedUnion('document', [
|
|
71
|
-
z.strictObject({
|
|
90
|
+
z.strictObject({
|
|
91
|
+
document: z.literal('plan'),
|
|
92
|
+
set: PlanSetSchema,
|
|
93
|
+
expectedSha256: z
|
|
94
|
+
.string()
|
|
95
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
96
|
+
.optional(),
|
|
97
|
+
}),
|
|
72
98
|
z.strictObject({
|
|
73
99
|
document: z.literal('milestones'),
|
|
100
|
+
expectedSha256: z
|
|
101
|
+
.string()
|
|
102
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
103
|
+
.optional(),
|
|
74
104
|
changes: z
|
|
75
105
|
.array(z.strictObject({ select: MilestoneSelectorSchema, set: MilestoneSetSchema }))
|
|
76
106
|
.min(1),
|
|
77
107
|
}),
|
|
78
108
|
]);
|
|
109
|
+
/** Guarded document replacement; null expectations permit only absent milestone creation. */
|
|
110
|
+
export const PlanDocumentWriteSchema = z.discriminatedUnion('document', [
|
|
111
|
+
z.strictObject({
|
|
112
|
+
document: z.literal('plan'),
|
|
113
|
+
expectedSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
114
|
+
body: z.string(),
|
|
115
|
+
set: PlanSetSchema.optional(),
|
|
116
|
+
}),
|
|
117
|
+
z.strictObject({
|
|
118
|
+
document: z.literal('milestones'),
|
|
119
|
+
expectedSha256: z
|
|
120
|
+
.string()
|
|
121
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
122
|
+
.nullable(),
|
|
123
|
+
source: z.string(),
|
|
124
|
+
assignMissingIds: z.boolean().optional(),
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
/** Removing a selected regular milestone document always requires its observed hash. */
|
|
128
|
+
export const RemoveMilestonesSchema = z.strictObject({
|
|
129
|
+
expectedSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
130
|
+
});
|
package/docs/getting-started.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Getting started
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The protocol and examples are usable directly. The reference library and CLI are optional; the [website's availability label](https://jg-plans.jesusgraterol.dev/docs/) identifies a verified published version. The checkout commands below work independently of npm publication.
|
|
4
4
|
|
|
5
5
|
Start with an ordinary [small plan](../examples/small/plan.md). It contains five core metadata fields and a short Markdown body. Add [milestones](../examples/with-milestones/milestones.md) only when separate units help review the work. Users can edit, move, or delete these files.
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ node dist/cli/cli.js check --directory examples/with-milestones --json
|
|
|
17
17
|
|
|
18
18
|
Inspect gives useful observations without requiring a conforming document. Check is optional. Missing files remain missing; neither operation restores history or approves execution. Document bodies are excluded from CLI output unless explicitly requested.
|
|
19
19
|
|
|
20
|
-
The [API reference](../api/README.md) describes pure parsing, checking, selected-directory inspection, exclusive creation,
|
|
20
|
+
The [API reference](../api/README.md) describes pure parsing, checking, selected-directory inspection, exclusive creation, pure preparation, guarded body/source writes, marker updates, removal, and shared locks. The [CLI guide](../cli/README.md) describes JSON input, automation envelopes, and exits. Read the [integration boundary](integrations.md) before building approval or execution behavior on these observations.
|
|
21
21
|
|
|
22
22
|
## Make an explicit edit
|
|
23
23
|
|
|
@@ -29,8 +29,8 @@ node dist/cli/cli.js create --slug small-change --input create.json --json
|
|
|
29
29
|
|
|
30
30
|
Use the returned directory for inspection and updates. Update input such as `{ "document": "plan", "set": { "revision": 2, "status": "in progress" } }` changes only those requested fields. Supply updated_at explicitly when appropriate; nothing advances automatically. The [runnable example](../examples/library.mjs) shows the full library journey with cleanup limited to its own temporary directory.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Inspect exact hashes and supply expectedSha256 for guarded edits. Shared local document locks coordinate library, CLI, and participating consumers. Unsupported affected YAML/ambiguous markers fail without writing, detected changes are not retried, and existing-target expectations never recreate deleted files. Use explicit body/source writes when replacing prose; all unaffected bytes remain intact.
|
|
33
33
|
|
|
34
34
|
## Contributing
|
|
35
35
|
|
|
36
|
-
The [development guide](https://github.com/jesusgraterol/jg-plans/blob/
|
|
36
|
+
The [development guide](https://github.com/jesusgraterol/jg-plans/blob/main/docs/development.md) owns checks and package/site boundaries. Tests exercise built root exports and an isolated installation of the actual tarball. The protocol remains provisional until separately approved skill and HQ integrations exercise it.
|
package/docs/integrations.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# Consumer
|
|
1
|
+
# Consumer integrations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Protocol 0.1 provides ordinary documents and deterministic operations. Consumers decide scope, authority, approval, progress semantics, retention, and reconciliation. A hash or checker result grants no permission.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Skills: explicit runtime and freshness policy
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Persisted planning workflows use the separately installed supported CLI, with explicit selected paths and structured JSON. Preflight the package version and envelope; never install it automatically. New skill plans use jg-plans/; an explicitly supplied custom path remains valid. Readers do not discover old roots or reconstruct deleted documents.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Keep one identical compact **jg-plans protocol 0.1** section in the seven planning reader/writer skills. Role-specific rules stay outside it. jg-plan owns substantive revisions and explicit updated_at/revision changes; jg-breakdown owns semantic coverage and sequence binding; jg-execute owns authorized work and progress. Reviews/challenges remain read-only. No missing/custom status is coerced into permission or completion.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Inspect before each mutation and supply the observed expectedSha256. Use write for plan-body/core-metadata changes or complete milestone source, update for narrow markers/core fields, and authorized guarded removal for an unnecessary sequence. Request missing UUIDs explicitly. Preserve existing identities and valid completion evidence; tools do not infer correspondence.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The skills bind a milestone sequence to plan id, revision, and structural content fingerprint, excluding exactly created_at, updated_at, revision, and status from that fingerprint. Other metadata and exact body remain covered. Revalidate coverage after revisions before stamping a fresh binding. A stale/unavailable binding cannot authorize sequence execution. Whole-plan execution without an expected sequence stays a supported single unit; disappearance of an expected sequence is a loss to report.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## HQ: one engine, application-owned authority
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Use public jg-plans observations/ranges, fingerprints, and pure preparation instead of another Markdown grammar. Retain HQ's strict project profile, host parser hook, approval digests, execution gates, limits, resource/error allocations, coordination journal, and reconciliation. Unknown contractual metadata and shared/trailing requirements must remain part of execution identity.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
For journaled writes, prepare once and persist the exact resulting source, generated IDs, and timestamps before applying it. Acquire HQ metadata ownership before shared document locks. Inside withPlanDocumentLocks, use pure preparation and HQ persistence; do not nest filesystem mutation entry points. Release shared locks successfully before completing the HQ ledger. Release failures can follow durable writes and require reconciliation with admission closed.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## Coordinating participating writers
|
|
22
22
|
|
|
23
|
-
The
|
|
23
|
+
The CLI and every filesystem mutation share canonical local document lock names with the public callback. Locks cover one bounded read/prepare/persist operation, never model turns. Fixed order is plan then milestones; release is reverse. Dead owners remain busy until explicit operator inspection/recovery. The library never takes over automatically or removes an unfamiliar lock.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
File hashes guard exact bytes; structural fingerprints cover caller-selected content; approval digests are application policy. Preserve this distinction. There is no cross-document transaction and nonparticipating editors retain the documented comparison-to-replacement/deletion race. Partial outcomes must be reported honestly rather than rolled back over newer content.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
See the [API](../api/README.md), [CLI](../cli/README.md), and [errors](../errors/README.md) for exact contracts. Protocol conformance is optional during inspection; stricter application admission remains consumer-owned.
|