@g3nomaly/g3n-skills 0.1.3 → 0.1.5
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/.claude-plugin/marketplace.json +18 -4
- package/README.md +9 -2
- package/package.json +2 -2
- package/skills/coding/.claude-plugin/plugin.json +2 -2
- package/skills/coding/code/SKILL.md +4 -1
- package/skills/coding/code/references/cpp.md +43 -0
- package/skills/coding/code/references/python.md +28 -0
- package/skills/coding/code/references/rust.md +26 -0
- package/skills/coding/document/SKILL.md +10 -0
- package/skills/coding/document/hooks/install-highlighting.ts +136 -0
- package/skills/coding/document/references/cpp.md +105 -0
- package/skills/coding/document/references/python.md +95 -0
- package/skills/coding/document/references/rust.md +122 -0
- package/skills/coding/document/vscode/package.json +28 -0
- package/skills/coding/document/vscode/syntaxes/python.json +56 -0
- package/skills/coding/document/vscode/syntaxes/rust.json +25 -0
- package/skills/coding/vocabulary/SKILL.md +47 -0
- package/skills/coding/vocabulary/agents/openai.yaml +5 -0
- package/skills/task-manager/.claude-plugin/plugin.json +1 -2
- package/skills/task-manager/task-manager/hooks/get-entry.ts +4 -4
- package/skills/task-manager/task-manager/hooks/guard-tree.ts +2 -2
- package/skills/task-manager/task-manager/hooks/next-entry.ts +3 -3
- package/skills/task-manager/task-manager/references/hooks.md +1 -1
- package/skills/task-manager/task-manager/references/tree.md +0 -1
- package/skills/task-manager/task-manager/src/hooks/cli.ts +11 -1
- package/skills/task-manager/task-manager/src/hooks/index.ts +1 -1
- package/skills/task-manager/task-manager/src/schemas/index.ts +2 -2
- package/skills/task-manager/task-manager/src/schemas/term.ts +9 -22
- package/skills/task-manager/task-manager/src/vocabulary/file.ts +40 -84
- package/skills/task-manager/task-manager/src/vocabulary/index.ts +3 -3
- package/skills/task-manager/task-manager/src/vocabulary/match.ts +3 -34
- package/skills/teach/.claude-plugin/plugin.json +7 -0
- package/skills/teach/architecture/SKILL.md +19 -0
- package/skills/teach/architecture/agents/openai.yaml +5 -0
- package/skills/teach/architecture/references/examples.md +16 -0
- package/skills/teach/deep-dive/SKILL.md +19 -0
- package/skills/teach/deep-dive/agents/openai.yaml +5 -0
- package/skills/teach/deep-dive/references/examples.md +15 -0
- package/skills/teach/explain/SKILL.md +19 -0
- package/skills/teach/explain/agents/openai.yaml +5 -0
- package/skills/teach/explain/references/examples.md +9 -0
- package/skills/teach/guided-debugging/SKILL.md +25 -0
- package/skills/teach/guided-debugging/agents/openai.yaml +5 -0
- package/skills/teach/guided-debugging/references/debugging-loop.md +18 -0
- package/skills/teach/guided-debugging/references/examples.md +24 -0
- package/skills/teach/guided-review/SKILL.md +20 -0
- package/skills/teach/guided-review/agents/openai.yaml +5 -0
- package/skills/teach/guided-review/references/examples.md +18 -0
- package/skills/teach/learn-by-building/SKILL.md +19 -0
- package/skills/teach/learn-by-building/agents/openai.yaml +5 -0
- package/skills/teach/learn-by-building/references/examples.md +15 -0
- package/skills/teach/teaching/SKILL.md +22 -0
- package/skills/teach/teaching/agents/openai.yaml +5 -0
- package/skills/teach/teaching/references/learning-model.md +24 -0
- package/skills/task-manager/vocabulary/SKILL.md +0 -42
- package/skills/task-manager/vocabulary/agents/openai.yaml +0 -5
- package/skills/task-manager/vocabulary/hooks/get-term.ts +0 -18
- package/skills/task-manager/vocabulary/hooks/list-terms.ts +0 -12
- package/skills/task-manager/vocabulary/hooks/remove-term.ts +0 -22
- package/skills/task-manager/vocabulary/hooks/set-term.ts +0 -37
- package/skills/task-manager/vocabulary/references/usage.md +0 -26
- package/skills/task-manager/vocabulary/tsconfig.json +0 -7
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
|
|
3
|
+
Rustdoc: a `/** */` block directly above the item, written in Markdown. Never `///`.
|
|
4
|
+
|
|
5
|
+
- **Line 1**: `// src/users/store.rs`, the relative path. No `//!` or `/*! */` module or crate docs.
|
|
6
|
+
- **Declarations with a docstring**: functions, methods, structs, enums, traits and type aliases.
|
|
7
|
+
- **Summary**: `/** Adds two numbers. */` on one line. The first line is the `@brief`: a longer block opens with the summary, then a blank ` *` line.
|
|
8
|
+
- **Tags**: Markdown sections `# Arguments`, `# Returns`, `# Errors`, `# Panics` and `# Examples`. Arguments are a list of `` * `name` - text ``.
|
|
9
|
+
- **Fields and variants**: Rust has no field tag, so this is the one exception to the owner rule. Each struct field and enum variant gets its own one-line `/** */`. Rustdoc and analyze both read it from there.
|
|
10
|
+
- **Examples**: a fenced block under `# Examples`. `cargo test` compiles and runs it, so it must build. Setup lines start with `# `, which hides them from the rendered docs.
|
|
11
|
+
|
|
12
|
+
```rust
|
|
13
|
+
/** Adds two numbers. */
|
|
14
|
+
pub fn add(a: i32, b: i32) -> i32 {
|
|
15
|
+
a + b
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves a user by id.
|
|
20
|
+
*
|
|
21
|
+
* # Arguments
|
|
22
|
+
*
|
|
23
|
+
* * `user_id` - Surrogate key from the `users` table, not the auth provider's id.
|
|
24
|
+
*
|
|
25
|
+
* # Returns
|
|
26
|
+
*
|
|
27
|
+
* Custom User struct used by the authentication service, or `None` when no row matches.
|
|
28
|
+
*
|
|
29
|
+
* # Errors
|
|
30
|
+
*
|
|
31
|
+
* `StoreError::Connection` when the database is unreachable.
|
|
32
|
+
*/
|
|
33
|
+
pub async fn get_user(user_id: i64) -> Result<Option<User>, StoreError> {
|
|
34
|
+
// ...
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Error for a file on disk that does not match its schema.
|
|
39
|
+
*
|
|
40
|
+
* Holds the path of the file and one issue per rejected field, and its
|
|
41
|
+
* `Display` lists them all. Returned by `parse_file` and by the readers in
|
|
42
|
+
* `filetree/`, `decisions/` and `vocabulary/` that check a file's layout by
|
|
43
|
+
* hand. Callers tell it apart from other errors by matching on the type.
|
|
44
|
+
*
|
|
45
|
+
* # Examples
|
|
46
|
+
*
|
|
47
|
+
* ```
|
|
48
|
+
* # use harbor::{FileValidationError, FileValidationIssue};
|
|
49
|
+
* let error = FileValidationError {
|
|
50
|
+
* file: "/home/me/app/docs/auth.md".into(),
|
|
51
|
+
* issues: vec![FileValidationIssue { field: "status".into(), message: r#"is "done""#.into() }],
|
|
52
|
+
* };
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
#[derive(Debug)]
|
|
56
|
+
pub struct FileValidationError {
|
|
57
|
+
/** Absolute path of the file that failed, e.g. `/home/me/app/docs/auth.md`. */
|
|
58
|
+
pub file: PathBuf,
|
|
59
|
+
/** Rejected fields, e.g. `FileValidationIssue { field: "phases.0.status".into(), message: "is required".into() }`. */
|
|
60
|
+
pub issues: Vec<FileValidationIssue>,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Entries a set of requests resolved to, and the requests that did not resolve. */
|
|
64
|
+
pub struct Resolution {
|
|
65
|
+
/** Resolved entries by key, e.g. `emphasise`. */
|
|
66
|
+
pub entries: HashMap<String, SourceOffer>,
|
|
67
|
+
/** Requests no source offers, e.g. `ghost@mine`. */
|
|
68
|
+
pub unknown: Vec<String>,
|
|
69
|
+
/** Why an entry nobody named is in the set, e.g. `("vocabulary", "dep of plan")`. */
|
|
70
|
+
pub notes: HashMap<String, String>,
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`harbor` in the example stands for the crate's own name. Doctests use the crate from outside, so `crate::` does not work there.
|
|
75
|
+
|
|
76
|
+
A function that returns `Result` always has `# Errors`. One that can panic always has `# Panics`.
|
|
77
|
+
|
|
78
|
+
## Bad
|
|
79
|
+
|
|
80
|
+
Each one next to what is wrong with it. The examples above show the fix.
|
|
81
|
+
|
|
82
|
+
```rust
|
|
83
|
+
/// Adds two numbers.
|
|
84
|
+
pub fn add(a: i32, b: i32) -> i32
|
|
85
|
+
```
|
|
86
|
+
A `///` comment. Use `/** Adds two numbers. */`.
|
|
87
|
+
|
|
88
|
+
```rust
|
|
89
|
+
/**
|
|
90
|
+
* Entries a set of requests resolved to.
|
|
91
|
+
*
|
|
92
|
+
* # Fields
|
|
93
|
+
*
|
|
94
|
+
* * `entries` - Resolved entries by key.
|
|
95
|
+
*/
|
|
96
|
+
pub struct Resolution {
|
|
97
|
+
pub entries: HashMap<String, SourceOffer>,
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
Fields listed in the struct's docstring. Neither rustdoc nor analyze reads them there. The `/** */` goes on the field.
|
|
101
|
+
|
|
102
|
+
```rust
|
|
103
|
+
/** Parses a file. */
|
|
104
|
+
pub fn parse_file(path: &Path) -> Result<Tree, FileValidationError>
|
|
105
|
+
```
|
|
106
|
+
Returns `Result` with no `# Errors` section.
|
|
107
|
+
|
|
108
|
+
```rust
|
|
109
|
+
/**
|
|
110
|
+
* # Examples
|
|
111
|
+
*
|
|
112
|
+
* ```
|
|
113
|
+
* let error = FileValidationError::new(path, vec![issue]);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
```
|
|
117
|
+
`path` and `issue` are never defined and the type is never imported, so `cargo test` fails on it.
|
|
118
|
+
|
|
119
|
+
```rust
|
|
120
|
+
/*! User storage. */
|
|
121
|
+
```
|
|
122
|
+
A module docstring. Line 1 is the path comment, and the items carry the docs.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "g3n-doc-tags",
|
|
3
|
+
"displayName": "g3n Doc Tags",
|
|
4
|
+
"description": "Colours the sections and argument names of Rust doc comments and Python docstrings like JSDoc tags.",
|
|
5
|
+
"publisher": "g3nomaly",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"license": "Elastic-2.0",
|
|
8
|
+
"engines": {
|
|
9
|
+
"vscode": "^1.80.0"
|
|
10
|
+
},
|
|
11
|
+
"categories": [
|
|
12
|
+
"Programming Languages"
|
|
13
|
+
],
|
|
14
|
+
"contributes": {
|
|
15
|
+
"grammars": [
|
|
16
|
+
{
|
|
17
|
+
"scopeName": "g3n.doc-tags.rust",
|
|
18
|
+
"path": "./syntaxes/rust.json",
|
|
19
|
+
"injectTo": ["source.rust", "text.html.markdown"]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"scopeName": "g3n.doc-tags.python",
|
|
23
|
+
"path": "./syntaxes/python.json",
|
|
24
|
+
"injectTo": ["source.python", "text.html.markdown"]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scopeName": "g3n.doc-tags.python",
|
|
3
|
+
"injectionSelector": "L:string.quoted.docstring.multi.python, L:string.quoted.docstring.raw.multi.python",
|
|
4
|
+
"patterns": [
|
|
5
|
+
{ "include": "#named-section" },
|
|
6
|
+
{ "include": "#raises-section" },
|
|
7
|
+
{ "include": "#plain-section" }
|
|
8
|
+
],
|
|
9
|
+
"repository": {
|
|
10
|
+
"named-section": {
|
|
11
|
+
"comment": "`Args:` or `Attributes:` and the `name (type):` items under it. The section ends at the first line that is not indented deeper, or at the closing quotes.",
|
|
12
|
+
"begin": "^(\\s*)(Args|Arguments|Attributes|Keyword Args|Keyword Arguments|Other Parameters)(:)\\s*$",
|
|
13
|
+
"end": "(?=\"\"\"|''')|^(?!\\1\\s+\\S|\\s*$)",
|
|
14
|
+
"beginCaptures": {
|
|
15
|
+
"2": { "name": "storage.type.class.jsdoc" },
|
|
16
|
+
"3": { "name": "punctuation.separator.jsdoc" }
|
|
17
|
+
},
|
|
18
|
+
"patterns": [
|
|
19
|
+
{
|
|
20
|
+
"match": "^\\s+(\\*{0,2}[A-Za-z_]\\w*)(?:\\s*\\(([^)]*)\\))?(:)(?=\\s|$)",
|
|
21
|
+
"captures": {
|
|
22
|
+
"1": { "name": "variable.other.jsdoc" },
|
|
23
|
+
"2": { "name": "entity.name.type.instance.jsdoc" },
|
|
24
|
+
"3": { "name": "punctuation.separator.jsdoc" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"raises-section": {
|
|
30
|
+
"comment": "`Raises:` and the `ErrorType:` items under it.",
|
|
31
|
+
"begin": "^(\\s*)(Raises)(:)\\s*$",
|
|
32
|
+
"end": "(?=\"\"\"|''')|^(?!\\1\\s+\\S|\\s*$)",
|
|
33
|
+
"beginCaptures": {
|
|
34
|
+
"2": { "name": "storage.type.class.jsdoc" },
|
|
35
|
+
"3": { "name": "punctuation.separator.jsdoc" }
|
|
36
|
+
},
|
|
37
|
+
"patterns": [
|
|
38
|
+
{
|
|
39
|
+
"match": "^\\s+([A-Za-z_][\\w.]*)(:)(?=\\s|$)",
|
|
40
|
+
"captures": {
|
|
41
|
+
"1": { "name": "entity.name.type.instance.jsdoc" },
|
|
42
|
+
"2": { "name": "punctuation.separator.jsdoc" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"plain-section": {
|
|
48
|
+
"comment": "Sections whose body is prose or code, such as `Returns:` and `Example:`. Only the heading is coloured.",
|
|
49
|
+
"match": "^\\s*(Returns|Yields|Example|Examples|Note|Notes|Warning|Warnings|See Also|Todo)(:)\\s*$",
|
|
50
|
+
"captures": {
|
|
51
|
+
"1": { "name": "storage.type.class.jsdoc" },
|
|
52
|
+
"2": { "name": "punctuation.separator.jsdoc" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scopeName": "g3n.doc-tags.rust",
|
|
3
|
+
"injectionSelector": "L:comment.block.documentation.rust",
|
|
4
|
+
"patterns": [
|
|
5
|
+
{ "include": "#section" },
|
|
6
|
+
{ "include": "#argument" }
|
|
7
|
+
],
|
|
8
|
+
"repository": {
|
|
9
|
+
"section": {
|
|
10
|
+
"comment": "A section heading such as ` * # Errors`. Only known names, so a hidden doctest line such as `# use harbor::Tree;` stays plain.",
|
|
11
|
+
"match": "^\\s*\\*?\\s*(#)\\s+(Arguments|Returns|Errors|Panics|Safety|Examples)\\s*$",
|
|
12
|
+
"captures": {
|
|
13
|
+
"1": { "name": "punctuation.definition.block.tag.jsdoc" },
|
|
14
|
+
"2": { "name": "storage.type.class.jsdoc" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"argument": {
|
|
18
|
+
"comment": "An item of the Arguments list: ` * * `user_id` - text`.",
|
|
19
|
+
"match": "^\\s*\\*?\\s*[*-]\\s+(`)([A-Za-z_]\\w*)(`)\\s+(-)(?=\\s)",
|
|
20
|
+
"captures": {
|
|
21
|
+
"2": { "name": "variable.other.jsdoc" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vocabulary
|
|
3
|
+
description: Use when about to name a domain concept in code, a task or a doc, when the user corrects a word, when two words are in use for one thing, or when another skill needs a term looked up or recorded.
|
|
4
|
+
registryDependencies: [domain-modeling]
|
|
5
|
+
disable-model-invocation: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Every session starts without the last one's words, so names _drift_: the _phase_ of Monday is the _stage_ of Tuesday, and by Friday the code has both. The glossary is the one list that stops it. Each **term** carries a definition and the **avoided words** it replaces.
|
|
9
|
+
|
|
10
|
+
The terms live in `docs/glossary.md`. Read and edit it directly. `g3n-analyze` reads the same file and links every term to its first use in the docs and code, and the task-manager's `get-entry` and `next-entry` list the terms each entry mentions.
|
|
11
|
+
|
|
12
|
+
## Looking a word up
|
|
13
|
+
|
|
14
|
+
Before you name a domain concept, search `docs/glossary.md` for the word.
|
|
15
|
+
|
|
16
|
+
- **Found as a term.** Use it as spelled.
|
|
17
|
+
- **Found under `_Avoid_`.** Use the term it belongs to instead, in prose and in identifiers alike.
|
|
18
|
+
- **Not found.** Use the word, and record it on its second sighting.
|
|
19
|
+
|
|
20
|
+
## Recording a term
|
|
21
|
+
|
|
22
|
+
**Record on the second sighting.** One use of a word is prose. The same concept named twice, or named differently by two people, is a term. A correction from the user is always a term.
|
|
23
|
+
|
|
24
|
+
Create the file when the first term is recorded. Keep this layout, because the parsers depend on it:
|
|
25
|
+
|
|
26
|
+
```md
|
|
27
|
+
# Glossary
|
|
28
|
+
|
|
29
|
+
One sentence on what the project is about.
|
|
30
|
+
|
|
31
|
+
## Planning
|
|
32
|
+
|
|
33
|
+
**Phase**:
|
|
34
|
+
One step of a plan, holding tasks.
|
|
35
|
+
_Avoid_: stage, milestone
|
|
36
|
+
|
|
37
|
+
**Task**:
|
|
38
|
+
One piece of work inside a phase, done in one session.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **Term.** A line that starts with the bold name and a colon: `**Phase**:`.
|
|
42
|
+
- **Definition.** One or two sentences on the lines below, saying what the thing _is_. Leave out how it is built.
|
|
43
|
+
- **`_Avoid_:`.** The synonyms actually seen, comma-separated, on the last line of the term. Leave the line out when there are none.
|
|
44
|
+
- **Groups.** A `##` heading above a run of terms groups them. A flat list is fine for a small glossary.
|
|
45
|
+
- **One meaning per word.** A word is a term or an avoided word of one term, never both and never twice. Before adding a word to `_Avoid_`, check that no other term already owns it.
|
|
46
|
+
|
|
47
|
+
A term whose meaning is still contested is sharpened with the `/domain-modeling` skill first, then recorded here.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-manager",
|
|
3
|
-
"description": "Query and drive a tree of plans, phases and tasks through hooks, with the plan's decisions
|
|
3
|
+
"description": "Query and drive a tree of plans, phases and tasks through hooks, with the plan's decisions kept beside them.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"hooks": "./hooks.json",
|
|
7
7
|
"skills": [
|
|
8
8
|
"./task-manager",
|
|
9
|
-
"./vocabulary",
|
|
10
9
|
"./decisions"
|
|
11
10
|
]
|
|
12
11
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/hooks/get-entry.ts
|
|
2
2
|
import { locateEntry, readEntry } from "../src/filetree";
|
|
3
|
-
import {
|
|
4
|
-
import { hasFlag, printJson, requireFlag, resolveRoot, runHook } from "../src/hooks";
|
|
3
|
+
import { readGlossary, termsMentioned } from "../src/vocabulary";
|
|
4
|
+
import { hasFlag, printJson, requireFlag, resolveProject, resolveRoot, runHook } from "../src/hooks";
|
|
5
5
|
|
|
6
|
-
/** Prints the plan, phase or task named by `--id` with its body and the
|
|
6
|
+
/** Prints the plan, phase or task named by `--id` with its body and the glossary terms it mentions. */
|
|
7
7
|
async function run(argv: readonly string[]): Promise<void> {
|
|
8
8
|
const root = await resolveRoot(argv);
|
|
9
9
|
const name = requireFlag(argv, "--id", "auth-rework");
|
|
@@ -19,7 +19,7 @@ async function run(argv: readonly string[]): Promise<void> {
|
|
|
19
19
|
trail: location.trail,
|
|
20
20
|
meta,
|
|
21
21
|
...(hasFlag(argv, "--meta-only") ? {} : { body }),
|
|
22
|
-
vocabulary: termsMentioned(`${meta.title}\n${meta.summary}\n${body}`, await
|
|
22
|
+
vocabulary: termsMentioned(`${meta.title}\n${meta.summary}\n${body}`, await readGlossary(resolveProject(argv))),
|
|
23
23
|
},
|
|
24
24
|
argv,
|
|
25
25
|
);
|
|
@@ -104,8 +104,8 @@ async function run(): Promise<void> {
|
|
|
104
104
|
deny(
|
|
105
105
|
`${target} is inside the task-manager tree, which is read through its hooks. ` +
|
|
106
106
|
"Use get-entry --id <name> for one plan, phase or task in full, next-entry for the " +
|
|
107
|
-
"front of a queue, list-entries --in <name> to see what a folder holds,
|
|
108
|
-
"
|
|
107
|
+
"front of a queue, list-entries --in <name> to see what a folder holds, or list-decisions " +
|
|
108
|
+
"--in <plan> for a plan's decisions.",
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/hooks/next-entry.ts
|
|
2
2
|
import { ENTRY_TYPE, listEntryNames, readEntry, resolveParent } from "../src/filetree";
|
|
3
3
|
import type { TaskStatus } from "../src/schemas";
|
|
4
|
-
import {
|
|
5
|
-
import { hasFlag, printJson, readFlag, resolveRoot, runHook } from "../src/hooks";
|
|
4
|
+
import { readGlossary, termsMentioned } from "../src/vocabulary";
|
|
5
|
+
import { hasFlag, printJson, readFlag, resolveProject, resolveRoot, runHook } from "../src/hooks";
|
|
6
6
|
|
|
7
7
|
const FINISHED: readonly TaskStatus[] = ["completed"];
|
|
8
8
|
const NOT_WORKABLE: readonly TaskStatus[] = ["completed", "blocked"];
|
|
@@ -20,7 +20,7 @@ async function run(argv: readonly string[]): Promise<void> {
|
|
|
20
20
|
const { file, meta, body } = await readEntry(dir, level, name);
|
|
21
21
|
if (skipped.includes(meta.status)) continue;
|
|
22
22
|
|
|
23
|
-
const vocabulary = termsMentioned(`${meta.title}\n${meta.summary}\n${body}`, await
|
|
23
|
+
const vocabulary = termsMentioned(`${meta.title}\n${meta.summary}\n${body}`, await readGlossary(resolveProject(argv)));
|
|
24
24
|
|
|
25
25
|
printJson({ type: ENTRY_TYPE[level], name, file, meta, body, vocabulary }, argv);
|
|
26
26
|
return;
|
|
@@ -16,7 +16,7 @@ JSON on stdout, messages on stderr.
|
|
|
16
16
|
|
|
17
17
|
- `next-entry` and `get-entry` return one entry; none found exits `1`.
|
|
18
18
|
- `list-entries` and `cleanup` return a list; `[]` exits `0`.
|
|
19
|
-
- `get-entry` and `next-entry` attach a `vocabulary` array: the terms the entry mentions.
|
|
19
|
+
- `get-entry` and `next-entry` attach a `vocabulary` array: the terms of `docs/glossary.md` the entry mentions.
|
|
20
20
|
- `set-state`, `cleanup` and `sync-tree` write; the others only read.
|
|
21
21
|
|
|
22
22
|
## Fired by the plugin
|
|
@@ -82,7 +82,17 @@ export async function resolveRoot(argv: readonly string[]): Promise<string> {
|
|
|
82
82
|
const override = readFlag(argv, "--root");
|
|
83
83
|
if (override !== undefined) return resolve(override);
|
|
84
84
|
|
|
85
|
-
return resolveFiletreeRoot(
|
|
85
|
+
return resolveFiletreeRoot(resolveProject(argv));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @brief Resolves the project the hook works in: `--project`, or the working directory.
|
|
90
|
+
*
|
|
91
|
+
* @param argv - Arguments of the hook.
|
|
92
|
+
* @returns Absolute project root.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveProject(argv: readonly string[]): string {
|
|
95
|
+
return resolve(readFlag(argv, "--project") ?? process.cwd());
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/** Reads everything piped to the hook on stdin, or returns `""` when run from a terminal. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/src/hooks/index.ts
|
|
2
|
-
export { readFlag, requireFlag, hasFlag, oneOf, resolveRoot, readStdin, printJson, runHook } from "./cli";
|
|
2
|
+
export { readFlag, requireFlag, hasFlag, oneOf, resolveRoot, resolveProject, readStdin, printJson, runHook } from "./cli";
|
|
3
3
|
export type { Hook } from "./cli";
|
|
@@ -32,8 +32,8 @@ export {
|
|
|
32
32
|
} from "./decision";
|
|
33
33
|
export type { DecisionStatus, Decision, DecisionIndex } from "./decision";
|
|
34
34
|
|
|
35
|
-
export { TermSchema,
|
|
36
|
-
export type { Term,
|
|
35
|
+
export { TermSchema, GlossarySchema } from "./term";
|
|
36
|
+
export type { Term, Glossary } from "./term";
|
|
37
37
|
|
|
38
38
|
export { CONFIG_FILE, DEFAULT_FILETREE_ROOT, FiletreeConfigSchema } from "./config";
|
|
39
39
|
export type { FiletreeConfig } from "./config";
|
|
@@ -1,38 +1,25 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/src/schemas/term.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
-
/** One
|
|
5
|
-
const TermNameSchema = z
|
|
6
|
-
.string()
|
|
7
|
-
.regex(/^[^\s#][^\n]*$/, "must be a single line that does not start with # or a space");
|
|
8
|
-
|
|
9
|
-
/** One entry of `vocabulary.md`: the word to use, what it means, and the words it replaces. */
|
|
4
|
+
/** One term of `docs/glossary.md`: the word to use, what it means, and the words it replaces. */
|
|
10
5
|
export const TermSchema = z.strictObject({
|
|
11
|
-
term:
|
|
12
|
-
definition: z
|
|
13
|
-
|
|
14
|
-
.min(1)
|
|
15
|
-
.refine(
|
|
16
|
-
(text) => !text.split("\n").some((line) => line.startsWith("#") || line.startsWith("_Avoid_:")),
|
|
17
|
-
"must not contain a line starting with # or _Avoid_:",
|
|
18
|
-
),
|
|
19
|
-
avoid: z
|
|
20
|
-
.array(TermNameSchema.refine((word) => !word.includes(","), "must not contain a comma"))
|
|
21
|
-
.default([]),
|
|
6
|
+
term: z.string().min(1),
|
|
7
|
+
definition: z.string(),
|
|
8
|
+
avoid: z.array(z.string().min(1)).default([]),
|
|
22
9
|
});
|
|
23
10
|
|
|
24
11
|
/**
|
|
25
|
-
* One
|
|
12
|
+
* One term of `docs/glossary.md`, as `TermSchema` parses it.
|
|
26
13
|
*
|
|
27
14
|
* @property term - The word to use.
|
|
28
|
-
* @property definition - What the term means, as written
|
|
15
|
+
* @property definition - What the term means, as written below its name.
|
|
29
16
|
* @property avoid - Words that mean the same thing and must not be used in its place.
|
|
30
17
|
*/
|
|
31
18
|
export type Term = z.infer<typeof TermSchema>;
|
|
32
19
|
|
|
33
|
-
export const
|
|
20
|
+
export const GlossarySchema = z.strictObject({
|
|
34
21
|
terms: z.array(TermSchema).default([]),
|
|
35
22
|
});
|
|
36
23
|
|
|
37
|
-
/** Every term of `
|
|
38
|
-
export type
|
|
24
|
+
/** Every term of `docs/glossary.md`, as `GlossarySchema` parses it. */
|
|
25
|
+
export type Glossary = z.infer<typeof GlossarySchema>;
|
|
@@ -1,114 +1,70 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/src/vocabulary/file.ts
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { FileValidationError, parseFile } from "../schemas/validation";
|
|
4
|
-
import {
|
|
4
|
+
import { GlossarySchema, type Term } from "../schemas/term";
|
|
5
5
|
|
|
6
|
-
/** File
|
|
7
|
-
export const
|
|
6
|
+
/** File holding every term, relative to the project root. Written by hand, as the `vocabulary` skill describes. */
|
|
7
|
+
export const GLOSSARY_FILE = join("docs", "glossary.md");
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
/** `**Phase**:` or `**Phase:**`, optionally followed by the start of the definition. */
|
|
10
|
+
const TERM_LINE = /^\*\*(.+?):?\*\*:?\s*(.*)$/;
|
|
11
|
+
/** `_Avoid_: stage, milestone` */
|
|
12
|
+
const AVOID_LINE = /^_Avoid_:\s*(.*)$/i;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* @brief
|
|
15
|
+
* @brief Reads every term of the project's glossary, in the order they sit in the file.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
* @returns Path of `vocabulary.md` inside `root`.
|
|
18
|
-
*/
|
|
19
|
-
export function vocabularyPathFor(root: string): string {
|
|
20
|
-
return join(root, VOCABULARY_FILE);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @brief Reads every term of the tree at `root`, in the order they sit in the file.
|
|
17
|
+
* Text that is not part of a term, such as the title, the intro and the group headings, is skipped.
|
|
25
18
|
*
|
|
26
|
-
* @param
|
|
27
|
-
* @returns Terms in file order, or an empty list when
|
|
28
|
-
* @throws FileValidationError when
|
|
29
|
-
* `writeVocabulary` produces, or names one term twice
|
|
19
|
+
* @param projectRoot - Absolute folder holding `docs/glossary.md`.
|
|
20
|
+
* @returns Terms in file order, or an empty list when the project has no glossary.
|
|
21
|
+
* @throws FileValidationError when one term is defined twice.
|
|
30
22
|
*/
|
|
31
|
-
export async function
|
|
32
|
-
const file =
|
|
23
|
+
export async function readGlossary(projectRoot: string): Promise<Term[]> {
|
|
24
|
+
const file = join(projectRoot, GLOSSARY_FILE);
|
|
33
25
|
if (!(await Bun.file(file).exists())) return [];
|
|
34
26
|
|
|
35
|
-
const terms = parseFile(
|
|
27
|
+
const terms = parseFile(GlossarySchema, file, { terms: parseTerms(await Bun.file(file).text()) }).terms;
|
|
36
28
|
rejectDuplicates(file, terms);
|
|
37
29
|
|
|
38
30
|
return terms;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
33
|
/**
|
|
42
|
-
* @brief
|
|
34
|
+
* @brief Splits the glossary text into raw terms, one per bold term line.
|
|
43
35
|
*
|
|
44
|
-
*
|
|
45
|
-
* @param terms - Terms to write; validated and checked for duplicate names first.
|
|
46
|
-
*/
|
|
47
|
-
export async function writeVocabulary(root: string, terms: readonly Term[]): Promise<void> {
|
|
48
|
-
const file = vocabularyPathFor(root);
|
|
49
|
-
const validated = parseFile(VocabularySchema, file, { terms }).terms;
|
|
50
|
-
rejectDuplicates(file, validated);
|
|
51
|
-
|
|
52
|
-
const sorted = [...validated].sort(
|
|
53
|
-
(a, b) => a.term.localeCompare(b.term, "en", { sensitivity: "base" }) || a.term.localeCompare(b.term),
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
await Bun.write(file, `${[TITLE, ...sorted.map(renderTerm)].join("\n\n")}\n`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @brief Renders one term as its `## ` block in the vocabulary file.
|
|
36
|
+
* A term runs until the next term line or heading. Its `_Avoid_:` line is its avoid list, the rest its definition.
|
|
61
37
|
*
|
|
62
|
-
* @param
|
|
38
|
+
* @param text - Full text of the glossary.
|
|
39
|
+
* @returns One raw term per bold term line, ready for `parseFile`.
|
|
63
40
|
*/
|
|
64
|
-
function
|
|
65
|
-
const lines
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
* The last `_Avoid_:` line of a block is its avoid list.
|
|
75
|
-
*
|
|
76
|
-
* @param file - Path of the file, used in the error message.
|
|
77
|
-
* @param text - Full text of the vocabulary file.
|
|
78
|
-
* @returns One raw entry per heading, ready for `parseFile`.
|
|
79
|
-
* @throws FileValidationError when text sits above the first heading.
|
|
80
|
-
*/
|
|
81
|
-
function parseTerms(file: string, text: string): unknown[] {
|
|
82
|
-
const terms: { term: string; lines: string[] }[] = [];
|
|
83
|
-
|
|
84
|
-
text.split("\n").forEach((line, index) => {
|
|
85
|
-
if (line.startsWith(TERM_HEADING)) {
|
|
86
|
-
terms.push({ term: line.slice(TERM_HEADING.length).trim(), lines: [] });
|
|
87
|
-
return;
|
|
41
|
+
function parseTerms(text: string): unknown[] {
|
|
42
|
+
const terms: { term: string; lines: string[]; avoid: string[] }[] = [];
|
|
43
|
+
let open = false;
|
|
44
|
+
|
|
45
|
+
for (const line of text.split("\n")) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (trimmed.startsWith("#")) {
|
|
48
|
+
open = false;
|
|
49
|
+
continue;
|
|
88
50
|
}
|
|
89
51
|
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
52
|
+
const termMatch = TERM_LINE.exec(trimmed);
|
|
53
|
+
if (termMatch) {
|
|
54
|
+
terms.push({ term: (termMatch[1] ?? "").trim(), lines: [termMatch[2] ?? ""], avoid: [] });
|
|
55
|
+
open = true;
|
|
56
|
+
continue;
|
|
94
57
|
}
|
|
95
58
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{ field: `line ${index + 1}`, message: `"${line}" sits above the first ${TERM_HEADING.trim()} heading` },
|
|
99
|
-
]);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
59
|
+
const current = terms.at(-1);
|
|
60
|
+
if (!open || current === undefined) continue;
|
|
102
61
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
: (lines[avoidAt] ?? "").slice(AVOID_PREFIX.length).split(",").map((word) => word.trim()).filter((word) => word !== "");
|
|
108
|
-
const definition = lines.filter((_, index) => index !== avoidAt).join("\n").trim();
|
|
62
|
+
const avoidMatch = AVOID_LINE.exec(trimmed);
|
|
63
|
+
if (avoidMatch) current.avoid = (avoidMatch[1] ?? "").split(",").map((word) => word.trim()).filter((word) => word !== "");
|
|
64
|
+
else current.lines.push(line);
|
|
65
|
+
}
|
|
109
66
|
|
|
110
|
-
|
|
111
|
-
});
|
|
67
|
+
return terms.map(({ term, lines, avoid }) => ({ term, definition: lines.join("\n").trim(), avoid }));
|
|
112
68
|
}
|
|
113
69
|
|
|
114
70
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/src/vocabulary/index.ts
|
|
2
|
-
export {
|
|
2
|
+
export { GLOSSARY_FILE, readGlossary } from "./file";
|
|
3
3
|
|
|
4
|
-
export {
|
|
5
|
-
export type {
|
|
4
|
+
export { termsMentioned } from "./match";
|
|
5
|
+
export type { TermMention } from "./match";
|