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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — document observations and tooling
4
+
5
+ - Add pure edit preparation, expected-hash guards, explicit body/core-metadata and milestone-source writes, opt-in UUID assignment, and guarded milestone removal. Return digests and assigned IDs through the library and CLI.
6
+ - Coordinate every filesystem mutation through shared canonical local document locks; preserve explicit crash recovery and truthful uncertain outcomes after failed release.
7
+
8
+ - Add complete UTF-16 source ranges for unfenced level-two sections, document preambles, milestone headings, and recognized Overview tables without dropping shared or trailing requirements.
9
+ - Share optional frontmatter parsing between plans and milestones; expose exact milestone body and raw extension metadata without requiring plan metadata fields.
10
+ - Return exact-byte SHA-256 values for readable documents. Add pure `fingerprintPlan` and optional inspection/CLI structural fingerprints using explicit caller-selected metadata exclusions and strict deterministic JSON.
11
+ - Use `jg-crypto` and `jg-utilities` as direct runtime dependencies. Protocol 0.1 remains provisional; fingerprints and observations grant no execution authority.
12
+
3
13
  ## 0.1.2 — documentation and examples
4
14
 
5
15
  - Complete the canonical documentation and consumer integration recipes alongside the visual example explorer, original-source downloads, and verified package availability presentation.
package/README.md CHANGED
@@ -29,7 +29,7 @@ Protocol 0.1 is provisional. The [website](https://jg-plans.jesusgraterol.dev) p
29
29
 
30
30
  Users own their documents and can edit or delete them. No history store, metadata field, CLI, or website is required to read Markdown. Optional conformance checks do not authorize execution or become a prerequisite to inspection.
31
31
 
32
- The protocol stays separate from application schemas, approvals, resource allocations, locking and reconciliation. Future jg-skills and HQ adoption are independent integrations.
32
+ The protocol stays separate from application schemas, approvals, resource allocations, coordination journals and reconciliation. Future jg-skills and HQ adoption are independent integrations.
33
33
 
34
34
  ## Development
35
35
 
@@ -44,7 +44,7 @@ pnpm check
44
44
 
45
45
  The [development guide](https://github.com/jesusgraterol/jg-plans/blob/main/docs/development.md) owns checks and package/site boundaries. Root tests build first, exercise public compiled exports, and install the actual tarball in an isolated consumer. Website tests verify production output in both themes and narrow layouts.
46
46
 
47
- The [runnable library example](examples/library.mjs) creates, inspects, updates, and checks documents in an owned temporary directory. Mutations preserve unrelated bytes and require callers to serialize edits to each document; unsupported edits fail without rewriting it. The [API](api/README.md) describes the exact preservation and concurrency limits.
47
+ The [runnable library example](examples/library.mjs) creates, inspects, updates, and checks documents in an owned temporary directory. Guarded mutations and pure preparation preserve unaffected bytes and share local document locks; unsupported edits fail without rewriting it. The [API](api/README.md) describes the exact preservation and concurrency limits.
48
48
 
49
49
  ## License
50
50
 
package/api/README.md CHANGED
@@ -7,7 +7,7 @@ The ESM root exports compiled JavaScript and TypeScript declarations. Node.js 24
7
7
  ```ts
8
8
  import { PROTOCOL_VERSION, parsePlan, parseMilestones, checkPlan, inspectPlan } from 'jg-plans';
9
9
 
10
- const plan = parsePlan('# Useful legacy Markdown');
10
+ const plan = parsePlan('# Useful Markdown');
11
11
  const milestones = parseMilestones('## Milestone 1: Work\n**Status:** pending');
12
12
  const check = checkPlan({ plan: plan.source, milestones: milestones.source });
13
13
  const observation = await inspectPlan('./jg-plans/example', { maxBytes: 1_048_576 });
@@ -17,27 +17,55 @@ const observation = await inspectPlan('./jg-plans/example', { maxBytes: 1_048_57
17
17
 
18
18
  `parsePlan(source)` retains the exact source and body. Its `metadata` contains independently validated core fields. `rawMetadata` retains the expanded YAML value, including unknown fields and invalid known values. Missing metadata, malformed YAML, unsupported expansion, and incompatible values receive structured diagnostics; they do not discard the body or authorize work.
19
19
 
20
- YAML uses maxAliasCount: 100 for expansion protection. Recursive aliases, unsafe integer expansions, and values JSON cannot represent receive a `YAML_VALUE_UNAVAILABLE` warning; rawMetadata is unavailable (`null`) and the original YAML remains in source. Raw observations use YAML's JavaScript conversion; source remains authoritative for original numeric spelling and presentation. Independently readable core fields are still observed. An unterminated frontmatter block retains the complete source as body because its boundary is unknown.
20
+ YAML uses maxAliasCount: 100 for expansion protection. Recursive aliases, unsafe integer expansions, non-string or ambiguous YAML mapping keys, and values JSON cannot represent receive a `YAML_VALUE_UNAVAILABLE` warning; rawMetadata is unavailable (`null`) and the original YAML remains in source. Raw observations use YAML's JavaScript conversion; source remains authoritative for original numeric spelling and presentation. Independently readable core fields are still observed. An unterminated frontmatter block retains the complete source as body because its boundary is unknown.
21
21
 
22
22
  `parseMilestones(source)` returns source, recognizable milestones, optional overview rows, overviewPresent, and diagnostics. overviewPresent distinguishes an absent table from an explicitly empty derived table; an existing table that omits observed milestones receives discrepancies. Each entry contains ordinal, title, observed status/ID or null, location, end offset, body, and all recognized status/ID markers. Duplicate markers remain visible and are diagnosed; the first observed marker supplies the convenience status/ID value. Consumers must consider ambiguity diagnostics before acting on it. Fenced examples are ignored. Missing IDs and overview tables are allowed. Gaps are observations, with no reconstructed entries or inferred completion.
23
23
 
24
24
  `checkPlan({ plan?, milestones? })` checks explicitly supplied strings against preferred writer conformance. Omit absent or deleted documents. It returns `{ conforms, diagnostics }`; error diagnostics make conforms false. It never writes, restores data, or runs as a prerequisite to another operation.
25
25
 
26
+ ## Source ranges and milestone metadata
27
+
28
+ Both parsers return a `preamble` span and ordered `sections` for every unfenced level-two heading. Each section has its observed title, a `heading` span including its line ending, and a `body` span extending to the next level-two heading or EOF. `ISourceSpan` has zero-based UTF-16 `start` and exclusive `end`; slice the returned original source with those offsets.
29
+
30
+ The preamble starts at zero and includes everything before the first observed section, including any BOM, frontmatter, title, and introductory prose. Concatenating the preamble and every heading/body span reproduces the complete original source. Sections keep shared, intervening, and trailing requirements visible even when they are not milestone entries. A milestone's `headingEnd` is the first offset after its heading line; its existing `end` and `body` still stop at the next level-two heading.
31
+
32
+ Milestones may begin with an optional frontmatter mapping. `frontmatter`, `rawMetadata`, and exact `body` are observed through the same YAML reader as plans, without requiring the five plan fields. Frontmatter is excluded from heading and marker scanning; Markdown-looking YAML never becomes work. Unterminated frontmatter remains ambiguous and retains the whole source as body. `overviewTables` contains only recognized table spans, from header through separator and consecutive rows; adjacent prose and fenced examples are excluded.
33
+
34
+ ## Structural fingerprints
35
+
36
+ ```ts
37
+ import { fingerprintPlan, inspectPlan } from 'jg-plans';
38
+
39
+ const exclusions = ['created_at', 'updated_at', 'revision', 'status'];
40
+ const fingerprint = fingerprintPlan('# Work', { excludeMetadataKeys: exclusions });
41
+ const inspection = await inspectPlan('./jg-plans/example', {
42
+ fingerprint: { excludeMetadataKeys: exclusions },
43
+ });
44
+ ```
45
+
46
+ `fingerprintPlan(source, options?)` returns `{ sha256, excludeMetadataKeys, diagnostics }`. It hashes strict deterministic JSON of `{ body, metadata }`, where body is exact parsed body text and metadata is raw top-level metadata after removing only explicitly named keys. The default exclusion list is empty; duplicate requested keys are reported once, in first-occurrence order. There are no implicit administrative exclusions. Unknown metadata, plan identity, nested values, and key/value distinctions remain covered. Nested fields with an excluded key name remain covered.
47
+
48
+ Metadata key order, YAML quoting, and YAML comments alone do not change this structural digest. Exact body text, including its whitespace and line endings, does. Metadata-free source uses an empty mapping and remains fingerprintable without preferred-field conformance. Unterminated or ambiguous framing, invalid YAML, nonmapping frontmatter, unresolved YAML warnings, or unavailable/non-JSON-safe values return `sha256: null` with safe diagnostics while ordinary inspection remains useful. The implementation uses `jg-utilities` strict deterministic serialization and `jg-crypto` SHA-256.
49
+
50
+ A structural fingerprint is distinct from an exact file hash and from an application's approval digest. Consumers choose exclusion policy and decide freshness, authorization, and scope coverage. It never grants approval or completion.
51
+
26
52
  ## Selected-directory inspection
27
53
 
28
- `inspectPlan(directory, { maxBytes? }?)` reads only plan.md and milestones.md from that exact directory, concurrently and with a default 1 MiB limit per file. Explicit custom paths and coding-agent-planning/ work as supplied; there is no repository/root/history discovery.
54
+ `inspectPlan(directory, { maxBytes?, fingerprint? }?)` reads only plan.md and milestones.md from that exact directory, concurrently and with a default 1 MiB limit per file. Explicit custom paths and coding-agent-planning/ work as supplied; there is no repository/root/history discovery.
55
+
56
+ The result includes the resolved directory, each document's path and readable/missing/unreadable state, useful parsed data for readable files, and aggregated diagnostics. Every readable document also has lowercase `sha256` computed from the exact original bytes, including BOM and line endings. A readable empty file differs from a missing file. Orphan milestones remain observable when the plan has been deleted. No predecessor is required or restored.
29
57
 
30
- The result includes the resolved directory, each document's path and readable/missing/unreadable state, useful parsed data for readable files, and aggregated diagnostics. A readable empty file differs from a missing file. Orphan milestones remain observable when the plan has been deleted. No predecessor is required or restored.
58
+ Supplying `fingerprint: { excludeMetadataKeys? }` adds a top-level `fingerprint` result, reusing the already-read and parsed plan. Missing/unreadable plans return an unavailable requested fingerprint rather than fabricated content. Omitting the option omits that result.
31
59
 
32
60
  Bytes must decode strictly as UTF-8. An existing BOM is retained in source. Invalid bytes receive INVALID_ENCODING, and another readable document still contributes observations. File-size failures and nonregular/unreadable targets receive operational diagnostics. Final-component file symlinks are not followed; the selected directory's ancestors are caller-chosen. The library does not silently transcode content.
33
61
 
34
62
  ## Types and schemas
35
63
 
36
- Creation and update contracts are described below. A [runnable library example](../examples/library.mjs) exercises creation, sequential updates, inspection, and checking in an owned temporary directory.
64
+ Creation, preparation, updates, guarded writes, removal, and locks are described below. A [runnable library example](../examples/library.mjs) exercises creation, sequential updates, inspection, and checking in an owned temporary directory.
37
65
 
38
- Named public types are IPlanMetadata, IDiagnostic, ISourceLocation, IReadOptions, IPlanDocuments, IParsedPlan, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, ICheckResult, IDocumentRead, IPlanInspection, ICreatePlanOptions, IPlanCreation, IPlanUpdate, and IPlanUpdateResult.
66
+ Named public types are IPlanMetadata, IDiagnostic, ISourceLocation, ISourceSpan, IHeadingSection, IReadOptions, IPlanFingerprintOptions, IInspectPlanOptions, IPlanFingerprint, IPlanDocuments, IParsedPlan, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, ICheckResult, IDocumentRead, IPlanInspection, ICreatePlanOptions, IPlanCreation, IPlanUpdate, IPlanUpdateResult, IPlanDocumentWrite, IPlanDocumentWriteResult, IRemoveMilestones, IRemoveMilestonesResult, IPreparedPlanEdit, and IAssignedMilestoneId.
39
67
 
40
- PlanMetadataSchema defines preferred complete metadata and allows project extensions. DiagnosticSchema, SourceLocationSchema, ReadOptionsSchema, and PlanDocumentsSchema own the corresponding boundary contracts. Known fields are validated independently during tolerant reads. PROTOCOL_VERSION is `0.1`; it is independent of the npm package version.
68
+ PlanMetadataSchema defines preferred complete metadata and allows project extensions. DiagnosticSchema, SourceLocationSchema, ReadOptionsSchema, PlanFingerprintOptionsSchema, PlanFingerprintSchema, InspectPlanOptionsSchema, and PlanDocumentsSchema own the corresponding boundary contracts. Fingerprint results and inspection/fingerprint options are closed schemas; only explicit supported keys are accepted. Known metadata fields are validated independently during tolerant reads. PROTOCOL_VERSION is `0.1`; it is independent of the npm package version.
41
69
 
42
70
  Source offsets count JavaScript UTF-16 code units. Lines and columns start at one. Marker value spans exclude surrounding whitespace and line endings. Entries and overview rows retain ordinalText; ordinal is null for nonpositive or unsafe integer text, avoiding rounded identities. These are observations, not general-purpose rewrite permission.
43
71
 
@@ -60,11 +88,11 @@ await updatePlan(created.directory, { document: 'plan', set: { revision: 2 } });
60
88
 
61
89
  The closed CreatePlanOptionsSchema accepts slug, optional root, body, optional JSON metadata, and optional milestones text. Missing core metadata defaults to the generated name as id, one captured ISO creation time for created_at and updated_at, revision 1, and pending status. Supplied values override defaults, including nonconforming known values; diagnostics describe them without refusing to preserve them. Unknown project metadata is retained. The supplied Markdown body and milestone bytes are preserved; creation emits new UTF-8 frontmatter with LF separators.
62
90
 
63
- The result contains directory, parsed plan, parsed milestones or null, and diagnostics. Existing destinations fail without overwriting. Failed creation attempts remove only recognized files created by that call and the empty owned plan directory. Intervening user content is preserved; incomplete cleanup is reported in error.data.cleanupFailedPaths. An empty selected storage root can remain after failure.
91
+ The result contains directory, parsed plan with its exact-byte sha256, parsed milestones with sha256 or null, and diagnostics. Existing destinations fail without overwriting. Failed creation attempts remove only recognized files created by that call and the empty owned plan directory. Intervening user content is preserved; incomplete cleanup is reported in error.data.cleanupFailedPaths. An empty selected storage root can remain after failure.
64
92
 
65
93
  ## Targeted updates
66
94
 
67
- `updatePlan(directory, patch, { maxBytes? }?)` edits one existing document and returns `{ directory, document, changed, diagnostics }`. The closed PlanUpdateSchema accepts either:
95
+ `updatePlan(directory, patch, { maxBytes? }?)` edits one existing document and returns `{ directory, document, changed, sha256, diagnostics, assignedMilestoneIds }`. The closed PlanUpdateSchema accepts either variant below, with optional expectedSha256 on either variant. Supply a freshly observed lowercase SHA-256 to reject stale or deleted sources before preparation:
68
96
 
69
97
  ```json
70
98
  { "document": "plan", "set": { "status": "in progress", "revision": 2 } }
@@ -83,12 +111,47 @@ The update reads only the selected document, bounded at 1 MiB by default. A malf
83
111
 
84
112
  Every byte outside the requested value spans or insertion points is preserved, including BOM, supplementary Unicode, unrelated nested metadata, comments, quotes, whitespace, line endings, and final-newline state. Equal values produce a no-op and skip replacement. Existing single quotes are retained when their style can represent the value; replacements otherwise use quoted YAML scalars.
85
113
 
86
- Unsupported edits fail without writing. These include malformed/unterminated frontmatter, a flow-style root mapping, complex/anchored/tagged root keys, merge keys, duplicate affected keys, and affected aliases, anchors, tags, block/multiline scalars, or implicit empty nulls. Duplicate/missing milestone selectors, repeated targets, duplicate affected markers, and overlapping spans also fail. Unrelated nonconforming fields are not a conformance gate. error.data.reason identifies the unsupported case, with field when relevant; it contains no source snippet. This is a conservative scalar/marker editor, with no general YAML or Markdown rewrite API.
114
+ Unsupported edits fail without writing. These include malformed/unterminated frontmatter, a flow-style root mapping, complex/anchored/tagged root keys, merge keys, duplicate affected keys, and affected aliases, anchors, tags, block/multiline scalars, or implicit empty nulls. Duplicate/missing milestone selectors, repeated targets, duplicate affected markers, and overlapping spans also fail. Unrelated nonconforming fields are not a conformance gate. error.data.reason identifies the unsupported case, with field when relevant; it contains no source snippet. This narrow operation is a conservative scalar/marker editor; use explicit document writes below for body or milestone-source replacement.
87
115
 
88
- Bytes must be valid UTF-8, and replacement strings cannot contain unpaired UTF-16 surrogates. The caller-selected directory and its ancestors are trusted path choices; links below that root are rejected. Replacement preserves file permissions. **Serialize the entire read/edit/write operation for a document, including within one process.** The prior-content comparison detects observed intervening changes, but a comparison-to-rename race remains. No lock, queue, journal, snapshot service, or automatic retry is provided.
116
+ Bytes must be valid UTF-8, and replacement strings cannot contain unpaired UTF-16 surrogates. The caller-selected directory and its ancestors are trusted path choices; links below that root are rejected. Replacement preserves file permissions. All filesystem mutations acquire the shared selected-document lock before reading or preparing. The prior-content comparison detects observed intervening changes, but nonparticipating editors retain a comparison-to-rename race. No journal, snapshot service, or automatic retry is provided.
89
117
 
90
118
  CreatePlanOptionsSchema and PlanUpdateSchema are public boundary schemas; PlanSetSchema, MilestoneSelectorSchema, and MilestoneSetSchema expose their constituent contracts. New metadata is JSON-compatible; original YAML source remains authoritative on reads.
91
119
 
120
+ ## Pure preparation and guarded document writes
121
+
122
+ `preparePlanUpdate(source, patch)` applies the same narrow edit entirely in memory. `preparePlanDocumentWrite(source, input)` prepares the explicit replacement below. Both return `{ source, changed, sha256, diagnostics, assignedMilestoneIds }` (IPreparedPlanEdit). They perform no filesystem access, locks, timestamps, revision increments, or implicit ID assignment. Persist the exact prepared source once in a consumer journal; do not regenerate random IDs during replay. IAssignedMilestoneId contains ordinal and id.
123
+
124
+ `writePlanDocument(directory, input, { maxBytes? }?)` reads only the selected document, validates its required expectedSha256 before preparation, and persists the prepared output under its shared lock. PlanDocumentWriteSchema is closed and discriminated by document:
125
+
126
+ ```ts
127
+ import { inspectPlan, writePlanDocument } from 'jg-plans';
128
+ const observed = await inspectPlan('./jg-plans/example');
129
+ if (observed.plan.state === 'readable') {
130
+ await writePlanDocument(observed.directory, {
131
+ document: 'plan',
132
+ expectedSha256: observed.plan.sha256,
133
+ body: '# Revised intent\n',
134
+ set: { revision: 2 },
135
+ });
136
+ }
137
+ ```
138
+
139
+ The plan variant requires an existing digest, a replacement body, and optional core metadata set. Body and supplied metadata are changed together, preserving all unaffected frontmatter bytes. Closed malformed YAML permits body-only replacement; ambiguous/unterminated boundaries fail. A needed line separator is inserted after a closing delimiter at EOF. The existing BOM encoding marker is preserved; on metadata-free source, a leading BOM also present in the supplied body is treated as that same marker. A body-only operation cannot introduce frontmatter into metadata-free source. Replacement strings must encode strictly as UTF-8.
140
+
141
+ The milestones variant accepts `{ document: 'milestones', expectedSha256: string | null, source: string, assignMissingIds?: boolean }`. Null requires an absent file and uses exclusive creation; a digest never recreates deleted files. Source is verbatim unless explicit assignMissingIds is true. That option preserves valid IDs and inserts random UUIDs immediately after unique canonical nonempty status lines. Duplicate/invalid identities, ordinals, or ambiguous insertion targets fail. It returns assigned ordinal/id pairs; without the option, missing IDs remain missing. No timestamps, completion, or approvals are inferred.
142
+
143
+ Pure preparation accepts null original source only for explicit absent milestone creation. Existing-source expectations are checked against strict UTF-8 bytes. Filesystem guards compare the original bytes before decoding. Writes return `{ directory, document, changed, sha256, diagnostics, assignedMilestoneIds }`; a no-op still checks its expectation and skips replacement. Existing permissions are preserved. maxBytes bounds reads rather than the size of the prepared output.
144
+
145
+ `removeMilestones(directory, { expectedSha256 }, { maxBytes? }?)` (RemoveMilestonesSchema) removes only an existing regular UTF-8 milestones.md matching that digest. Missing or changed targets fail SOURCE_CHANGED. The result is `{ directory, document: 'milestones', changed: true, sha256: null, diagnostics: [] }`. The plan, selected directory, coordination files, and history are untouched. Links, nonregular targets, unreadable/oversized source, and unsupported encoding fail. Nonparticipating editors retain a check-to-unlink race.
146
+
147
+ ## Shared document locks
148
+
149
+ `withPlanDocumentLocks(directory, documents, operation)` accepts a nonempty unique selection of plan/milestones and calls operation with the canonical real directory. It acquires `.plan.md.jg-plans.lock` before `.milestones.md.jg-plans.lock` regardless of input order, then releases in reverse order. Every filesystem mutation, including initial creation, uses this namespace. Failed creation releases locks before owned empty-directory cleanup.
150
+
151
+ Use pure preparation and consumer persistence inside a callback; nesting filesystem mutations reacquires the held lock and fails busy. Keep callbacks bounded to one operation, never across model/agent turns. Locks use jg-fs ownership checks, support local filesystems, and fail immediately for busy or dead owners. There is no TTL, polling, or automatic takeover. After a crash, inspect the owner and affected documents before explicit operator recovery through jg-fs; never remove an unfamiliar lock.
152
+
153
+ LOCK_BUSY and acquisition LOCK_FAILED report phase/document/reason and mutationOutcome not-applied. Release LOCK_FAILED reports operationCompleted and mutationOutcome uncertain when the callback started, even if all writes landed; inspect/reconcile durable state before retrying. Callback and release failures are retained in its internal AggregateError cause. These locks coordinate participants, not every editor. They provide neither a cross-document transaction nor an HQ journal.
154
+
92
155
  ## Failures
93
156
 
94
157
  Invalid operation arguments throw JG_PLANS_INVALID_INPUT: Invalid jg-plans input. File absence, formatting defects, encoding failures, and read failures are returned as inspection diagnostics. Mutations throw operational errors for unsupported text/edits, existing destinations, detected source changes, and read/write failures. See the [error reference](../errors/README.md) for exact codes/messages and per-operation mapping. ERRORS exposes the stable operational codes.
package/cli/README.md CHANGED
@@ -5,6 +5,7 @@ The executable is tested from the packed archive. Check the [website's availabil
5
5
  ```sh
6
6
  jg-plans inspect --directory ./jg-plans/example
7
7
  jg-plans inspect --directory ./custom-root/example --json
8
+ jg-plans inspect --directory ./jg-plans/example --fingerprint-exclude '["created_at","updated_at","revision","status"]' --json
8
9
  jg-plans inspect --directory ./coding-agent-planning/example --include-body --json
9
10
  jg-plans check --directory ./jg-plans/example --json
10
11
  jg-plans --help
@@ -15,7 +16,9 @@ jg-plans --version
15
16
 
16
17
  Inspect reads only the selected plan.md and optional milestones.md. It reports readable, missing, or unreadable files, recognized metadata and progress, and useful partial observations. Unknown metadata and custom status strings remain visible. It does not read history, discover other plans, restore deleted files, or infer permission or completion from absence.
17
18
 
18
- Bodies and complete document sources are excluded by default. `--include-body` explicitly includes them for inspect. Metadata, titles, markers, and locations remain part of ordinary observations.
19
+ Bodies and complete document sources are excluded by default. `--include-body` explicitly includes them for inspect, including the whole milestone document body. Raw metadata for both documents, titles, markers, locations, source ranges, and exact-byte SHA-256 values for readable files remain part of ordinary observations.
20
+
21
+ `--fingerprint-exclude <JSON-array>` requests a structural plan fingerprint and supplies its explicit top-level metadata exclusions. Pass `[]` to cover every key. The array must contain strings; duplicate keys are applied once. This flag is accepted only by inspect. The result includes `data.fingerprint` with sha256, the unique applied keys, and diagnostics. Invalid framing/YAML or unavailable structure yields a null fingerprint without hiding useful observations. It is an observation, never permission or a conformance gate. See [structural fingerprints](../api/README.md#structural-fingerprints).
19
22
 
20
23
  ## Check
21
24
 
@@ -39,9 +42,9 @@ Create input is a closed JSON object with body, optional metadata, and optional
39
42
  }
40
43
  ```
41
44
 
42
- Slug and root are command flags, not JSON fields. Creation uses ./jg-plans relative to the working directory by default, without root discovery or migration. It creates an exclusive timestamp_slug directory, defaults missing metadata, preserves supplied body/milestones, and never overwrites. Slugs use lowercase ASCII kebab case; the generated component must fit 64 characters. Returned create data contains directory, plan metadata/rawMetadata, and milestone summaries or null. No body is emitted.
45
+ Slug and root are command flags, not JSON fields. Creation uses ./jg-plans relative to the working directory by default, without root discovery or migration. It creates an exclusive timestamp_slug directory, defaults missing metadata, preserves supplied body/milestones, and never overwrites. Slugs use lowercase ASCII kebab case; the generated component must fit 64 characters. Returned create data contains directory, plan metadata/rawMetadata/sha256, and milestones as { sha256, milestones: summaries } or null. No body is emitted.
43
46
 
44
- Update input selects one document. For a plan, use `{ "document": "plan", "set": { "status": "done", "revision": 2 } }`. The only allowed fields are id, created_at, updated_at, revision, and status. For milestones:
47
+ Update input selects one document and optionally expectedSha256 from a fresh inspection; a stale or deleted guarded target fails before preparation. For a plan, use `{ "document": "plan", "set": { "status": "done", "revision": 2 } }`. The only allowed fields are id, created_at, updated_at, revision, and status. For milestones:
45
48
 
46
49
  ```json
47
50
  {
@@ -50,11 +53,26 @@ Update input selects one document. For a plan, use `{ "document": "plan", "set":
50
53
  }
51
54
  ```
52
55
 
53
- Select by ordinal or UUID id, with one change per target. Set status and/or UUID id. The result contains directory, document, and changed. Equal values skip the write. Revision/timestamps never advance implicitly. Overview rows, prose, and extension metadata remain byte-identical. Missing selected files are not recreated. Existing misplaced markers remain in place with diagnostics; new markers require canonical insertion points.
56
+ Select by ordinal or UUID id, with one change per target. Set status and/or UUID id. The result contains directory, document, changed, sha256, and assignedMilestoneIds (empty for narrow updates). Equal values skip the write. Revision/timestamps never advance implicitly. Overview rows, prose, and extension metadata remain byte-identical. Missing selected files are not recreated. Existing misplaced markers remain in place with diagnostics; new markers require canonical insertion points.
54
57
 
55
58
  Input comes from a file or `-` for stdin and must be valid UTF-8 JSON, at most 1 MiB by default. `--max-bytes` overrides both mutation-input and selected-document limits. Unknown JSON fields, nonconforming requested core values, invalid encoding, unsupported YAML, ambiguous edits, destination collisions, detected source changes, and I/O failures exit 2. Unsupported edits explain the case in error.data.reason; cleanup failures report owned paths. See [the API](../api/README.md) for precise preservation and unsupported-edit limits.
56
59
 
57
- Serialize commands that edit the same document, including the whole read/edit/write operation. A prior-content check detects observed intervening changes, but the comparison-to-rename race remains. The CLI supplies no lock, queue, retry, or history service.
60
+ All mutations share local document locks with library/HQ consumers. Busy and dead owners fail immediately; recovery requires explicit operator inspection. A prior-content check detects observed intervening changes, but nonparticipating writers retain a comparison-to-rename race. Release failures report uncertain mutation outcomes even when bytes were written. There is no cross-document transaction, automatic retry, or history service.
61
+
62
+ ## Write and remove milestones
63
+
64
+ ```sh
65
+ jg-plans write --directory ./jg-plans/example --input write.json --json
66
+ jg-plans remove-milestones --directory ./jg-plans/example --input remove.json --json
67
+ ```
68
+
69
+ Write accepts PlanDocumentWriteSchema. For plan bodies, use `{ "document": "plan", "expectedSha256": "<observed 64-character lowercase hash>", "body": "# Revised intent\n", "set": { "revision": 2 } }`. Set is optional and changes only core metadata. Body-only writes can preserve closed malformed YAML; unterminated framing fails. Unaffected frontmatter, BOM, and permissions are retained.
70
+
71
+ For complete milestone replacement use `{ "document": "milestones", "expectedSha256": "<observed hash>", "source": "## Milestone 1: Work\n**Status:** pending\n", "assignMissingIds": true }`. Null expectedSha256 explicitly creates an absent milestone file exclusively. Existing expectations never recreate deleted targets. UUID assignment is opt-in, preserves valid IDs, and rejects ambiguous identities/insertion points. Write output includes directory, document, changed, sha256, and assignedMilestoneIds, never full source.
72
+
73
+ Remove input is `{ "expectedSha256": "<observed milestone hash>" }`. Only matching regular UTF-8 milestones.md is removed; absent/stale targets fail SOURCE_CHANGED. Successful data contains directory, document milestones, changed true, and sha256 null. No plan, directory, or coordination file is removed.
74
+
75
+ The placeholders above must be replaced with actual hashes returned by inspect/create or a preceding mutation. Every command uses the same input limits, safe errors, envelope, and exits. See [the API](../api/README.md#pure-preparation-and-guarded-document-writes) for source preservation and [shared document locks](../api/README.md#shared-document-locks).
58
76
 
59
77
  ## Limits and output
60
78
 
@@ -1,6 +1,6 @@
1
1
  import { parseStrictArgs } from 'jg-process';
2
2
  import { planErrors } from "../errors/index.js";
3
- import { ReadOptionsSchema } from "../protocol/index.js";
3
+ import { PlanFingerprintOptionsSchema, ReadOptionsSchema, } from "../protocol/index.js";
4
4
  /**
5
5
  * Validate a single CLI request before reading documents.
6
6
  * @throws
@@ -18,6 +18,7 @@ export const parseArguments = (args) => {
18
18
  root: { type: 'string' },
19
19
  'max-bytes': { type: 'string' },
20
20
  'include-body': { type: 'boolean' },
21
+ 'fingerprint-exclude': { type: 'string' },
21
22
  json: { type: 'boolean' },
22
23
  help: { type: 'boolean', short: 'h' },
23
24
  version: { type: 'boolean', short: 'v' },
@@ -33,8 +34,21 @@ export const parseArguments = (args) => {
33
34
  return { command: 'help', json: flags.json, includeBody: false };
34
35
  if (flags.version)
35
36
  return { command: 'version', json: flags.json, includeBody: false };
36
- if (first !== 'inspect' && first !== 'check' && first !== 'create' && first !== 'update')
37
+ if (!['inspect', 'check', 'create', 'update', 'write', 'remove-milestones'].includes(first))
37
38
  throw planErrors.create('invalidInput');
39
+ let fingerprint;
40
+ if (flags['fingerprint-exclude'] !== undefined) {
41
+ if (first !== 'inspect')
42
+ throw planErrors.create('invalidInput');
43
+ try {
44
+ fingerprint = PlanFingerprintOptionsSchema.parse({
45
+ excludeMetadataKeys: JSON.parse(flags['fingerprint-exclude']),
46
+ });
47
+ }
48
+ catch {
49
+ throw planErrors.create('invalidInput');
50
+ }
51
+ }
38
52
  if (first === 'create') {
39
53
  if (!flags.slug || !flags.input || flags.directory || flags['include-body'])
40
54
  throw planErrors.create('invalidInput');
@@ -42,7 +56,7 @@ export const parseArguments = (args) => {
42
56
  else {
43
57
  if (!flags.directory || flags.root || flags.slug)
44
58
  throw planErrors.create('invalidInput');
45
- if (first === 'update'
59
+ if (first === 'update' || first === 'write' || first === 'remove-milestones'
46
60
  ? !flags.input || flags['include-body']
47
61
  : Boolean(flags.input) || (first === 'check' && flags['include-body']))
48
62
  throw planErrors.create('invalidInput');
@@ -60,6 +74,7 @@ export const parseArguments = (args) => {
60
74
  ...(flags.slug ? { slug: flags.slug } : {}),
61
75
  ...(flags.root ? { root: flags.root } : {}),
62
76
  includeBody: flags['include-body'],
77
+ ...(fingerprint ? { fingerprint } : {}),
63
78
  json: flags.json,
64
79
  ...(maxBytes === undefined ? {} : { maxBytes }),
65
80
  };
@@ -6,22 +6,24 @@ import { z } from 'zod';
6
6
  import { checkPlan } from "../documents/index.js";
7
7
  import { decodeSource } from "../edits/index.js";
8
8
  import { planErrors } from "../errors/index.js";
9
- import { createPlan, inspectPlan, updatePlan } from "../files/index.js";
10
- import { CreatePlanOptionsSchema, DEFAULT_MAX_BYTES, PlanUpdateSchema, } from "../protocol/index.js";
9
+ import { createPlan, inspectPlan, updatePlan, writePlanDocument, removeMilestones, } from "../files/index.js";
10
+ import { CreatePlanOptionsSchema, DEFAULT_MAX_BYTES, PlanUpdateSchema, PlanDocumentWriteSchema, RemoveMilestonesSchema, } from "../protocol/index.js";
11
11
  import { parseArguments } from "./arguments.js";
12
- const HELP = `jg-plans — inspect Markdown plans without enforcing a workflow
12
+ const HELP = `jg-plans — read and edit Markdown plans without enforcing a workflow
13
13
 
14
14
  Commands:
15
- inspect --directory <path> [--include-body] [--json] [--max-bytes <bytes>]
15
+ inspect --directory <path> [--fingerprint-exclude <JSON-array>] [--include-body] [--json] [--max-bytes <bytes>]
16
16
  check --directory <path> [--json] [--max-bytes <bytes>]
17
17
  create --slug <slug> --input <file-or-dash> [--root <path>] [--json] [--max-bytes <bytes>]
18
18
  update --directory <path> --input <file-or-dash> [--json] [--max-bytes <bytes>]
19
+ write --directory <path> --input <file-or-dash> [--json] [--max-bytes <bytes>]
20
+ remove-milestones --directory <path> --input <file-or-dash> [--json] [--max-bytes <bytes>]
19
21
  --help, -h
20
22
  --version, -v
21
23
 
22
24
  Only the selected plan.md and optional milestones.md are read.
23
25
  The default bound is 1048576 bytes per file. Checks are optional.
24
- Updates to the same document must run sequentially. No history is required.
26
+ Mutations share local document locks. Busy owners require explicit recovery. No history is required.
25
27
  Exit 0: completed inspection, successful mutation, or conforming check; 1: conformance errors;
26
28
  2: invalid command or operational failure. Missing files do not imply completion.`;
27
29
  const projectInspection = (inspection, includeBody) => {
@@ -31,14 +33,18 @@ const projectInspection = (inspection, includeBody) => {
31
33
  const milestoneValue = milestones.state === 'readable' ? milestones.value : undefined;
32
34
  return {
33
35
  directory: inspection.directory,
36
+ ...(inspection.fingerprint ? { fingerprint: inspection.fingerprint } : {}),
34
37
  plan: {
35
38
  state: plan.state,
36
39
  path: plan.path,
37
40
  ...(planValue
38
41
  ? {
39
42
  metadata: planValue.metadata,
43
+ sha256: plan.state === 'readable' ? plan.sha256 : undefined,
40
44
  rawMetadata: planValue.rawMetadata,
41
45
  frontmatter: planValue.frontmatter,
46
+ preamble: planValue.preamble,
47
+ sections: planValue.sections,
42
48
  ...(includeBody ? { source: planValue.source, body: planValue.body } : {}),
43
49
  }
44
50
  : {}),
@@ -49,12 +55,18 @@ const projectInspection = (inspection, includeBody) => {
49
55
  ...(milestoneValue
50
56
  ? {
51
57
  overview: milestoneValue.overview,
58
+ sha256: milestones.state === 'readable' ? milestones.sha256 : undefined,
59
+ rawMetadata: milestoneValue.rawMetadata,
60
+ frontmatter: milestoneValue.frontmatter,
61
+ preamble: milestoneValue.preamble,
62
+ sections: milestoneValue.sections,
63
+ overviewTables: milestoneValue.overviewTables,
52
64
  overviewPresent: milestoneValue.overviewPresent,
53
65
  milestones: milestoneValue.milestones.map(({ body, ...entry }) => ({
54
66
  ...entry,
55
67
  ...(includeBody ? { body } : {}),
56
68
  })),
57
- ...(includeBody ? { source: milestoneValue.source } : {}),
69
+ ...(includeBody ? { source: milestoneValue.source, body: milestoneValue.body } : {}),
58
70
  }
59
71
  : {}),
60
72
  },
@@ -118,15 +130,24 @@ export const runCli = async (args) => {
118
130
  diagnostics = result.diagnostics;
119
131
  data = {
120
132
  directory: result.directory,
121
- plan: { metadata: result.plan.metadata, rawMetadata: result.plan.rawMetadata },
122
- milestones: result.milestones?.milestones.map((entry) => ({
123
- ordinal: entry.ordinal,
124
- ordinalText: entry.ordinalText,
125
- title: entry.title,
126
- id: entry.id,
127
- status: entry.status,
128
- location: entry.location,
129
- })) ?? null,
133
+ plan: {
134
+ sha256: result.plan.sha256,
135
+ metadata: result.plan.metadata,
136
+ rawMetadata: result.plan.rawMetadata,
137
+ },
138
+ milestones: result.milestones
139
+ ? {
140
+ sha256: result.milestones.sha256,
141
+ milestones: result.milestones.milestones.map((entry) => ({
142
+ ordinal: entry.ordinal,
143
+ ordinalText: entry.ordinalText,
144
+ title: entry.title,
145
+ id: entry.id,
146
+ status: entry.status,
147
+ location: entry.location,
148
+ })),
149
+ }
150
+ : null,
130
151
  };
131
152
  }
132
153
  else if (options.command === 'update') {
@@ -136,10 +157,48 @@ export const runCli = async (args) => {
136
157
  throw planErrors.create('invalidInput');
137
158
  const result = await updatePlan(options.directory, parsed.data, options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes });
138
159
  diagnostics = result.diagnostics;
139
- data = { directory: result.directory, document: result.document, changed: result.changed };
160
+ data = {
161
+ directory: result.directory,
162
+ document: result.document,
163
+ changed: result.changed,
164
+ sha256: result.sha256,
165
+ ...('assignedMilestoneIds' in result
166
+ ? { assignedMilestoneIds: result.assignedMilestoneIds }
167
+ : {}),
168
+ };
169
+ }
170
+ else if (options.command === 'write' || options.command === 'remove-milestones') {
171
+ const raw = await readMutationInput(options.input, options.maxBytes ?? DEFAULT_MAX_BYTES);
172
+ const readOptions = options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes };
173
+ let result;
174
+ if (options.command === 'write') {
175
+ const parsed = PlanDocumentWriteSchema.safeParse(raw);
176
+ if (!parsed.success)
177
+ throw planErrors.create('invalidInput');
178
+ result = await writePlanDocument(options.directory, parsed.data, readOptions);
179
+ }
180
+ else {
181
+ const parsed = RemoveMilestonesSchema.safeParse(raw);
182
+ if (!parsed.success)
183
+ throw planErrors.create('invalidInput');
184
+ result = await removeMilestones(options.directory, parsed.data, readOptions);
185
+ }
186
+ diagnostics = result.diagnostics;
187
+ data = {
188
+ directory: result.directory,
189
+ document: result.document,
190
+ changed: result.changed,
191
+ sha256: result.sha256,
192
+ ...('assignedMilestoneIds' in result
193
+ ? { assignedMilestoneIds: result.assignedMilestoneIds }
194
+ : {}),
195
+ };
140
196
  }
141
197
  else {
142
- const inspection = await inspectPlan(options.directory, options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes });
198
+ const inspection = await inspectPlan(options.directory, {
199
+ ...(options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes }),
200
+ ...(options.fingerprint ? { fingerprint: options.fingerprint } : {}),
201
+ });
143
202
  diagnostics = inspection.diagnostics;
144
203
  const operationalFailure = diagnostics.some((item) => item.code === 'UNREADABLE_DOCUMENT' || item.code === 'FILE_TOO_LARGE');
145
204
  if (options.command === 'check') {
@@ -1,11 +1,13 @@
1
+ import type { IPlanFingerprintOptions } from '../protocol/index.ts';
1
2
  /** Validated CLI requests; file paths stay explicit. */
2
3
  export type IArguments = {
3
- command: 'inspect' | 'check' | 'create' | 'update' | 'help' | 'version';
4
+ command: 'inspect' | 'check' | 'create' | 'update' | 'write' | 'remove-milestones' | 'help' | 'version';
4
5
  directory?: string;
5
6
  maxBytes?: number;
6
7
  input?: string;
7
8
  slug?: string;
8
9
  root?: string;
9
10
  includeBody: boolean;
11
+ fingerprint?: IPlanFingerprintOptions;
10
12
  json: boolean;
11
13
  };
@@ -0,0 +1,19 @@
1
+ import { type IPlanFingerprintOptions } from '../protocol/index.ts';
2
+ import type { IParsedPlan, IPlanFingerprint } from './types.ts';
3
+ /** Package-private reuse of an existing observation, including absent/unreadable plans. */
4
+ export declare const fingerprintObservation: (plan: IParsedPlan | null, options: IPlanFingerprintOptions) => IPlanFingerprint;
5
+ /**
6
+ * Hash strict deterministic JSON of the exact body and raw metadata after explicit exclusions.
7
+ * Missing preferred metadata does not prevent hashing. Ambiguous/non-JSON-safe YAML does.
8
+ * The digest is an observation, never an approval or completion decision.
9
+ * @param source Original plan Markdown.
10
+ * @param options Metadata keys to omit; defaults to none and reports unique applied keys.
11
+ * @returns Lowercase SHA-256, or null with diagnostics when the structure is unavailable.
12
+ * @throws
13
+ * - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
14
+ * @example
15
+ * import { fingerprintPlan } from 'jg-plans';
16
+ * const fingerprint = fingerprintPlan('# Work', { excludeMetadataKeys: ['status'] });
17
+ * console.log(fingerprint.sha256);
18
+ */
19
+ export declare const fingerprintPlan: (source: string, options?: IPlanFingerprintOptions) => IPlanFingerprint;
@@ -0,0 +1,56 @@
1
+ import { hashValue } from 'jg-crypto';
2
+ import { stringifyJsonDeterministically } from 'jg-utilities';
3
+ import { planErrors } from "../errors/index.js";
4
+ import { PlanFingerprintOptionsSchema } from "../protocol/index.js";
5
+ import { parsePlan } from "./plan.js";
6
+ import { diagnostic } from "./utilities.js";
7
+ const UNAVAILABLE_CODES = new Set([
8
+ 'UNTERMINATED_FRONTMATTER',
9
+ 'INVALID_YAML',
10
+ 'YAML_WARNING',
11
+ 'METADATA_NOT_MAPPING',
12
+ 'YAML_VALUE_UNAVAILABLE',
13
+ ]);
14
+ /** Package-private reuse of an existing observation, including absent/unreadable plans. */
15
+ export const fingerprintObservation = (plan, options) => {
16
+ const parsed = PlanFingerprintOptionsSchema.safeParse(options);
17
+ if (!parsed.success)
18
+ throw planErrors.create('invalidInput');
19
+ const excludeMetadataKeys = [...new Set(parsed.data.excludeMetadataKeys ?? [])];
20
+ const diagnostics = plan?.diagnostics.filter((item) => UNAVAILABLE_CODES.has(item.code)) ?? [];
21
+ if (plan && !diagnostics.length) {
22
+ try {
23
+ const excluded = new Set(excludeMetadataKeys);
24
+ const metadata = Object.fromEntries(Object.entries(plan.frontmatter ? plan.rawMetadata : {}).filter(([key]) => !excluded.has(key)));
25
+ const payload = stringifyJsonDeterministically({ body: plan.body, metadata }, { strict: true });
26
+ return { sha256: hashValue(payload), excludeMetadataKeys, diagnostics: [] };
27
+ }
28
+ catch {
29
+ // Serialization failures may include source values; expose only a safe observation.
30
+ }
31
+ }
32
+ return {
33
+ sha256: null,
34
+ excludeMetadataKeys,
35
+ diagnostics: diagnostics.length
36
+ ? diagnostics
37
+ : [
38
+ diagnostic('plan', 'FINGERPRINT_UNAVAILABLE', 'A structural fingerprint is unavailable for this document.'),
39
+ ],
40
+ };
41
+ };
42
+ /**
43
+ * Hash strict deterministic JSON of the exact body and raw metadata after explicit exclusions.
44
+ * Missing preferred metadata does not prevent hashing. Ambiguous/non-JSON-safe YAML does.
45
+ * The digest is an observation, never an approval or completion decision.
46
+ * @param source Original plan Markdown.
47
+ * @param options Metadata keys to omit; defaults to none and reports unique applied keys.
48
+ * @returns Lowercase SHA-256, or null with diagnostics when the structure is unavailable.
49
+ * @throws
50
+ * - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
51
+ * @example
52
+ * import { fingerprintPlan } from 'jg-plans';
53
+ * const fingerprint = fingerprintPlan('# Work', { excludeMetadataKeys: ['status'] });
54
+ * console.log(fingerprint.sha256);
55
+ */
56
+ export const fingerprintPlan = (source, options = {}) => fingerprintObservation(parsePlan(source), options);
@@ -1,5 +1,6 @@
1
1
  export { checkPlan } from './check.ts';
2
2
  export { parseMilestones } from './milestones.ts';
3
3
  export { parsePlan } from './plan.ts';
4
+ export { fingerprintPlan } from './fingerprints.ts';
4
5
  export { sourceLines } from './utilities.ts';
5
- export type { ICheckResult, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, IParsedPlan, } from './types.ts';
6
+ export type { ICheckResult, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, IParsedPlan, ISourceSpan, IHeadingSection, IPlanFingerprint, } from './types.ts';
@@ -1,5 +1,6 @@
1
1
  export { checkPlan } from "./check.js";
2
2
  export { parseMilestones } from "./milestones.js";
3
3
  export { parsePlan } from "./plan.js";
4
+ export { fingerprintPlan } from "./fingerprints.js";
4
5
  // Package-private lexical support; omitted from the package's public root exports.
5
6
  export { sourceLines } from "./utilities.js";