@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.
Files changed (62) hide show
  1. package/.claude-plugin/marketplace.json +18 -4
  2. package/README.md +9 -2
  3. package/package.json +2 -2
  4. package/skills/coding/.claude-plugin/plugin.json +2 -2
  5. package/skills/coding/code/SKILL.md +4 -1
  6. package/skills/coding/code/references/cpp.md +43 -0
  7. package/skills/coding/code/references/python.md +28 -0
  8. package/skills/coding/code/references/rust.md +26 -0
  9. package/skills/coding/document/SKILL.md +10 -0
  10. package/skills/coding/document/hooks/install-highlighting.ts +136 -0
  11. package/skills/coding/document/references/cpp.md +105 -0
  12. package/skills/coding/document/references/python.md +95 -0
  13. package/skills/coding/document/references/rust.md +122 -0
  14. package/skills/coding/document/vscode/package.json +28 -0
  15. package/skills/coding/document/vscode/syntaxes/python.json +56 -0
  16. package/skills/coding/document/vscode/syntaxes/rust.json +25 -0
  17. package/skills/coding/vocabulary/SKILL.md +47 -0
  18. package/skills/coding/vocabulary/agents/openai.yaml +5 -0
  19. package/skills/task-manager/.claude-plugin/plugin.json +1 -2
  20. package/skills/task-manager/task-manager/hooks/get-entry.ts +4 -4
  21. package/skills/task-manager/task-manager/hooks/guard-tree.ts +2 -2
  22. package/skills/task-manager/task-manager/hooks/next-entry.ts +3 -3
  23. package/skills/task-manager/task-manager/references/hooks.md +1 -1
  24. package/skills/task-manager/task-manager/references/tree.md +0 -1
  25. package/skills/task-manager/task-manager/src/hooks/cli.ts +11 -1
  26. package/skills/task-manager/task-manager/src/hooks/index.ts +1 -1
  27. package/skills/task-manager/task-manager/src/schemas/index.ts +2 -2
  28. package/skills/task-manager/task-manager/src/schemas/term.ts +9 -22
  29. package/skills/task-manager/task-manager/src/vocabulary/file.ts +40 -84
  30. package/skills/task-manager/task-manager/src/vocabulary/index.ts +3 -3
  31. package/skills/task-manager/task-manager/src/vocabulary/match.ts +3 -34
  32. package/skills/teach/.claude-plugin/plugin.json +7 -0
  33. package/skills/teach/architecture/SKILL.md +19 -0
  34. package/skills/teach/architecture/agents/openai.yaml +5 -0
  35. package/skills/teach/architecture/references/examples.md +16 -0
  36. package/skills/teach/deep-dive/SKILL.md +19 -0
  37. package/skills/teach/deep-dive/agents/openai.yaml +5 -0
  38. package/skills/teach/deep-dive/references/examples.md +15 -0
  39. package/skills/teach/explain/SKILL.md +19 -0
  40. package/skills/teach/explain/agents/openai.yaml +5 -0
  41. package/skills/teach/explain/references/examples.md +9 -0
  42. package/skills/teach/guided-debugging/SKILL.md +25 -0
  43. package/skills/teach/guided-debugging/agents/openai.yaml +5 -0
  44. package/skills/teach/guided-debugging/references/debugging-loop.md +18 -0
  45. package/skills/teach/guided-debugging/references/examples.md +24 -0
  46. package/skills/teach/guided-review/SKILL.md +20 -0
  47. package/skills/teach/guided-review/agents/openai.yaml +5 -0
  48. package/skills/teach/guided-review/references/examples.md +18 -0
  49. package/skills/teach/learn-by-building/SKILL.md +19 -0
  50. package/skills/teach/learn-by-building/agents/openai.yaml +5 -0
  51. package/skills/teach/learn-by-building/references/examples.md +15 -0
  52. package/skills/teach/teaching/SKILL.md +22 -0
  53. package/skills/teach/teaching/agents/openai.yaml +5 -0
  54. package/skills/teach/teaching/references/learning-model.md +24 -0
  55. package/skills/task-manager/vocabulary/SKILL.md +0 -42
  56. package/skills/task-manager/vocabulary/agents/openai.yaml +0 -5
  57. package/skills/task-manager/vocabulary/hooks/get-term.ts +0 -18
  58. package/skills/task-manager/vocabulary/hooks/list-terms.ts +0 -12
  59. package/skills/task-manager/vocabulary/hooks/remove-term.ts +0 -22
  60. package/skills/task-manager/vocabulary/hooks/set-term.ts +0 -37
  61. package/skills/task-manager/vocabulary/references/usage.md +0 -26
  62. 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.
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "Vocabulary"
3
+ short_description: "One word per concept across sessions, recorded in docs/glossary.md"
4
+ policy:
5
+ allow_implicit_invocation: false
@@ -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 and the project vocabulary kept beside them.",
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 { readVocabulary, termsMentioned } from "../src/vocabulary";
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 vocabulary terms it mentions. */
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 readVocabulary(root)),
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, get-term --term <word> " +
108
- "for the vocabulary, or list-decisions --in <plan> for a plan's decisions.",
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 { readVocabulary, termsMentioned } from "../src/vocabulary";
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 readVocabulary(root));
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
@@ -3,7 +3,6 @@
3
3
  ```
4
4
  <filetreeRoot>/ # skills.settings.filetreeRoot, default docs/task-manager
5
5
  ├── root-index.json
6
- ├── vocabulary.md
7
6
  └── <plan>/
8
7
  ├── <plan>.md
9
8
  ├── plan-index.json
@@ -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(resolve(readFlag(argv, "--project") ?? process.cwd()));
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, VocabularySchema } from "./term";
36
- export type { Term, Vocabulary } from "./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 line of text that cannot be mistaken for a Markdown heading. */
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: TermNameSchema,
12
- definition: z
13
- .string()
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 entry of `vocabulary.md`, as `TermSchema` parses it.
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 under its heading.
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 VocabularySchema = z.strictObject({
20
+ export const GlossarySchema = z.strictObject({
34
21
  terms: z.array(TermSchema).default([]),
35
22
  });
36
23
 
37
- /** Every term of `vocabulary.md`, as `VocabularySchema` parses it. */
38
- export type Vocabulary = z.infer<typeof VocabularySchema>;
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 { VocabularySchema, type Term } from "../schemas/term";
4
+ import { GlossarySchema, type Term } from "../schemas/term";
5
5
 
6
- /** File at the tree root holding every term. Written by the term hooks only. */
7
- export const VOCABULARY_FILE = "vocabulary.md";
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
- const TITLE = "# Vocabulary";
10
- const TERM_HEADING = "## ";
11
- const AVOID_PREFIX = "_Avoid_:";
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 Builds the path of the vocabulary file in a tree.
15
+ * @brief Reads every term of the project's glossary, in the order they sit in the file.
15
16
  *
16
- * @param root - Absolute tree root folder.
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 root - Absolute tree root folder.
27
- * @returns Terms in file order, or an empty list when there is no vocabulary file yet.
28
- * @throws FileValidationError when the file strays from the layout
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 readVocabulary(root: string): Promise<Term[]> {
32
- const file = vocabularyPathFor(root);
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(VocabularySchema, file, { terms: parseTerms(file, await Bun.file(file).text()) }).terms;
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 Replaces the vocabulary file with `terms`, sorted by name so the file reads the same on every write.
34
+ * @brief Splits the glossary text into raw terms, one per bold term line.
43
35
  *
44
- * @param root - Absolute tree root folder.
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 term - Term to render.
38
+ * @param text - Full text of the glossary.
39
+ * @returns One raw term per bold term line, ready for `parseFile`.
63
40
  */
64
- function renderTerm(term: Term): string {
65
- const lines = [`${TERM_HEADING}${term.term}`, "", term.definition.trim()];
66
- if (term.avoid.length > 0) lines.push("", `${AVOID_PREFIX} ${term.avoid.join(", ")}`);
67
-
68
- return lines.join("\n");
69
- }
70
-
71
- /**
72
- * @brief Splits the file text at its `## ` headings into raw term entries.
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 current = terms.at(-1);
91
- if (current !== undefined) {
92
- current.lines.push(line);
93
- return;
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
- if (line.trim() !== "" && line.trim() !== TITLE) {
97
- throw new FileValidationError(file, [
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
- return terms.map(({ term, lines }) => {
104
- const avoidAt = lines.map((line) => line.startsWith(AVOID_PREFIX)).lastIndexOf(true);
105
- const avoid = avoidAt === -1
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
- return { term, definition, avoid };
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 { VOCABULARY_FILE, vocabularyPathFor, readVocabulary, writeVocabulary } from "./file";
2
+ export { GLOSSARY_FILE, readGlossary } from "./file";
3
3
 
4
- export { findTerm, termsMentioned } from "./match";
5
- export type { TermMatch, TermMention } from "./match";
4
+ export { termsMentioned } from "./match";
5
+ export type { TermMention } from "./match";