@handsupmin/gc-tree 1.1.2 → 1.1.4

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/dist/src/cli.js CHANGED
@@ -9,7 +9,7 @@ import { dispatchGcTreeHook } from './hook.js';
9
9
  import { onboardBranch } from './onboard.js';
10
10
  import { buildProviderLaunchPlan, maybeLaunchProvider, promptLanguageSelection, promptLaunchProviderSelection, promptProviderSelection, } from './provider.js';
11
11
  import { DEFAULT_BRANCH, branchDir, resolveHome } from './paths.js';
12
- import { branchRepoMapPath, branchScopeStatus, detectCurrentRepoId, promptResolveScopeDecision, readBranchRepoMap, resolveBranchForRepo, setRepoScopeForBranch, } from './repo-map.js';
12
+ import { branchRepoMapPath, detectCurrentRepoId, promptResolveScopeDecision, readBranchRepoMap, resolveBranchForRepo, setRepoScopeForBranch, } from './repo-map.js';
13
13
  import { findRelatedDocs, getDocById, resolveContext } from './resolve.js';
14
14
  import { scaffoldHostIntegration } from './scaffold.js';
15
15
  import { appendScaffoldedHost, requirePreferredProvider, writeSettings, readSettings } from './settings.js';
@@ -47,14 +47,14 @@ function usage() {
47
47
  gctree branches [--home DIR]
48
48
  gctree repo-map [--home DIR]
49
49
  gctree set-repo-scope --branch NAME [--repo NAME] [--cwd DIR] (--include|--exclude) [--home DIR]
50
- gctree status [--home DIR] [--cwd DIR]
50
+ gctree status [--home DIR] [--branch NAME] [--cwd DIR]
51
51
  gctree onboard [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
52
52
  gctree verify-onboarding [--home DIR] [--branch NAME]
53
53
  gctree reset-gc-branch [--home DIR] [--branch NAME] --yes
54
54
  gctree uninstall [--home DIR] [--target DIR] [--host <codex|claude-code|both>] [--keep-home] --yes
55
55
  gctree resolve --query TEXT [--home DIR] [--branch NAME] [--cwd DIR]
56
- gctree show-doc --id ID [--home DIR] [--branch NAME]
57
- gctree related --id ID [--home DIR] [--branch NAME]
56
+ gctree show-doc --id ID [--home DIR] [--branch NAME] [--cwd DIR]
57
+ gctree related --id ID [--home DIR] [--branch NAME] [--cwd DIR]
58
58
  gctree update-global-context [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
59
59
  gctree update-gc [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
60
60
  gctree ugc [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
@@ -78,6 +78,15 @@ async function readStdinText() {
78
78
  }
79
79
  return Buffer.concat(chunks).toString('utf8');
80
80
  }
81
+ async function resolveCommandBranch({ home, explicitBranch, cwd, }) {
82
+ const head = (await readHead(home)) || DEFAULT_BRANCH;
83
+ return resolveBranchForRepo({
84
+ home,
85
+ head,
86
+ explicitBranch,
87
+ cwd: cwd || process.cwd(),
88
+ });
89
+ }
81
90
  function compactMatchCommands(id, home, branch) {
82
91
  const homeArg = `--home ${JSON.stringify(home)}`;
83
92
  const branchArg = `--branch ${JSON.stringify(branch)}`;
@@ -276,22 +285,26 @@ async function main() {
276
285
  return;
277
286
  }
278
287
  case 'status': {
279
- const gcBranch = (await readHead(home)) || DEFAULT_BRANCH;
288
+ const resolved = await resolveCommandBranch({
289
+ home,
290
+ explicitBranch: readArg('--branch') || undefined,
291
+ cwd: readArg('--cwd') || process.cwd(),
292
+ });
293
+ const gcBranch = resolved.gc_branch;
280
294
  if (!existsSync(branchDir(home, gcBranch))) {
281
295
  throw new Error('gc-tree is not initialized. Run `gctree init` first.');
282
296
  }
283
297
  const result = await statusForBranch(home, gcBranch);
284
298
  const settings = await readSettings(home);
285
- const mapping = await readBranchRepoMap(home);
286
- const currentRepo = await detectCurrentRepoId(readArg('--cwd') || process.cwd());
287
299
  console.log(JSON.stringify({
288
300
  ...result,
289
301
  provider_mode: settings?.provider_mode || null,
290
302
  preferred_provider: settings?.preferred_provider || null,
291
303
  preferred_language: settings?.preferred_language || null,
292
- current_repo: currentRepo,
304
+ current_repo: resolved.current_repo,
305
+ source: resolved.source,
293
306
  branch_repo_map_path: branchRepoMapPath(home),
294
- repo_scope_status: branchScopeStatus(mapping, gcBranch, currentRepo),
307
+ repo_scope_status: resolved.scope_status,
295
308
  }, null, 2));
296
309
  return;
297
310
  }
@@ -365,10 +378,8 @@ async function main() {
365
378
  const query = readArg('--query');
366
379
  if (!query)
367
380
  usage();
368
- const head = (await readHead(home)) || DEFAULT_BRANCH;
369
- const resolved = await resolveBranchForRepo({
381
+ const resolved = await resolveCommandBranch({
370
382
  home,
371
- head,
372
383
  explicitBranch: readArg('--branch') || undefined,
373
384
  cwd: readArg('--cwd') || process.cwd(),
374
385
  });
@@ -491,10 +502,24 @@ async function main() {
491
502
  const id = readArg('--id');
492
503
  if (!id)
493
504
  usage();
494
- const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
505
+ const resolved = await resolveCommandBranch({
506
+ home,
507
+ explicitBranch: readArg('--branch') || undefined,
508
+ cwd: readArg('--cwd') || process.cwd(),
509
+ });
510
+ const gcBranch = resolved.gc_branch;
495
511
  const branchStatus = await statusForBranch(home, gcBranch);
496
512
  if (branchStatus.doc_count === 0) {
497
- console.log(JSON.stringify({ status: 'empty_branch', gc_branch: gcBranch, id, message: `gc-branch "${gcBranch}" has no docs yet.`, doc: null }, null, 2));
513
+ console.log(JSON.stringify({
514
+ status: 'empty_branch',
515
+ gc_branch: gcBranch,
516
+ id,
517
+ current_repo: resolved.current_repo,
518
+ source: resolved.source,
519
+ repo_scope_status: resolved.scope_status,
520
+ message: `gc-branch "${gcBranch}" has no docs yet.`,
521
+ doc: null,
522
+ }, null, 2));
498
523
  return;
499
524
  }
500
525
  const doc = await getDocById({ home, branch: gcBranch, id });
@@ -502,6 +527,9 @@ async function main() {
502
527
  status: doc ? 'matched' : 'doc_not_found',
503
528
  gc_branch: gcBranch,
504
529
  id,
530
+ current_repo: resolved.current_repo,
531
+ source: resolved.source,
532
+ repo_scope_status: resolved.scope_status,
505
533
  message: doc ? `Loaded doc "${id}".` : `Doc "${id}" was not found in gc-branch "${gcBranch}".`,
506
534
  doc,
507
535
  }, null, 2));
@@ -511,12 +539,20 @@ async function main() {
511
539
  const id = readArg('--id');
512
540
  if (!id)
513
541
  usage();
514
- const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
542
+ const resolved = await resolveCommandBranch({
543
+ home,
544
+ explicitBranch: readArg('--branch') || undefined,
545
+ cwd: readArg('--cwd') || process.cwd(),
546
+ });
547
+ const gcBranch = resolved.gc_branch;
515
548
  const result = await findRelatedDocs({ home, branch: gcBranch, id });
516
549
  console.log(JSON.stringify({
517
550
  status: result.status,
518
551
  gc_branch: gcBranch,
519
552
  id,
553
+ current_repo: resolved.current_repo,
554
+ source: resolved.source,
555
+ repo_scope_status: resolved.scope_status,
520
556
  message: result.status === 'matched'
521
557
  ? `Found ${result.matches.length} related docs for "${id}".`
522
558
  : result.status === 'no_related_docs'
package/dist/src/hook.js CHANGED
@@ -27,14 +27,14 @@ function limitMatches(matches, max = 3) {
27
27
  function displayKeyword(match) {
28
28
  return (match.label || match.title || match.id).trim();
29
29
  }
30
- function formatMatches(matches) {
30
+ function formatMatches(matches, gcBranch) {
31
31
  return limitMatches(matches)
32
32
  .map((match) => {
33
33
  const summary = match.summary?.trim() ?? '';
34
34
  const excerpt = match.excerpt?.trim() ?? '';
35
35
  const context = summary || excerpt;
36
36
  const shortContext = context.length > 180 ? `${context.slice(0, 177).trim()}...` : context;
37
- const showDoc = `gctree show-doc --id "${match.id}"`;
37
+ const showDoc = `gctree show-doc --id "${match.id}" --branch "${gcBranch}"`;
38
38
  return [
39
39
  `[${displayKeyword(match)}]`,
40
40
  shortContext,
@@ -59,7 +59,7 @@ function buildMatchContext({ gcBranch, currentRepo, repoScopeStatus, matches, })
59
59
  const lines = [
60
60
  `[gc-tree] USE FIRST: ${Math.min(matches.length, 3)} docs gc-branch="${gcBranch}" repo="${currentRepo || 'unscoped'}" scope=${repoScopeStatus}.`,
61
61
  '',
62
- formatMatches(matches),
62
+ formatMatches(matches, gcBranch),
63
63
  '',
64
64
  `Rule: 위 문서를 먼저 근거로 삼고, 부족하면 details 명령으로 세부 정보를 확인.`,
65
65
  ];
@@ -107,25 +107,45 @@ function renderCodexOnboardSkill() {
107
107
  function renderUpdateProtocol() {
108
108
  return [
109
109
  '1. Run `gctree status` and state the active gc-branch.',
110
- '2. Understand what changed read the user\'s request, inspect relevant code or docs if needed, and determine the right content to capture.',
111
- '3. Decide category and path automatically based on content — never ask the user about scope or placement:',
110
+ '2. Find the target doc before writing. Use `gctree resolve --query "<new fact or term>"`; if a match is incomplete, read the full doc with `gctree show-doc --id "<id>"`. For an existing doc, preserve useful existing details and index entries unless the user explicitly wants them removed.',
111
+ '3. Decide category and slug automatically based on content — never ask the user about scope or placement:',
112
112
  ' - `docs/workflows/<name>.md` — cross-repo action sequences, step-by-step procedures',
113
113
  ' - `docs/conventions/<repo>.md` — code patterns, naming, validation, DTO/service conventions for a repo',
114
114
  ' - `docs/repos/<repo>.md` — repo role, key paths, cross-repo dependencies',
115
115
  ' - `docs/domain/<concept>.md` — domain terms, acronyms, business logic',
116
116
  ' - `docs/infra/<topic>.md` — infra, deployment, environment config',
117
117
  ' - `docs/role/<name>.md` — team member roles, responsibilities',
118
- '4. Write a `## Summary` that is actionable: actual patterns/commands/constraints a developer needs not a sentence about what the doc covers.',
119
- '5. Include a `## Index Entries` section in each doc\'s content with many keywords: aliases, related terms, command names, field names, workflow names, acronyms. More entries = more chances of being found by `gctree resolve`. Fewer than 5 entries almost always means more are needed.',
120
- '5a. **Bilingual index entries**: when the workflow language is not English, every doc\'s `## Index Entries` MUST contain BOTH the workflow language AND English forms for every technical term, repo name, workflow name, and concept. gc-tree retrieval is token-overlap based and cannot translate at query time, so a Korean-language doc that omits English aliases will silently miss any English query (and vice versa). Acronyms (JWT, EMPI, VCF) stay in original form in both. Body and `## Summary` stay in the workflow language; bilingual coverage lives in the index.',
121
- '6. Create a temporary JSON file with the updated `docs[]` and run `gctree __apply-update --input <temp-file>`.',
122
- '7. Run `gctree verify-onboarding --branch <gc-branch>` and inspect the output:',
118
+ '4. In the update JSON, `slug` is the path without `docs/` and without `.md`. Example: existing file `docs/conventions/mvldev-assignment-backend-smson.md` must use `slug: "conventions/mvldev-assignment-backend-smson"`.',
119
+ '5. Use exactly this JSON schema. Unknown fields are rejected; do not use `id`, `path`, or `content`.',
120
+ '```json',
121
+ '{',
122
+ ' "branch": "<gc-branch>",',
123
+ ' "docs": [',
124
+ ' {',
125
+ ' "title": "conventions: example repo",',
126
+ ' "slug": "conventions/example-repo",',
127
+ ' "category": "conventions",',
128
+ ' "summary": "Actionable one-paragraph summary with patterns, commands, and constraints.",',
129
+ ' "body": "## Patterns\\n\\n- Details body only. Do not include # title, ## Summary, or ## Index Entries here.",',
130
+ ' "indexLabel": "example repo conventions",',
131
+ ' "indexEntries": ["example repo", "ExampleRepo", "주요 한국어 검색어", "command names", "field names"]',
132
+ ' }',
133
+ ' ]',
134
+ '}',
135
+ '```',
136
+ '6. Write an actionable `summary`: actual patterns/commands/constraints a developer needs — not a sentence about what the doc covers.',
137
+ '7. Put search terms in `indexEntries`, not inside `body`. Include aliases, related terms, command names, field names, workflow names, acronyms, and the exact phrases users are likely to ask. Fewer than 5 entries almost always means more are needed.',
138
+ '7a. **Bilingual index entries**: when the workflow language is not English, `indexEntries` MUST contain BOTH the workflow language AND English forms for every technical term, repo name, workflow name, and concept. gc-tree retrieval is token-overlap based and cannot translate at query time. Acronyms (JWT, EMPI, VCF) stay in original form in both. Body and summary stay in the workflow language; bilingual coverage lives in the index.',
139
+ '8. Create a temporary JSON file with the updated `docs[]` and run `gctree __apply-update --input <temp-file>`.',
140
+ '9. Run `gctree verify-onboarding --branch <gc-branch>` and inspect the output:',
123
141
  ' - Check that every updated doc appears in the verified doc list.',
124
142
  ' - Check that index entries for the new content exist and are searchable (not just generic titles).',
125
143
  ' - Check that no doc has `category: "general"` — reassign to correct category if so.',
126
144
  ' - Check that important concepts from the update are present as index labels — if missing, re-apply with added index entries.',
127
145
  ' - Self-heal any issues without asking the user: fix category, add index entries, re-run `gctree __apply-update`, re-verify.',
128
- '8. Show the user which docs were updated and confirm the index now covers the new content.',
146
+ '10. Run `gctree resolve --query "<new keyword>"` and `gctree show-doc --id "<slug>"` for at least one new keyword/doc to prove retrieval and details are correct.',
147
+ '11. If you intended to update an existing doc and `doc_count` increased unexpectedly, do not report success. Inspect the duplicate, remove the accidental doc, then re-apply with the correct `slug`.',
148
+ '12. Show the user which docs were updated and confirm the index now covers the new content.',
129
149
  ];
130
150
  }
131
151
  function renderCodexUpdateSkill() {
@@ -4,6 +4,14 @@ import { dirname, join } from 'node:path';
4
4
  import { renderDocMarkdown, slugify } from './markdown.js';
5
5
  import { branchDocsDir, DEFAULT_BRANCH, settingsPath } from './paths.js';
6
6
  import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
7
+ const UPDATE_ROOT_KEYS = new Set(['branch', 'branchSummary', 'docs']);
8
+ const UPDATE_DOC_KEYS = new Set(['title', 'slug', 'summary', 'body', 'tags', 'category', 'indexLabel', 'indexEntries']);
9
+ const VALID_UPDATE_CATEGORIES = new Set(['role', 'repos', 'domain', 'workflows', 'conventions', 'infra', 'verification']);
10
+ const LEGACY_DOC_FIELD_HINTS = {
11
+ id: 'use `slug` instead, without `docs/` or `.md`',
12
+ path: 'use `slug` instead, without `docs/` or `.md`',
13
+ content: 'use `body` instead, and put search terms in `indexEntries`',
14
+ };
7
15
  function docRelativePath(doc) {
8
16
  if (doc.slug?.includes('/'))
9
17
  return `${doc.slug.replace(/\.md$/i, '')}.md`;
@@ -11,7 +19,118 @@ function docRelativePath(doc) {
11
19
  const category = doc.category ? slugify(doc.category) : '';
12
20
  return category ? `${category}/${fileBase}.md` : `${fileBase}.md`;
13
21
  }
22
+ function isRecord(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+ function unknownKeys(record, allowed) {
26
+ return Object.keys(record).filter((key) => !allowed.has(key));
27
+ }
28
+ function requireNonEmptyString(record, key, subject) {
29
+ const value = record[key];
30
+ if (typeof value !== 'string' || value.trim() === '') {
31
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must be a non-empty string.`);
32
+ }
33
+ return value.trim();
34
+ }
35
+ function validateOptionalString(record, key, subject) {
36
+ const value = record[key];
37
+ if (value === undefined)
38
+ return undefined;
39
+ if (typeof value !== 'string' || value.trim() === '') {
40
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must be a non-empty string when provided.`);
41
+ }
42
+ return value.trim();
43
+ }
44
+ function validateOptionalStringArray(record, key, subject) {
45
+ const value = record[key];
46
+ if (value === undefined)
47
+ return undefined;
48
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
49
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must be an array of strings when provided.`);
50
+ }
51
+ const entries = value.map((entry) => entry.trim()).filter(Boolean);
52
+ if (entries.length === 0) {
53
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must contain at least one non-empty string when provided.`);
54
+ }
55
+ return entries;
56
+ }
57
+ function validateUpdateSlug(slug, subject) {
58
+ if (slug.startsWith('docs/')) {
59
+ throw new Error(`Invalid gctree update input: ${subject}.slug must omit the docs/ prefix. Example: use "conventions/example" instead of "docs/conventions/example.md".`);
60
+ }
61
+ if (slug.endsWith('.md')) {
62
+ throw new Error(`Invalid gctree update input: ${subject}.slug must omit the .md suffix. Example: use "conventions/example" instead of "conventions/example.md".`);
63
+ }
64
+ if (slug.startsWith('/') || slug.includes('\\') || slug.split('/').includes('..')) {
65
+ throw new Error(`Invalid gctree update input: ${subject}.slug must be a relative doc slug without absolute paths, backslashes, or "..".`);
66
+ }
67
+ if (slug.includes('/')) {
68
+ const category = slug.split('/')[0];
69
+ if (!VALID_UPDATE_CATEGORIES.has(category)) {
70
+ throw new Error(`Invalid gctree update input: ${subject}.slug category must be one of ${[...VALID_UPDATE_CATEGORIES].join(', ')}.`);
71
+ }
72
+ }
73
+ }
74
+ function validateUpdateBody(body, subject) {
75
+ if (/^\s*#\s+/.test(body)) {
76
+ throw new Error(`Invalid gctree update input: ${subject}.body must not include the top-level markdown title; use the title field.`);
77
+ }
78
+ if (/^## Summary\b/m.test(body)) {
79
+ throw new Error(`Invalid gctree update input: ${subject}.body must not include ## Summary; use the summary field.`);
80
+ }
81
+ if (/^## Index Entries\b/m.test(body)) {
82
+ throw new Error(`Invalid gctree update input: ${subject}.body must not include ## Index Entries; use the indexEntries array.`);
83
+ }
84
+ }
85
+ function validateContextUpdateInput(input) {
86
+ if (!isRecord(input)) {
87
+ throw new Error('Invalid gctree update input: root must be an object with docs[].');
88
+ }
89
+ const extraRootKeys = unknownKeys(input, UPDATE_ROOT_KEYS);
90
+ if (extraRootKeys.length > 0) {
91
+ throw new Error(`Invalid gctree update input: unsupported root field(s): ${extraRootKeys.join(', ')}.`);
92
+ }
93
+ validateOptionalString(input, 'branch', 'input');
94
+ validateOptionalString(input, 'branchSummary', 'input');
95
+ if (!Array.isArray(input.docs) || input.docs.length === 0) {
96
+ throw new Error('Invalid gctree update input: docs must be a non-empty array.');
97
+ }
98
+ input.docs.forEach((doc, index) => {
99
+ const subject = `docs[${index}]`;
100
+ if (!isRecord(doc)) {
101
+ throw new Error(`Invalid gctree update input: ${subject} must be an object.`);
102
+ }
103
+ const extraDocKeys = unknownKeys(doc, UPDATE_DOC_KEYS);
104
+ if (extraDocKeys.length > 0) {
105
+ const hints = extraDocKeys
106
+ .map((key) => LEGACY_DOC_FIELD_HINTS[key])
107
+ .filter((hint) => Boolean(hint));
108
+ const suffix = hints.length > 0 ? ` ${[...new Set(hints)].join('; ')}.` : '';
109
+ throw new Error(`Invalid gctree update input: ${subject} has unsupported field(s): ${extraDocKeys.join(', ')}.${suffix}`);
110
+ }
111
+ requireNonEmptyString(doc, 'title', subject);
112
+ const slug = requireNonEmptyString(doc, 'slug', subject);
113
+ requireNonEmptyString(doc, 'summary', subject);
114
+ const body = requireNonEmptyString(doc, 'body', subject);
115
+ const category = validateOptionalString(doc, 'category', subject);
116
+ validateOptionalString(doc, 'indexLabel', subject);
117
+ validateOptionalStringArray(doc, 'tags', subject);
118
+ const indexEntries = validateOptionalStringArray(doc, 'indexEntries', subject);
119
+ if (!indexEntries) {
120
+ throw new Error(`Invalid gctree update input: ${subject}.indexEntries must be a non-empty array of search terms.`);
121
+ }
122
+ validateUpdateSlug(slug, subject);
123
+ validateUpdateBody(body, subject);
124
+ if (category && !VALID_UPDATE_CATEGORIES.has(category)) {
125
+ throw new Error(`Invalid gctree update input: ${subject}.category must be one of ${[...VALID_UPDATE_CATEGORIES].join(', ')}.`);
126
+ }
127
+ if (category && slug.includes('/') && slug.split('/')[0] !== category) {
128
+ throw new Error(`Invalid gctree update input: ${subject}.category must match the first segment of slug "${slug}".`);
129
+ }
130
+ });
131
+ }
14
132
  export async function updateBranchContext({ home, input, branch, }) {
133
+ validateContextUpdateInput(input);
15
134
  const targetBranch = branch || input.branch || DEFAULT_BRANCH;
16
135
  await ensureBranchExists(home, targetBranch);
17
136
  await mkdir(branchDocsDir(home, targetBranch), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handsupmin/gc-tree",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Global Context Tree, a lightweight branch-aware global context orchestrator for AI coding tools",
5
5
  "type": "module",
6
6
  "private": false,
@@ -18,4 +18,34 @@ Use guided updates for durable changes and reserve onboarding for empty gc-branc
18
18
  - `gctree update-global-context`
19
19
  - `gctree update-gc`
20
20
  - `gctree ugc`
21
- 4. Keep the current gc-branch explicit throughout the conversation.
21
+ 4. Before writing update JSON, find the target doc with `gctree resolve --query "<new fact or term>"`. If the summary is insufficient, read the full doc with `gctree show-doc --id "<id>"`.
22
+ 5. For an existing doc, preserve useful existing Details and Index Entries unless the user explicitly wants them removed.
23
+ 6. Use `slug` as the durable path without `docs/` and without `.md`. Example: `docs/conventions/mvldev-assignment-backend-smson.md` becomes `"slug": "conventions/mvldev-assignment-backend-smson"`.
24
+ 7. Use exactly this `gctree __apply-update` JSON schema. Do not invent fields. In particular, never use `id`, `path`, or `content`; use `slug`, `body`, and `indexEntries`.
25
+
26
+ ```json
27
+ {
28
+ "branch": "<gc-branch>",
29
+ "docs": [
30
+ {
31
+ "title": "conventions: example repo",
32
+ "slug": "conventions/example-repo",
33
+ "category": "conventions",
34
+ "summary": "Actionable one-paragraph summary with patterns, commands, and constraints.",
35
+ "body": "## Patterns\n\n- Details body only. Do not include # title, ## Summary, or ## Index Entries here.",
36
+ "indexLabel": "example repo conventions",
37
+ "indexEntries": ["example repo", "ExampleRepo", "주요 한국어 검색어", "command names", "field names"]
38
+ }
39
+ ]
40
+ }
41
+ ```
42
+
43
+ 8. `body` is only the Details body. It may contain useful second-level sections such as `## Patterns`, but must not contain the top-level title, `## Summary`, or `## Index Entries`.
44
+ 9. `indexEntries` is mandatory. Add aliases, related terms, command names, field names, workflow names, acronyms, Korean terms, and English terms so `gctree resolve` can find the doc without translation.
45
+ 10. Apply the update with `gctree __apply-update --input <temp-file>`.
46
+ 11. Verify immediately:
47
+ - `gctree verify-onboarding --branch <gc-branch>`
48
+ - `gctree resolve --query "<new keyword>"`
49
+ - `gctree show-doc --id "<slug>"`
50
+ 12. If you intended to update an existing doc and `doc_count` increases unexpectedly, do not report success. Inspect the duplicate, remove the accidental doc, and re-apply with the correct `slug`.
51
+ 13. Keep the current gc-branch explicit throughout the conversation and tell the user exactly which docs changed.
@@ -1,73 +0,0 @@
1
- import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
- import { basename, join } from 'node:path';
3
- import { renderDocMarkdown, slugify } from './markdown.js';
4
- import { branchDir, branchDocsDir, branchProposalsDir, DEFAULT_BRANCH } from './paths.js';
5
- import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
6
- export async function proposeUpdate({ home, input, branch, }) {
7
- const targetBranch = branch || input.branch || DEFAULT_BRANCH;
8
- await ensureBranchExists(home, targetBranch);
9
- await mkdir(branchProposalsDir(home, targetBranch), { recursive: true });
10
- const now = new Date().toISOString();
11
- const id = `${now.replace(/[:.]/g, '-')}-${slugify(input.title)}`;
12
- const proposal = {
13
- version: 1,
14
- id,
15
- status: 'proposed',
16
- branch: targetBranch,
17
- title: input.title.trim(),
18
- summary: input.summary.trim(),
19
- created_at: now,
20
- changes: input.docs.map((doc) => ({
21
- path: `docs/${slugify(doc.slug || doc.title)}.md`,
22
- title: doc.title.trim(),
23
- summary: doc.summary.trim(),
24
- body: doc.body.trim(),
25
- ...(doc.tags?.length ? { tags: doc.tags } : {}),
26
- })),
27
- };
28
- const proposalPath = join(branchProposalsDir(home, targetBranch), `${id}.json`);
29
- await writeFile(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`, 'utf8');
30
- return { proposal_path: proposalPath, proposal };
31
- }
32
- export async function applyProposal({ home, proposalPath, }) {
33
- const raw = await readFile(proposalPath, 'utf8');
34
- const proposal = JSON.parse(raw);
35
- await ensureBranchExists(home, proposal.branch);
36
- await mkdir(branchDocsDir(home, proposal.branch), { recursive: true });
37
- const updatedDocs = [];
38
- for (const change of proposal.changes) {
39
- const fullPath = join(branchDir(home, proposal.branch), change.path);
40
- await writeFile(fullPath, renderDocMarkdown({
41
- title: change.title,
42
- summary: change.summary,
43
- body: change.body,
44
- tags: change.tags,
45
- }), 'utf8');
46
- updatedDocs.push(fullPath);
47
- }
48
- proposal.status = 'applied';
49
- proposal.applied_at = new Date().toISOString();
50
- await writeFile(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`, 'utf8');
51
- await writeIndexFromDocs(home, proposal.branch);
52
- await updateBranchMeta(home, proposal.branch, {
53
- summary: proposal.summary || (await readBranchSummary(home, proposal.branch)),
54
- });
55
- return {
56
- branch: proposal.branch,
57
- updated_docs: updatedDocs,
58
- proposal_path: proposalPath,
59
- };
60
- }
61
- async function readBranchSummary(home, branch) {
62
- const branchJson = await readFile(join(branchDir(home, branch), 'branch.json'), 'utf8');
63
- return JSON.parse(branchJson).summary || '';
64
- }
65
- export async function listProposals(home, branch) {
66
- return (await readdir(branchProposalsDir(home, branch)).catch(() => []))
67
- .filter((file) => file.endsWith('.json'))
68
- .sort()
69
- .map((file) => join(branchProposalsDir(home, branch), file));
70
- }
71
- export function proposalBasename(path) {
72
- return basename(path);
73
- }