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.
Files changed (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +8 -8
  3. package/api/README.md +75 -12
  4. package/cli/README.md +24 -6
  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 +4 -4
  44. package/docs/integrations.md +14 -14
  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
+ });
@@ -1,6 +1,6 @@
1
1
  # Getting started
2
2
 
3
- **Development preview.** The protocol and examples are usable directly. The reference APIs and CLI are implemented; a public npm release has not been verified.
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, 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,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
- 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
 
36
- The [development guide](https://github.com/jesusgraterol/jg-plans/blob/development/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.
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.
@@ -1,27 +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 implemented reference library/CLI; public npm availability is not yet verified. 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. Existing progress and supported public consumer contracts need evidence before migration. Integration recipes and the final development-time conformance guidance are completed alongside the tested reference package and website; no downstream adoption is claimed here.
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.