taurus 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +518 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +1529 -0
- data/Rakefile +7 -0
- data/TODO.impl/01-architecture.md +217 -0
- data/TODO.impl/02-ffi-declarations.md +236 -0
- data/TODO.impl/03-document-node-element-nodeset.md +382 -0
- data/TODO.impl/04-sax-parser.md +203 -0
- data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
- data/benchmark/README.md +168 -0
- data/benchmark/taurus_vs_nokogiri.rb +105 -0
- data/docs/ARCHITECTURE.adoc +559 -0
- data/docs/BUILD.md +395 -0
- data/docs/ERROR_MESSAGES.md +458 -0
- data/docs/FFI_ARCHITECTURE.md +439 -0
- data/docs/FUTURE_VISION.md +303 -0
- data/docs/GITHUB_ACTIONS.md +293 -0
- data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +459 -0
- data/docs/PERFORMANCE.adoc +668 -0
- data/docs/PERFORMANCE.md +448 -0
- data/docs/RELEASE_NOTES_v1.0.0.md +515 -0
- data/docs/XPATH_SPEC_COMPLIANCE.md +298 -0
- data/docs/completion/taurus.bash +86 -0
- data/docs/completion/taurus.zsh +74 -0
- data/docs/man/taurus-format.1 +227 -0
- data/docs/man/taurus-parse.1 +178 -0
- data/docs/man/taurus-xpath.1 +312 -0
- data/docs/man/taurus.1 +160 -0
- data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +217 -0
- data/docs/v0.9.0_RELEASE_SUMMARY.md +281 -0
- data/docs/v1.0.0_CONTINUATION_PLAN.md +172 -0
- data/docs/v1.0.0_CONTINUATION_PROMPT.md +382 -0
- data/docs/v1.0.0_SESSION_6_CONTINUATION.md +434 -0
- data/docs/v1.0.0_SESSION_6_PROMPT.md +231 -0
- data/docs/v1.0.0_STATUS_TRACKER.md +224 -0
- data/docs/v1.1.0_CONTINUATION_PLAN.md +299 -0
- data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +201 -0
- data/docs/v1.1.0_SESSION_3_PROMPT.md +223 -0
- data/docs/v1.1.0_STATUS_TRACKER.md +355 -0
- data/docs/xml-performance.adoc +115 -0
- data/docs/xpath-performance.adoc +379 -0
- data/lib/taurus/version.rb +5 -0
- data/lib/taurus/xml/attr.rb +43 -0
- data/lib/taurus/xml/c14n.rb +23 -0
- data/lib/taurus/xml/cdata.rb +16 -0
- data/lib/taurus/xml/comment.rb +16 -0
- data/lib/taurus/xml/css_to_xpath.rb +177 -0
- data/lib/taurus/xml/doc_type.rb +54 -0
- data/lib/taurus/xml/document.rb +202 -0
- data/lib/taurus/xml/document_fragment.rb +42 -0
- data/lib/taurus/xml/element.rb +278 -0
- data/lib/taurus/xml/ffi.rb +420 -0
- data/lib/taurus/xml/namespace.rb +43 -0
- data/lib/taurus/xml/node.rb +221 -0
- data/lib/taurus/xml/node_set.rb +143 -0
- data/lib/taurus/xml/parse_options.rb +19 -0
- data/lib/taurus/xml/processing_instruction.rb +26 -0
- data/lib/taurus/xml/sax/document.rb +45 -0
- data/lib/taurus/xml/sax/parser.rb +148 -0
- data/lib/taurus/xml/sax.rb +12 -0
- data/lib/taurus/xml/searchable.rb +93 -0
- data/lib/taurus/xml/text.rb +16 -0
- data/lib/taurus/xml.rb +29 -0
- data/lib/taurus.rb +7 -0
- data/taurus.gemspec +42 -0
- metadata +157 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# Taurus v1.0.0 - Session 1 Continuation Prompt
|
|
2
|
+
|
|
3
|
+
**Date**: 2024-12-05
|
|
4
|
+
**Current Version**: 0.9.0 (Released)
|
|
5
|
+
**Target Version**: 1.0.0
|
|
6
|
+
**Current Session**: Session 1 of 12
|
|
7
|
+
**Current Feature**: Enhanced Error Messages
|
|
8
|
+
**Current Phase**: Phase 1 - Error Infrastructure
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
Taurus v0.9.0 has been successfully released with:
|
|
13
|
+
- ✅ Custom namespace API (Ruby layer)
|
|
14
|
+
- ✅ XPath performance optimizations (2-3× faster)
|
|
15
|
+
- ✅ Complete benchmarking (all 27 XPath functions)
|
|
16
|
+
- ✅ All tests passing (16/16)
|
|
17
|
+
- ✅ Documentation complete
|
|
18
|
+
|
|
19
|
+
v1.0.0 will add three major features:
|
|
20
|
+
1. **Enhanced Error Messages** (Foundation - Week 1)
|
|
21
|
+
2. **C-Level Namespace Registry** (Performance - Week 2)
|
|
22
|
+
3. **SAX Parser Interface** (Enterprise Scale - Week 3)
|
|
23
|
+
|
|
24
|
+
## Session 1 Objective
|
|
25
|
+
|
|
26
|
+
**Feature**: Enhanced Error Messages
|
|
27
|
+
**Phase**: Phase 1 - Error Infrastructure
|
|
28
|
+
**Time**: 2 hours
|
|
29
|
+
**Priority**: 🔥 High (Foundation for all other features)
|
|
30
|
+
|
|
31
|
+
### Goals
|
|
32
|
+
1. Create error infrastructure in C (`errors.c`, `errors.h`)
|
|
33
|
+
2. Define error codes and context structure
|
|
34
|
+
3. Implement basic error creation/retrieval API
|
|
35
|
+
4. Add FFI bindings for error extraction
|
|
36
|
+
5. Test basic error flow
|
|
37
|
+
|
|
38
|
+
### Success Criteria
|
|
39
|
+
- [ ] `lib/src/errors.c` created (~400 lines)
|
|
40
|
+
- [ ] `lib/src/errors.h` created (~100 lines)
|
|
41
|
+
- [ ] 15+ error codes defined
|
|
42
|
+
- [ ] `TaurusError` struct with context fields
|
|
43
|
+
- [ ] FFI bindings working
|
|
44
|
+
- [ ] Basic tests passing
|
|
45
|
+
- [ ] Zero memory leaks
|
|
46
|
+
|
|
47
|
+
## Planning Documents
|
|
48
|
+
|
|
49
|
+
**MUST READ** before starting:
|
|
50
|
+
1. [`docs/v1.0.0_CONTINUATION_PLAN.md`](v1.0.0_CONTINUATION_PLAN.md) - Complete feature plan
|
|
51
|
+
2. [`docs/v1.0.0_STATUS_TRACKER.md`](v1.0.0_STATUS_TRACKER.md) - Current progress
|
|
52
|
+
3. [`docs/v0.9.0_RELEASE_SUMMARY.md`](v0.9.0_RELEASE_SUMMARY.md) - What was just completed
|
|
53
|
+
|
|
54
|
+
## Implementation Steps
|
|
55
|
+
|
|
56
|
+
### Step 1: Create Error Header (30 min)
|
|
57
|
+
|
|
58
|
+
Create `lib/src/errors.h`:
|
|
59
|
+
|
|
60
|
+
```c
|
|
61
|
+
#ifndef TAURUS_ERRORS_H
|
|
62
|
+
#define TAURUS_ERRORS_H
|
|
63
|
+
|
|
64
|
+
#include <stddef.h>
|
|
65
|
+
#include <stdbool.h>
|
|
66
|
+
|
|
67
|
+
// Error codes (add more as needed)
|
|
68
|
+
typedef enum {
|
|
69
|
+
TAURUS_ERROR_NONE = 0,
|
|
70
|
+
|
|
71
|
+
// Parse errors (1xx)
|
|
72
|
+
TAURUS_ERROR_PARSE_UNCLOSED_TAG = 100,
|
|
73
|
+
TAURUS_ERROR_PARSE_INVALID_ATTR = 101,
|
|
74
|
+
TAURUS_ERROR_PARSE_ENCODING = 102,
|
|
75
|
+
TAURUS_ERROR_PARSE_NAMESPACE = 103,
|
|
76
|
+
TAURUS_ERROR_PARSE_MALFORMED = 104,
|
|
77
|
+
|
|
78
|
+
// XPath errors (2xx)
|
|
79
|
+
TAURUS_ERROR_XPATH_SYNTAX = 200,
|
|
80
|
+
TAURUS_ERROR_XPATH_FUNCTION = 201,
|
|
81
|
+
TAURUS_ERROR_XPATH_TYPE_MISMATCH = 202,
|
|
82
|
+
TAURUS_ERROR_XPATH_NAMESPACE = 203,
|
|
83
|
+
TAURUS_ERROR_XPATH_UNKNOWN_AXIS = 204,
|
|
84
|
+
|
|
85
|
+
// Evaluation errors (3xx)
|
|
86
|
+
TAURUS_ERROR_EVAL_CONTEXT = 300,
|
|
87
|
+
TAURUS_ERROR_EVAL_ARGUMENT = 301,
|
|
88
|
+
TAURUS_ERROR_EVAL_OVERFLOW = 302,
|
|
89
|
+
|
|
90
|
+
// Generic errors (9xx)
|
|
91
|
+
TAURUS_ERROR_OUT_OF_MEMORY = 900,
|
|
92
|
+
TAURUS_ERROR_INTERNAL = 999
|
|
93
|
+
} TaurusErrorCode;
|
|
94
|
+
|
|
95
|
+
// Error context structure
|
|
96
|
+
typedef struct {
|
|
97
|
+
TaurusErrorCode code;
|
|
98
|
+
char* message; // Primary error message
|
|
99
|
+
char* context_snippet; // Code snippet around error (optional)
|
|
100
|
+
int line; // Line number (1-based, 0 if N/A)
|
|
101
|
+
int column; // Column number (1-based, 0 if N/A)
|
|
102
|
+
size_t byte_offset; // Byte offset in input (0 if N/A)
|
|
103
|
+
} TaurusError;
|
|
104
|
+
|
|
105
|
+
// Error API
|
|
106
|
+
TaurusError* taurus_error_create(TaurusErrorCode code, const char* message);
|
|
107
|
+
TaurusError* taurus_error_create_with_context(
|
|
108
|
+
TaurusErrorCode code,
|
|
109
|
+
const char* message,
|
|
110
|
+
const char* input,
|
|
111
|
+
size_t byte_offset,
|
|
112
|
+
int line,
|
|
113
|
+
int column
|
|
114
|
+
);
|
|
115
|
+
void taurus_error_free(TaurusError* error);
|
|
116
|
+
const char* taurus_error_get_message(TaurusError* error);
|
|
117
|
+
const char* taurus_error_get_full_message(TaurusError* error);
|
|
118
|
+
|
|
119
|
+
// Global error state (thread-local in future)
|
|
120
|
+
void taurus_error_set(TaurusError* error);
|
|
121
|
+
TaurusError* taurus_error_get(void);
|
|
122
|
+
void taurus_error_clear(void);
|
|
123
|
+
bool taurus_error_has_error(void);
|
|
124
|
+
|
|
125
|
+
#endif // TAURUS_ERRORS_H
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Checklist**:
|
|
129
|
+
- [ ] File created
|
|
130
|
+
- [ ] Error codes defined (15+)
|
|
131
|
+
- [ ] `TaurusError` struct defined
|
|
132
|
+
- [ ] API functions declared
|
|
133
|
+
- [ ] Global error state functions declared
|
|
134
|
+
|
|
135
|
+
### Step 2: Implement Error Functions (60 min)
|
|
136
|
+
|
|
137
|
+
Create `lib/src/errors.c`:
|
|
138
|
+
|
|
139
|
+
**Key Functions**:
|
|
140
|
+
1. `taurus_error_create()` - Basic error creation
|
|
141
|
+
2. `taurus_error_create_with_context()` - Error with line/column
|
|
142
|
+
3. `taurus_error_free()` - Memory cleanup
|
|
143
|
+
4. `taurus_error_get_message()` - Get basic message
|
|
144
|
+
5. `taurus_error_get_full_message()` - Get formatted message with context
|
|
145
|
+
6. Global error state management
|
|
146
|
+
|
|
147
|
+
**Context Snippet Extraction**:
|
|
148
|
+
```c
|
|
149
|
+
static char* extract_context_snippet(const char* input, size_t offset, int line) {
|
|
150
|
+
// Extract ±2 lines around error
|
|
151
|
+
// Format with line numbers
|
|
152
|
+
// Highlight error position with ^
|
|
153
|
+
return formatted_snippet;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Message Formatting**:
|
|
158
|
+
```c
|
|
159
|
+
const char* taurus_error_get_full_message(TaurusError* error) {
|
|
160
|
+
// Format: "message at line X, column Y\n\nContext:\n<snippet>"
|
|
161
|
+
// Reuse allocated buffer or create new one
|
|
162
|
+
return formatted_message;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Checklist**:
|
|
167
|
+
- [ ] All functions implemented
|
|
168
|
+
- [ ] Memory management correct (no leaks)
|
|
169
|
+
- [ ] Context snippet extraction works
|
|
170
|
+
- [ ] Message formatting includes line/column
|
|
171
|
+
- [ ] Global error state works
|
|
172
|
+
- [ ] Code compiles cleanly
|
|
173
|
+
|
|
174
|
+
### Step 3: Add FFI Bindings (20 min)
|
|
175
|
+
|
|
176
|
+
Update `lib/taurus/ffi/errors.rb`:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
module Taurus
|
|
180
|
+
module FFI
|
|
181
|
+
# Error code enum
|
|
182
|
+
module ErrorCode
|
|
183
|
+
NONE = 0
|
|
184
|
+
PARSE_UNCLOSED_TAG = 100
|
|
185
|
+
PARSE_INVALID_ATTR = 101
|
|
186
|
+
# ... etc
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Error struct
|
|
190
|
+
class ErrorStruct < ::FFI::Struct
|
|
191
|
+
layout :code, :int,
|
|
192
|
+
:message, :string,
|
|
193
|
+
:context_snippet, :string,
|
|
194
|
+
:line, :int,
|
|
195
|
+
:column, :int,
|
|
196
|
+
:byte_offset, :size_t
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# FFI bindings
|
|
200
|
+
attach_function :taurus_error_get, [], ErrorStruct.ptr
|
|
201
|
+
attach_function :taurus_error_clear, [], :void
|
|
202
|
+
attach_function :taurus_error_has_error, [], :bool
|
|
203
|
+
attach_function :taurus_error_free, [ErrorStruct.ptr], :void
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
module ErrorHandling
|
|
207
|
+
def self.check_error!
|
|
208
|
+
return unless FFI.taurus_error_has_error
|
|
209
|
+
|
|
210
|
+
error_ptr = FFI.taurus_error_get
|
|
211
|
+
return if error_ptr.null?
|
|
212
|
+
|
|
213
|
+
error = error_ptr.read_pointer
|
|
214
|
+
code = error[:code]
|
|
215
|
+
message = error[:message]
|
|
216
|
+
line = error[:line]
|
|
217
|
+
column = error[:column]
|
|
218
|
+
context = error[:context_snippet]
|
|
219
|
+
|
|
220
|
+
FFI.taurus_error_clear
|
|
221
|
+
|
|
222
|
+
# Raise appropriate Ruby exception
|
|
223
|
+
raise create_ruby_error(code, message, line, column, context)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
def self.create_ruby_error(code, message, line, column, context)
|
|
229
|
+
# Map C error codes to Ruby error classes
|
|
230
|
+
# Add context information
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Checklist**:
|
|
237
|
+
- [ ] Error struct defined in FFI
|
|
238
|
+
- [ ] Error codes mapped
|
|
239
|
+
- [ ] Bindings attached
|
|
240
|
+
- [ ] `check_error!` implemented
|
|
241
|
+
- [ ] Error extraction works
|
|
242
|
+
|
|
243
|
+
### Step 4: Basic Testing (10 min)
|
|
244
|
+
|
|
245
|
+
Create `spec/taurus/errors_spec.rb`:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
RSpec.describe "Error Infrastructure" do
|
|
249
|
+
describe "C error creation" do
|
|
250
|
+
it "creates basic error"
|
|
251
|
+
it "creates error with context"
|
|
252
|
+
it "extracts line/column"
|
|
253
|
+
it "formats context snippet"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
describe "FFI bridging" do
|
|
257
|
+
it "extracts error from C"
|
|
258
|
+
it "maps error codes correctly"
|
|
259
|
+
it "clears error state"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
describe "Memory management" do
|
|
263
|
+
it "frees error memory"
|
|
264
|
+
it "no memory leaks" # Run with valgrind
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Checklist**:
|
|
270
|
+
- [ ] Test file created
|
|
271
|
+
- [ ] Basic tests passing
|
|
272
|
+
- [ ] FFI tests passing
|
|
273
|
+
- [ ] Memory tests passing (valgrind clean)
|
|
274
|
+
|
|
275
|
+
## File Checklist
|
|
276
|
+
|
|
277
|
+
### New Files
|
|
278
|
+
- [ ] `lib/src/errors.c` (~400 lines)
|
|
279
|
+
- [ ] `lib/src/errors.h` (~100 lines)
|
|
280
|
+
- [ ] `lib/taurus/ffi/errors.rb` (enhanced, ~100 lines added)
|
|
281
|
+
- [ ] `spec/taurus/errors_spec.rb` (~100 lines)
|
|
282
|
+
|
|
283
|
+
### Modified Files
|
|
284
|
+
- [ ] `lib/src/Makefile` or `CMakeLists.txt` (add errors.c)
|
|
285
|
+
- [ ] `lib/taurus/ffi.rb` (require errors.rb)
|
|
286
|
+
|
|
287
|
+
## Build & Test Commands
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# Compile C library
|
|
291
|
+
cd build
|
|
292
|
+
cmake .. && make
|
|
293
|
+
|
|
294
|
+
# Run Ruby tests
|
|
295
|
+
cd ..
|
|
296
|
+
bundle exec rspec spec/taurus/errors_spec.rb
|
|
297
|
+
|
|
298
|
+
# Memory leak check
|
|
299
|
+
valgrind --leak-check=full bundle exec rspec spec/taurus/errors_spec.rb
|
|
300
|
+
|
|
301
|
+
# Full test suite
|
|
302
|
+
bundle exec rspec
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Success Verification
|
|
306
|
+
|
|
307
|
+
Before completing session:
|
|
308
|
+
- [ ] All new files created
|
|
309
|
+
- [ ] Code compiles without errors
|
|
310
|
+
- [ ] Tests pass (at least basic error creation)
|
|
311
|
+
- [ ] No memory leaks (valgrind clean)
|
|
312
|
+
- [ ] Documentation comments in code
|
|
313
|
+
- [ ] Status tracker updated
|
|
314
|
+
|
|
315
|
+
## Architecture Principles (CRITICAL)
|
|
316
|
+
|
|
317
|
+
**MUST FOLLOW**:
|
|
318
|
+
1. **MECE** - Errors mutually exclusive, collectively exhaustive
|
|
319
|
+
2. **Object-Oriented** - Error objects, not static strings
|
|
320
|
+
3. **Separation of Concerns** - C errors, FFI bridge, Ruby exceptions separate
|
|
321
|
+
4. **No Code Guards** - Architectural solutions, not `#ifdef`
|
|
322
|
+
5. **Single Responsibility** - Each function does one thing
|
|
323
|
+
6. **Clean Memory** - ALL allocations must be freed
|
|
324
|
+
|
|
325
|
+
**File Size Limits**:
|
|
326
|
+
- Target: ≤700 lines per file
|
|
327
|
+
- Maximum: 800 lines per file
|
|
328
|
+
- Current plan: errors.c (400), errors.h (100) ✅
|
|
329
|
+
|
|
330
|
+
## Common Pitfalls to Avoid
|
|
331
|
+
|
|
332
|
+
1. **Don't** use static buffers for messages (memory leaks)
|
|
333
|
+
2. **Don't** forget to free error context snippets
|
|
334
|
+
3. **Don't** use `printf` for error messages (use allocation)
|
|
335
|
+
4. **Don't** hardcode error messages (use error codes + formatting)
|
|
336
|
+
5. **Don't** skip memory leak testing (valgrind is required)
|
|
337
|
+
|
|
338
|
+
## Next Session Preview
|
|
339
|
+
|
|
340
|
+
**Session 2**: Phase 2 - Parse Error Enhancement
|
|
341
|
+
- Enhance `parse_simple.c` with line/column tracking
|
|
342
|
+
- Implement context snippet extraction for XML
|
|
343
|
+
- Create Ruby `ParseError` classes
|
|
344
|
+
- Test parse error scenarios
|
|
345
|
+
|
|
346
|
+
**Estimated Time**: 2 hours
|
|
347
|
+
|
|
348
|
+
## Questions to Consider
|
|
349
|
+
|
|
350
|
+
1. Should we use thread-local storage for error state?
|
|
351
|
+
2. Should error context snippets be limited in size?
|
|
352
|
+
3. Should we cache error strings or compute each time?
|
|
353
|
+
4. Should we include stack traces in internal errors?
|
|
354
|
+
|
|
355
|
+
**Default Answers** (for now):
|
|
356
|
+
1. No - single-threaded first, add later if needed
|
|
357
|
+
2. Yes - max 200 chars per snippet
|
|
358
|
+
3. Compute each time - cleaner memory model
|
|
359
|
+
4. No - keep errors simple and clear
|
|
360
|
+
|
|
361
|
+
## Reference Documentation
|
|
362
|
+
|
|
363
|
+
- **C Error Patterns**: See `old-docs/` for historical approaches
|
|
364
|
+
- **FFI Examples**: See existing `lib/taurus/ffi/bridge.rb`
|
|
365
|
+
- **Test Patterns**: See existing `spec/taurus/element_spec.rb`
|
|
366
|
+
|
|
367
|
+
## Final Checklist
|
|
368
|
+
|
|
369
|
+
Before marking Phase 1 complete:
|
|
370
|
+
- [ ] All code written and tested
|
|
371
|
+
- [ ] valgrind shows zero leaks
|
|
372
|
+
- [ ] Code follows MECE principles
|
|
373
|
+
- [ ] Architecture is clean and extensible
|
|
374
|
+
- [ ] Documentation comments complete
|
|
375
|
+
- [ ] Status tracker updated
|
|
376
|
+
- [ ] Ready for Phase 2
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
**Ready to begin Session 1: Enhanced Error Messages - Phase 1!** 🚀
|
|
381
|
+
|
|
382
|
+
Start by reading the three planning documents listed above, then proceed with Step 1.
|