jg-plans 0.1.2 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/api/README.md +74 -11
  4. package/cli/README.md +23 -5
  5. package/dist/cli/arguments.js +18 -3
  6. package/dist/cli/commands.js +76 -17
  7. package/dist/cli/types.d.ts +3 -1
  8. package/dist/documents/fingerprints.d.ts +19 -0
  9. package/dist/documents/fingerprints.js +56 -0
  10. package/dist/documents/index.d.ts +2 -1
  11. package/dist/documents/index.js +1 -0
  12. package/dist/documents/milestones.js +44 -33
  13. package/dist/documents/plan.js +33 -68
  14. package/dist/documents/types.d.ts +22 -0
  15. package/dist/documents/utilities.d.ts +16 -2
  16. package/dist/documents/utilities.js +105 -4
  17. package/dist/edits/index.d.ts +2 -0
  18. package/dist/edits/index.js +2 -1
  19. package/dist/edits/preparations.d.ts +30 -0
  20. package/dist/edits/preparations.js +159 -0
  21. package/dist/edits/types.d.ts +14 -0
  22. package/dist/edits/utilities.d.ts +1 -0
  23. package/dist/edits/utilities.js +9 -2
  24. package/dist/errors/constants.d.ts +3 -1
  25. package/dist/errors/constants.js +4 -0
  26. package/dist/files/documents.d.ts +46 -0
  27. package/dist/files/documents.js +176 -0
  28. package/dist/files/index.d.ts +3 -1
  29. package/dist/files/index.js +2 -0
  30. package/dist/files/locks.d.ts +16 -0
  31. package/dist/files/locks.js +127 -0
  32. package/dist/files/mutations.d.ts +7 -3
  33. package/dist/files/mutations.js +32 -83
  34. package/dist/files/types.d.ts +19 -5
  35. package/dist/files/utilities.d.ts +4 -4
  36. package/dist/files/utilities.js +24 -7
  37. package/dist/index.d.ts +10 -8
  38. package/dist/index.js +5 -4
  39. package/dist/protocol/index.d.ts +4 -4
  40. package/dist/protocol/index.js +2 -2
  41. package/dist/protocol/types.d.ts +63 -0
  42. package/dist/protocol/types.js +53 -1
  43. package/docs/getting-started.md +2 -2
  44. package/docs/integrations.md +14 -36
  45. package/errors/README.md +6 -4
  46. package/examples/library.mjs +34 -2
  47. package/package.json +4 -3
  48. package/protocol/0.1/README.md +8 -2
@@ -1,13 +1,15 @@
1
- import { lstat, mkdir, open, rmdir, unlink } from 'node:fs/promises';
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 { assertNoLinksBelow, readBoundedFile, writeFileAtomicIfUnchanged } from 'jg-fs';
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 { applyEdits, decodeSource, encodeSource, milestoneEdits, planEdits, } from "../edits/index.js";
8
+ import { decodeSource, encodeSource, preparePlanUpdate } from "../edits/index.js";
9
9
  import { planErrors } from "../errors/index.js";
10
- import { CreatePlanOptionsSchema, DEFAULT_MAX_BYTES, PlanUpdateSchema, ReadOptionsSchema, } from "../protocol/index.js";
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 writeExclusive(root, join(directory, 'plan.md'), planBytes, owned);
150
- if (milestoneBytes !== null)
151
- await writeExclusive(root, join(directory, 'milestones.md'), milestoneBytes, owned);
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 one explicit metadata or marker patch, preserving every other source byte.
171
- * Callers serialize the whole read/edit/write operation. The final byte comparison
172
- * detects observed changes but has a comparison-to-rename race; no retry or lock is used.
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
- const selected = resolve(directory);
193
- const path = join(selected, `${request.document}.md`);
194
- let before;
195
- try {
196
- await assertNoLinksBelow(selected, path);
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
  };
@@ -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
- milestones: IParsedMilestones | null;
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
- changed: boolean;
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 IReadOptions } from '../protocol/index.ts';
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 or legacy locations.
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?: IReadOptions) => Promise<IPlanInspection>;
13
+ export declare const inspectPlan: (directory: string, options?: IInspectPlanOptions) => Promise<IPlanInspection>;
@@ -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, ReadOptionsSchema, } from "../protocol/index.js";
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 or legacy locations.
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
- !ReadOptionsSchema.safeParse(options).success)
73
+ !parsed.success)
71
74
  throw planErrors.create('invalidInput');
72
75
  const selected = resolve(directory);
73
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
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
- return { directory: selected, plan, milestones, diagnostics };
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 { PROTOCOL_VERSION, DiagnosticSchema, PlanDocumentsSchema, PlanMetadataSchema, ReadOptionsSchema, SourceLocationSchema, } from "./protocol/index.js";
6
- export { CreatePlanOptionsSchema, MilestoneSelectorSchema, MilestoneSetSchema, PlanSetSchema, PlanUpdateSchema, } from "./protocol/index.js";
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";
@@ -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';
@@ -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";
@@ -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>;
@@ -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({ document: z.literal('plan'), set: PlanSetSchema }),
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
+ });
@@ -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, and targeted metadata/marker updates. 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.
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,7 +29,7 @@ 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
- Serialize each document's complete read/edit/write operation. Unsupported YAML/ambiguous markers fail without writing, detected changes are not retried, and deleted targets are never recreated. The library preserves unrelated bytes; it does not rewrite prose or coordinate competing writers.
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
 
@@ -1,49 +1,27 @@
1
- # Consumer integration
1
+ # Consumer integrations
2
2
 
3
- Use [protocol 0.1](../protocol/0.1/README.md) as the compact shared format. A project can read Markdown itself or use the reference library/CLI. The [website](https://jg-plans.jesusgraterol.dev) identifies verified package availability. The protocol is usable without installing a tool or calling a website.
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
- ## Keep policy with the consumer
5
+ ## Skills: explicit runtime and freshness policy
6
6
 
7
- Unknown metadata and custom status strings belong to the document's author/project. Keep approvals, digests, authorization, scopes, priorities, allocations, registries, locks, execution and reconciliation in the consuming application. A closed local schema is a stricter consumer profile, not a universal protocol rule.
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
- For example, [the extension example](../examples/extensions/plan.md) contains project.owner and labels. A generic reader preserves them. The example's waiting-for-feedback status does not require every consumer to implement that workflow.
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
- ## Roots and deletion
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
- Honor an explicit planning root. Otherwise preserve an established coding-agent-planning/ or custom root and use jg-plans/ for a new default. The reference CLI uses explicit paths; root discovery and publication exclusions belong to integrations. Do not move existing plans automatically.
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
- Missing, edited, or deleted documents remain ordinary user choices. Return observations about what exists, preserve known progress, and avoid restoring removed files/history. An optional writer-format checker does not gate inspection or a safe independent edit. Status and metadata never grant execution permission.
15
+ ## HQ: one engine, application-owned authority
16
16
 
17
- ## Editing and coordination
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
- Serialize the complete read/edit/write operation for each document, in one process and across processes. The standalone CLI does not coordinate concurrent writers. Atomic replacement and prior-content checks prevent partial output and detect observed conflicts, but retain a comparison-to-rename race.
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
- The reference implementation edits only explicitly requested core metadata or milestone markers, preserves unrelated bytes, and reports unsupported operations. It does not update overview tables or rewrite revision summaries. Missing selected documents are never recreated; inspection/checking does not gate a safe independent edit. Revision and timestamps change only when requested. Use the [API contracts](../api/README.md) and their closed schemas at a programmatic boundary; do not invent a second parser in a CLI wrapper.
21
+ ## Coordinating participating writers
22
22
 
23
- The selected directory and ancestors are trusted caller choices. The library rejects links below that root and preserves file permissions on replacement. Unsafe affected YAML/encoding/selector ambiguity leaves the source untouched. The caller must decide how to serialize the complete operation, including same-process calls. A writer that rewrites prose chooses its own best-effort snapshot location/retention; readers never depend on those snapshots.
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
- ## Adoption boundary
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
- Adopting this protocol in jg-skills or an HQ project is separately scoped work. Protocol 0.1 stays provisional until jg-skills and at least one HQ consumer have exercised it. Those integrations are not prerequisites for using this project, and no downstream adoption is claimed here.
28
-
29
- ## Skills: one shared section, distinct responsibilities
30
-
31
- Keep installed skills self-contained. Copy the compact writer/reader rules into one identically maintained section stamped **jg-plans protocol 0.1**. During the skills build, compare that section across every generated skill that includes it and fail on drift. Do not make loading or using a skill depend on the website, a network request, or running the jg-plans CLI.
32
-
33
- Keep role-specific instructions outside that common section:
34
-
35
- - **jg-plan:** write identity, creation/update timestamps, revision, and status; explain the intent and revision summary in Markdown. Revising a plan preserves existing milestones and progress unless the user asks to change or remove them. Avoid creating redundant companion files for a small task.
36
- - **jg-breakdown:** add optional milestones when they help. Place status immediately after the milestone heading; place an optional UUID ID on the next line. Preserve established identities and progress while revising the breakdown. Treat any overview as derived information.
37
- - **jg-execute:** read the selected documents and the user's actual authorization. Existing status markers describe progress; missing metadata, removed files, and checker findings do not create or revoke permission.
38
- - **jg-review and jg-review-session:** assess the relevant work against available evidence and the selected plan. Name missing evidence without inventing history or treating a deleted document as completion. The status line remains authoritative over an overview row.
39
- - **Publication/discovery skills:** recognize both coding-agent-planning/ and jg-plans/, as well as explicitly configured roots. Exclude planning files from broad publication unless the user deliberately includes them. Report a selected planning directory that is not ignored; do not silently change ignore rules or move documents.
40
-
41
- For root selection, honor an explicit path first. Otherwise keep the project's configured or established root, including an existing coding-agent-planning/ directory, before introducing jg-plans/ as a new default. When multiple roots are present, resolve the intended scope through the integration's configuration or the user's selection. A root name never grants permission to read unrelated old plans.
42
-
43
- Use the CLI only during development to validate documents produced by skill behavior evaluations. Include small plans, custom metadata/statuses, revisions with existing done milestones, edited marker placement, deleted metadata, and deleted companion files. Verify that skills preserve existing progress and obey their role, beyond checking preferred writer conformance. A development evaluation failure is feedback for the skill author; runtime document inspection and user edits remain available.
44
-
45
- ## HQ consumers: depend on the reference implementation
46
-
47
- Use jg-plans for generic parsing and explicit document operations rather than maintaining another interpretation. Preserve a supported public consumer API with the smallest necessary wrapper, or make a documented breaking release when moving to the new dependency changes its contract. Test against that consumer's established documents and public API before migration.
48
-
49
- Keep approval digests, authorization, strict project frontmatter, resource schemas, execution, coordination, serialization, and reconciliation inside HQ. A stricter local profile may reject a document for a project-specific action; that does not make the document unreadable under the protocol. Avoid automatic repair, restoration of deleted plans, or silent rewriting to satisfy a closed project schema.
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.