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.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/api/README.md +74 -11
- package/cli/README.md +23 -5
- 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 +2 -2
- package/docs/integrations.md +14 -36
- 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
|
@@ -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,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
|
-
|
|
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
|
|
package/docs/integrations.md
CHANGED
|
@@ -1,49 +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
|
-
|
|
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.
|