@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 +51 -15
- package/dist/src/hook.js +3 -3
- package/dist/src/scaffold.js +28 -8
- package/dist/src/update.js +119 -0
- package/package.json +1 -1
- package/skills/update-global-context/SKILL.md +31 -1
- package/dist/src/proposals.js +0 -73
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,
|
|
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
|
|
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:
|
|
304
|
+
current_repo: resolved.current_repo,
|
|
305
|
+
source: resolved.source,
|
|
293
306
|
branch_repo_map_path: branchRepoMapPath(home),
|
|
294
|
-
repo_scope_status:
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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
|
];
|
package/dist/src/scaffold.js
CHANGED
|
@@ -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.
|
|
111
|
-
'3. Decide category and
|
|
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.
|
|
119
|
-
'5.
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
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
|
-
'
|
|
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() {
|
package/dist/src/update.js
CHANGED
|
@@ -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
|
@@ -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.
|
|
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.
|
package/dist/src/proposals.js
DELETED
|
@@ -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
|
-
}
|