@nazty_labs/common-ground 0.5.1
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/LICENSE +21 -0
- package/README.md +64 -0
- package/SETUP.md +219 -0
- package/dist/access.d.ts +172 -0
- package/dist/access.js +175 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +198 -0
- package/dist/commands.d.ts +189 -0
- package/dist/commands.js +202 -0
- package/dist/discovery.d.ts +73 -0
- package/dist/discovery.js +417 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +22 -0
- package/dist/export.d.ts +14 -0
- package/dist/export.js +86 -0
- package/dist/guidance.d.ts +11 -0
- package/dist/guidance.js +75 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/hooks.js +141 -0
- package/dist/init.d.ts +304 -0
- package/dist/init.js +150 -0
- package/dist/maintenance.d.ts +126 -0
- package/dist/maintenance.js +23 -0
- package/dist/matching.d.ts +17 -0
- package/dist/matching.js +32 -0
- package/dist/model.d.ts +974 -0
- package/dist/model.js +21 -0
- package/dist/navigation.d.ts +164 -0
- package/dist/navigation.js +164 -0
- package/dist/operations.d.ts +10 -0
- package/dist/operations.js +130 -0
- package/dist/paging.d.ts +5 -0
- package/dist/paging.js +32 -0
- package/dist/retrieval.d.ts +146 -0
- package/dist/retrieval.js +150 -0
- package/dist/review-files.d.ts +3 -0
- package/dist/review-files.js +106 -0
- package/dist/review.d.ts +86 -0
- package/dist/review.js +124 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +105 -0
- package/dist/source-search.d.ts +63 -0
- package/dist/source-search.js +245 -0
- package/dist/store.d.ts +452 -0
- package/dist/store.js +718 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/workflow.d.ts +450 -0
- package/dist/workflow.js +317 -0
- package/docs/architecture.md +65 -0
- package/docs/audit-0.4.0.md +42 -0
- package/docs/demo.md +42 -0
- package/docs/discovery.md +70 -0
- package/docs/knowledge-policy.md +51 -0
- package/docs/pillar-contract.md +98 -0
- package/docs/quiet-workflow.md +98 -0
- package/docs/releases.md +157 -0
- package/package.json +52 -0
- package/schemas/admission.schema.json +75 -0
- package/schemas/knowledge.schema.json +192 -0
- package/schemas/patch.schema.json +220 -0
- package/schemas/update.schema.json +218 -0
package/dist/access.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { relativePath } from './model.js';
|
|
4
|
+
import { same } from './store.js';
|
|
5
|
+
import { page } from './paging.js';
|
|
6
|
+
import { reviewDocuments } from './review-files.js';
|
|
7
|
+
import { queryTerms, queryPath, termWeights, matchFields, termMatch } from './matching.js';
|
|
8
|
+
import { isSourceSearchPathAllowed } from './source-search.js';
|
|
9
|
+
const within = (file, scope) => file === scope || file.startsWith(`${scope}/`);
|
|
10
|
+
const overlaps = (a, b) => within(a, b) || within(b, a);
|
|
11
|
+
const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
12
|
+
const tracks = (fact, file) => fact.sourceScope.some(s => within(file, s)) || fact.evidence.some(e => e.path === file);
|
|
13
|
+
const paging = { cursor: z.string().optional(), limit: z.number().int().min(1).max(20).optional() };
|
|
14
|
+
export const Lookup = z.object({ path: relativePath.optional(), query: z.string().trim().min(1).optional(), verify: z.boolean().default(false), verbose: z.boolean().default(false), cursor: paging.cursor, limit: z.number().int().min(1).max(5).default(5) }).strict();
|
|
15
|
+
export const Assess = z.object({ paths: z.array(relativePath), review: z.boolean().default(false), ...paging }).strict();
|
|
16
|
+
async function registryOrSetup(store) {
|
|
17
|
+
try {
|
|
18
|
+
return await store.read();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT')
|
|
22
|
+
throw error;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const setup = { state: 'knowledge-unavailable', writesKnowledge: false, items: [], next: 'Continue from source. Run cground init and review setup when appropriate; lookup requires no task context.' };
|
|
27
|
+
/** Explicit verification is fact-scoped. A cache lives for this call only, never across reads/publication. */
|
|
28
|
+
async function verifySelected(store, registry, selected) {
|
|
29
|
+
const index = new Map(store.facts(registry).map(entry => [entry.key, entry]));
|
|
30
|
+
const required = [...new Set(selected.flatMap(key => [key, ...store.upstreamFacts(registry, key, index)]))];
|
|
31
|
+
const cache = new Map();
|
|
32
|
+
const checked = new Map();
|
|
33
|
+
for (const key of required) {
|
|
34
|
+
const { chapter, fact } = index.get(key);
|
|
35
|
+
const errors = [];
|
|
36
|
+
let changedPaths = [];
|
|
37
|
+
try {
|
|
38
|
+
await store.validateFacts({ paths: chapter.paths, facts: [fact] });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
errors.push(error.message);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const current = await store.snapshot({ paths: chapter.paths, facts: [fact] }, cache);
|
|
45
|
+
const baseline = Object.fromEntries(Object.entries(chapter.sources).filter(([file]) => tracks(fact, file)));
|
|
46
|
+
changedPaths = [...new Set([...Object.keys(current), ...Object.keys(baseline)])].filter(file => current[file] !== baseline[file]).sort();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
errors.push(error.message);
|
|
50
|
+
}
|
|
51
|
+
checked.set(key, { changedPaths, errors });
|
|
52
|
+
}
|
|
53
|
+
return new Map(selected.map(key => {
|
|
54
|
+
const { chapter, chapterId } = index.get(key), own = checked.get(key);
|
|
55
|
+
const fingerprints = store.dependencyFingerprints(registry, chapterId);
|
|
56
|
+
const dependencyIssues = store.upstreamFacts(registry, key, index).filter(dep => {
|
|
57
|
+
const check = checked.get(dep);
|
|
58
|
+
return check.errors.length || check.changedPaths.length || chapter.dependencyFingerprints[dep] !== fingerprints[dep];
|
|
59
|
+
});
|
|
60
|
+
return [key, { status: own.errors.length || own.changedPaths.length || dependencyIssues.length ? 'needs-review' : 'evidence-unchanged', changedPaths: own.changedPaths.slice(0, 10), changedPathCount: own.changedPaths.length, errors: own.errors.slice(0, 3), errorCount: own.errors.length, dependencyIssues: dependencyIssues.slice(0, 5), dependencyIssueCount: dependencyIssues.length }];
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
/** Cheap navigation: registry reads only unless live verification is explicitly requested. */
|
|
64
|
+
export async function lookup(store, input) {
|
|
65
|
+
const args = Lookup.parse(input);
|
|
66
|
+
if (!args.path && !args.query)
|
|
67
|
+
throw new Error('Supply a lookup query or --path.');
|
|
68
|
+
const registry = await registryOrSetup(store);
|
|
69
|
+
if (!registry)
|
|
70
|
+
return { summary: 'No direct answer found.', ...setup, coverage: { status: 'unavailable' }, sourceSearch: { status: 'not-run', next: 'Choose source paths and run cground source-search QUERY --path PATH; this reads live source without requiring stored knowledge.' } };
|
|
71
|
+
const entries = store.facts(registry), candidatePath = queryPath(args.query);
|
|
72
|
+
const inferredPath = !args.path && candidatePath && store.chapters(registry).some(({ chapter }) => [...chapter.paths, ...chapter.facts.flatMap(f => f.evidence.map(e => e.path))].some(p => overlaps(candidatePath, p))) ? candidatePath : undefined;
|
|
73
|
+
const requestedPath = args.path ?? inferredPath, terms = queryTerms(args.query ?? '');
|
|
74
|
+
const { weights, commonTerms, distinctiveTerms } = termWeights(terms, entries.map(({ key, fact }) => `${key} ${fact.statement} ${fact.evidence.map(e => e.path).join(' ')}`));
|
|
75
|
+
const matches = entries.map(entry => {
|
|
76
|
+
const { fact, chapter } = entry;
|
|
77
|
+
const pathScore = !requestedPath ? 0 : fact.evidence.some(e => overlaps(requestedPath, e.path)) ? 100 : fact.sourceScope.some(s => overlaps(requestedPath, s)) ? 50 : chapter.paths.some(s => overlaps(requestedPath, s)) ? 1 : 0;
|
|
78
|
+
const sourceText = fact.evidence.map(e => e.path).join(' ');
|
|
79
|
+
const lexical = matchFields(terms, `${entry.key} ${sourceText}`, fact.statement, weights);
|
|
80
|
+
const direct = pathScore !== 1 && (inferredPath || !terms.length ? pathScore === 100 : distinctiveTerms.length > 0 && distinctiveTerms.every(term => termMatch(`${fact.statement} ${sourceText}`, term) > 0));
|
|
81
|
+
return { entry, pathScore, direct, ...lexical };
|
|
82
|
+
}).filter(m => requestedPath ? m.pathScore > 0 : m.matchedTerms.length > 0)
|
|
83
|
+
.sort((a, b) => b.pathScore - a.pathScore || b.weight - a.weight || b.matchedTerms.length - a.matchedTerms.length || a.entry.key.localeCompare(b.entry.key));
|
|
84
|
+
const direct = matches.some(m => m.direct);
|
|
85
|
+
const records = matches.map(({ entry, pathScore, matchedTerms, direct }) => ({ factId: entry.key, statement: entry.fact.statement, sourcePaths: [...new Set(entry.fact.evidence.map(e => e.path))],
|
|
86
|
+
relevance: pathScore === 1 ? 'ownership-only' : terms.length && matchedTerms.length < terms.length ? 'partial-query' : direct ? 'matched' : 'weak-match',
|
|
87
|
+
...(args.verbose ? { chapterId: entry.chapterId, matchReason: pathScore === 100 ? 'direct-evidence' : pathScore === 50 ? 'source-scope' : pathScore === 1 ? 'ownership-suggestion' : 'query-terms',
|
|
88
|
+
matchedTerms, unmatchedTerms: terms.filter(t => !matchedTerms.includes(t)), queryCoverage: terms.length ? { matched: matchedTerms.length, total: terms.length } : null,
|
|
89
|
+
matchedPaths: requestedPath ? (pathScore === 100 ? entry.fact.evidence.map(e => e.path) : pathScore === 50 ? entry.fact.sourceScope : entry.chapter.paths).filter(p => overlaps(requestedPath, p)) : [] } : {}),
|
|
90
|
+
}));
|
|
91
|
+
const selected = page(records, args.cursor, args.limit, digest({ registry, path: args.path, query: args.query, verify: args.verify, verbose: args.verbose }), 6000);
|
|
92
|
+
const verified = args.verify ? await verifySelected(store, registry, selected.items.map(item => item.factId)) : undefined;
|
|
93
|
+
if (args.verify && !same(registry, await store.read()))
|
|
94
|
+
throw new Error('Knowledge changed during lookup; retry.');
|
|
95
|
+
// Navigation remains separate from facts and does not scan or hash the filesystem.
|
|
96
|
+
const routes = store.chapters(registry).map(({ key, pillar, chapter }) => {
|
|
97
|
+
const paths = [...new Set([...chapter.paths, ...Object.keys(chapter.sources).filter(file => chapter.paths.some(scope => within(file, scope)))])];
|
|
98
|
+
const lexical = matchFields(terms, `${key} ${pillar.title} ${chapter.title} ${paths.join(' ')}`, `${pillar.scope} ${chapter.scope}`, weights);
|
|
99
|
+
const pathMatch = !!requestedPath && chapter.paths.some(scope => overlaps(requestedPath, scope));
|
|
100
|
+
const ranked = paths.sort((a, b) => Number(!!requestedPath && overlaps(requestedPath, b)) - Number(!!requestedPath && overlaps(requestedPath, a))
|
|
101
|
+
|| terms.reduce((n, t) => n + (termMatch(b, t) - termMatch(a, t)) * (weights.get(t) ?? 1), 0) || a.localeCompare(b));
|
|
102
|
+
return { chapterId: key, title: chapter.title, paths: ranked.slice(0, 5), pathCount: paths.length, pathsTruncated: paths.length > 5,
|
|
103
|
+
matchedTerms: lexical.matchedTerms, pathMatch, weight: lexical.weight };
|
|
104
|
+
}).filter(route => requestedPath ? route.pathMatch : route.matchedTerms.length > 0)
|
|
105
|
+
.sort((a, b) => b.weight - a.weight || b.matchedTerms.length - a.matchedTerms.length || a.chapterId.localeCompare(b.chapterId));
|
|
106
|
+
const navigation = { kind: 'ownership-navigation', freshness: 'not-checked', items: routes.slice(0, 3).map(({ pathMatch, weight, matchedTerms, pathCount, pathsTruncated, ...route }) => ({ ...route, ...(args.verbose ? { matchedTerms, pathCount, pathsTruncated } : {}) })), total: routes.length, truncated: routes.length > 3 };
|
|
107
|
+
const fallbackPaths = [...new Set(requestedPath ? [requestedPath] : [
|
|
108
|
+
...matches.slice(0, 5).flatMap(({ entry }) => entry.fact.evidence.map(e => e.path)),
|
|
109
|
+
...routes.slice(0, 3).flatMap(r => r.paths),
|
|
110
|
+
])].filter(isSourceSearchPathAllowed).slice(0, 5);
|
|
111
|
+
const fallbackTerms = distinctiveTerms.length ? distinctiveTerms : terms;
|
|
112
|
+
const resultItems = selected.items.map(item => {
|
|
113
|
+
const freshness = verified?.get(item.factId) ?? { status: 'not-checked' };
|
|
114
|
+
return { ...item, freshness: !args.verbose && freshness.status === 'evidence-unchanged' ? { status: freshness.status } : freshness };
|
|
115
|
+
});
|
|
116
|
+
return { summary: direct ? 'Relevant stored facts found.' : 'No direct answer found.', state: records.length ? 'matches' : 'no-matches', writesKnowledge: false,
|
|
117
|
+
coverage: { status: direct ? 'direct-match' : records.length ? 'weak' : 'none', ...(args.verbose ? { terms, downweightedTerms: commonTerms, distinctiveTerms } : {}) },
|
|
118
|
+
// With weak coverage, navigation precedes stored partial matches in serialized output.
|
|
119
|
+
...(!direct ? { navigation } : {}), items: resultItems, total: selected.total, nextCursor: selected.nextCursor, ...(direct ? { navigation } : {}),
|
|
120
|
+
...(!direct ? { sourceSearch: { status: 'not-run', operation: 'source-search', ...(fallbackPaths.length ? { args: { query: fallbackTerms.join(' ') || args.query || requestedPath, paths: fallbackPaths } } : {}),
|
|
121
|
+
next: fallbackPaths.length ? 'Run this separate operation to inspect live source evidence; adjust the suggested paths and query first if needed.' : 'Choose eligible source paths before running cground source-search QUERY --path PATH. Read any excluded configuration paths directly.' } } : {}),
|
|
122
|
+
caveat: 'Stored matches are lexical navigation, not proof of an answer or complete coverage. Read source. Navigation paths are unverified; --verify checks only selected facts and dependencies.',
|
|
123
|
+
next: direct ? 'Read the cited source before relying on these claims.' : 'Read source using the navigation hints or the separate source-search fallback.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Compare only task-touched portions of candidate fact scopes, including additions/deletions. */
|
|
127
|
+
export async function changedFacts(store, registry, paths) {
|
|
128
|
+
const candidates = store.facts(registry).filter(({ fact }) => paths.some(p => fact.sourceScope.some(s => overlaps(p, s)) || fact.evidence.some(e => overlaps(p, e.path))));
|
|
129
|
+
const cache = new Map(), observations = [];
|
|
130
|
+
const changed = [];
|
|
131
|
+
for (const entry of candidates) {
|
|
132
|
+
const { fact, chapter } = entry;
|
|
133
|
+
const errors = [];
|
|
134
|
+
let changedPaths = [];
|
|
135
|
+
const sourceScope = [...new Set(fact.sourceScope.flatMap(scope => paths.filter(p => overlaps(p, scope)).map(p => within(p, scope) ? p : scope)))];
|
|
136
|
+
const evidence = fact.evidence.filter(e => paths.some(p => overlaps(p, e.path)));
|
|
137
|
+
try {
|
|
138
|
+
const current = await store.snapshot({ paths: chapter.paths, facts: [{ ...fact, sourceScope, evidence }] }, cache);
|
|
139
|
+
const baseline = Object.fromEntries(Object.entries(chapter.sources).filter(([file]) => tracks(fact, file) && paths.some(p => overlaps(p, file))));
|
|
140
|
+
changedPaths = [...new Set([...Object.keys(current), ...Object.keys(baseline)])].filter(p => current[p] !== baseline[p]).sort();
|
|
141
|
+
observations.push({ key: entry.key, current });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
errors.push(error.message);
|
|
145
|
+
observations.push({ key: entry.key, errors });
|
|
146
|
+
}
|
|
147
|
+
if (changedPaths.length || errors.length)
|
|
148
|
+
changed.push({ entry, changedPaths, errors });
|
|
149
|
+
}
|
|
150
|
+
return { candidates, candidateFactCount: candidates.length, changed, fingerprint: digest(observations) };
|
|
151
|
+
}
|
|
152
|
+
/** Read-only change assessment. Review records are created only by subsequent preparation. */
|
|
153
|
+
export async function assessChanges(store, input) {
|
|
154
|
+
const { paths: raw, cursor, limit, review } = Assess.parse(input), paths = [...new Set(raw)].sort();
|
|
155
|
+
const registry = await registryOrSetup(store);
|
|
156
|
+
if (!registry)
|
|
157
|
+
return setup;
|
|
158
|
+
const impact = await changedFacts(store, registry, paths);
|
|
159
|
+
const scopes = review ? [...new Set(impact.candidates.map(entry => entry.chapterId))].sort().map(chapterId => ({ kind: 'review-scope', chapterId, requiredChapterIds: store.related(registry, chapterId) })) : [];
|
|
160
|
+
const keys = [...new Set(scopes.flatMap(scope => scope.requiredChapterIds))].sort();
|
|
161
|
+
// Whole chapters are required only on the branch that might revise knowledge.
|
|
162
|
+
const files = keys.length ? await store.reviewFiles(registry, keys, paths) : { sourceFiles: [], documentFiles: await reviewDocuments(store, paths, 'focused') };
|
|
163
|
+
const entries = [...impact.changed.map(({ entry, changedPaths, errors }) => ({ kind: 'affected-fact', factId: entry.key, statement: entry.fact.statement, sourcePaths: [...new Set(entry.fact.evidence.map(e => e.path))], changedPaths: changedPaths.slice(0, 10), changedPathCount: changedPaths.length, errors })),
|
|
164
|
+
...scopes, ...keys.flatMap(chapterId => {
|
|
165
|
+
const chapter = store.chapter(registry, chapterId);
|
|
166
|
+
return [{ kind: 'chapter', chapterId, expectedRevision: chapter.revision, factCount: chapter.facts.length },
|
|
167
|
+
...chapter.facts.map(fact => ({ kind: 'fact', chapterId, fact }))];
|
|
168
|
+
}), ...files.sourceFiles.map(path => ({ kind: 'source', path })), ...files.documentFiles.map(path => ({ kind: 'document', path }))];
|
|
169
|
+
if (!same(registry, await store.read()))
|
|
170
|
+
throw new Error('Knowledge changed during assessment; retry.');
|
|
171
|
+
return { state: keys.length ? 'review-required' : impact.changed.length ? 'source-review-required' : 'no-fact-review', writesKnowledge: false, createsTask: false,
|
|
172
|
+
candidateFactCount: impact.candidateFactCount, affectedFactCount: impact.changed.length,
|
|
173
|
+
...page(entries, cursor, limit, digest({ registry, paths, review, observations: impact.fingerprint }), 12000),
|
|
174
|
+
next: keys.length ? 'Read every package page and its source/documentation. Prepare each initiating review-scope with its exact required chapters; do not combine unrelated scopes. prepare-patch accepts no taskId; commit only after verification. Source drift alone does not prove a claim false.' : impact.changed.length ? 'Verify affected claims against source. If a correction is needed, rerun assess with --review for the complete chapter/source package. No task start or finish is needed.' : 'Review the listed local documentation and source relevant to the edit. No task start or finish is needed. Path matching cannot rule out semantic connections discovered in source.' };
|
|
175
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { parseCommand, helpText, payloadExamples } from './commands.js';
|
|
6
|
+
import { version } from './version.js';
|
|
7
|
+
const list = (value) => value ? String(value).split(',') : [];
|
|
8
|
+
async function json(file) {
|
|
9
|
+
try {
|
|
10
|
+
if (file === '-') {
|
|
11
|
+
process.stdin.setEncoding('utf8');
|
|
12
|
+
let text = '';
|
|
13
|
+
for await (const chunk of process.stdin)
|
|
14
|
+
text += chunk;
|
|
15
|
+
return JSON.parse(text);
|
|
16
|
+
}
|
|
17
|
+
return JSON.parse(await fs.readFile(path.resolve(file), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const { GroundError } = await import('./errors.js');
|
|
21
|
+
const code = error.code;
|
|
22
|
+
throw new GroundError(code === 'ENOENT' ? 'INPUT_NOT_FOUND' : error instanceof SyntaxError ? 'INVALID_JSON' : 'INPUT_UNREADABLE', `Cannot read JSON from ${file}: ${error.message}`, [file], code === 'ENOENT' ? 'Check the input filename, or supply the payload with --stdin.' : error instanceof SyntaxError ? 'Correct the JSON syntax; inspect cground schema <operation>.' : 'Check the input file permissions and ensure it is a readable regular file.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Adapt shell arguments to the same structured operations used by MCP.
|
|
26
|
+
async function input(name, args, values) {
|
|
27
|
+
const [first, second, third] = args;
|
|
28
|
+
const paging = { cursor: values.cursor, limit: values.limit === undefined ? undefined : Number(values.limit) };
|
|
29
|
+
switch (name) {
|
|
30
|
+
case 'init': return { skipHook: values['skip-hook'] ?? false };
|
|
31
|
+
case 'lookup': return { path: values.path, query: args.join(' ') || undefined, verify: values.verify ?? false, verbose: values.verbose ?? false, ...paging };
|
|
32
|
+
case 'source-search': return { paths: list(values.path), query: args.join(' '), limit: paging.limit };
|
|
33
|
+
case 'assess': return { paths: list(values.touched), review: values.review ?? false, ...paging };
|
|
34
|
+
case 'schema': return { operation: first, both: values.both ?? false };
|
|
35
|
+
case 'scan': return { exclude: list(values.exclude) };
|
|
36
|
+
case 'bootstrap':
|
|
37
|
+
case 'seed-batch': return { ...await json(first), dryRun: values['dry-run'] ?? false, approved: values.approve, preflight: values.preflight };
|
|
38
|
+
case 'approve': return { pillars: (await json(first)).pillars, approved: values.approve, verbose: values.verbose, standaloneReason: values['standalone-reason'] };
|
|
39
|
+
case 'approve-chapters': return { pillarId: first, chapters: (await json(second)).chapters, approved: values.approve, verbose: values.verbose };
|
|
40
|
+
case 'seed':
|
|
41
|
+
case 'admit': return { chapterId: first, facts: await json(second), approved: values.approve, verbose: values.verbose };
|
|
42
|
+
case 'migrate': return { approved: values.approve };
|
|
43
|
+
case 'task start': return { paths: list(values.touched), signal: values.signal };
|
|
44
|
+
case 'task assess': return { taskId: first, paths: list(values.touched), ...paging };
|
|
45
|
+
case 'task finish': return { taskId: first, ...paging };
|
|
46
|
+
case 'read-knowledge':
|
|
47
|
+
case 'prepare-patch':
|
|
48
|
+
case 'prepare': return json(first);
|
|
49
|
+
case 'propose-facts': return { taskId: first, chapterId: second, facts: await json(third) };
|
|
50
|
+
case 'drop-facts': return { taskId: first, factKeys: args.slice(1) };
|
|
51
|
+
case 'accept-facts': return { taskId: first, review: await json(second), approved: values.approve };
|
|
52
|
+
case 'start':
|
|
53
|
+
case 'owners': return { path: values.path, signal: values.signal ?? (args.join(' ') || undefined), ...paging };
|
|
54
|
+
case 'graph': return { pillarId: first, ...paging };
|
|
55
|
+
case 'check':
|
|
56
|
+
case 'validate': return { target: first ?? 'all', cleanup: String(values.cleanup).toLowerCase() === 'y', allResults: values['all-results'] ?? false, ...paging };
|
|
57
|
+
case 'review': return { target: first ?? 'all', staged: values.staged ?? false, evidence: values.evidence ?? false, ...paging };
|
|
58
|
+
case 'tidy': return { target: first, ...paging };
|
|
59
|
+
case 'review-checklist': return { chapterIds: args, touchedPaths: list(values.touched), ...paging };
|
|
60
|
+
case 'list': return paging;
|
|
61
|
+
case 'chapters': return { pillarId: first, ...paging };
|
|
62
|
+
case 'read': return { chapterId: first, ...paging };
|
|
63
|
+
case 'fact': return { chapterId: first, factId: second };
|
|
64
|
+
case 'review-plan': return { chapterId: first, factIds: values.facts === undefined ? undefined : list(values.facts) };
|
|
65
|
+
case 'search': return { query: args.join(' '), chapterId: values.chapter, limit: paging.limit };
|
|
66
|
+
case 'commit': return { proposalId: first };
|
|
67
|
+
default: return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function initText(result, root) {
|
|
71
|
+
const link = (file) => `[${file}](${path.resolve(root, file)})`;
|
|
72
|
+
return [result.setup.status === 'complete-with-warnings' ? 'Common Ground initialized with setup warnings.' : 'Common Ground initialized.', result.hook,
|
|
73
|
+
'existingRegistry' in result
|
|
74
|
+
? `Ask your agent to summarize knowledge changes with cground review before sharing.`
|
|
75
|
+
: `Ask your agent to summarize the proposed responsibility map and verify its facts.\nPublish with content approval or explicit developer delegation to save the initial map within scope.`,
|
|
76
|
+
`Local Markdown: ${link(result.markdown.path)}`].join('\n');
|
|
77
|
+
}
|
|
78
|
+
function checkText(result) {
|
|
79
|
+
const lines = [`${result.target}: ${result.valid ? 'knowledge checks passed' : 'knowledge needs review'}.`,
|
|
80
|
+
`${result.summary.selectedFacts} selected facts; ${result.summary.needsReview} need review.`];
|
|
81
|
+
if (result.summary.unpopulatedChapters.length)
|
|
82
|
+
lines.push(`Empty chapters: ${result.summary.unpopulatedChapters.join(', ')}. Complete approved setup with your agent.`);
|
|
83
|
+
if (!result.summary.selectedFacts)
|
|
84
|
+
lines.push('No facts recorded. Complete setup with your agent.');
|
|
85
|
+
for (const item of result.items) {
|
|
86
|
+
lines.push(`${item.factId}: ${item.status}`);
|
|
87
|
+
for (const error of item.errors)
|
|
88
|
+
lines.push(` ${error}`);
|
|
89
|
+
if (item.changedPaths.length)
|
|
90
|
+
lines.push(` Changed: ${item.changedPaths.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
if (result.nextCursor)
|
|
93
|
+
lines.push(`More results: rerun with --cursor ${result.nextCursor}`);
|
|
94
|
+
if (result.cleanup)
|
|
95
|
+
lines.push(`Cleanup plan: ${result.cleanup.tidyId}`, 'Ask your agent to review the affected source and apply verified corrections.');
|
|
96
|
+
lines.push(`Review changes with your agent: cground review`, `Markdown: ${result.markdown.path}`);
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
async function main() {
|
|
100
|
+
const parsed = parseCommand(process.argv.slice(2));
|
|
101
|
+
if (parsed.help) {
|
|
102
|
+
console.log(helpText(parsed.command, parsed.group));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const { command, values, positionals } = parsed;
|
|
106
|
+
if (!command) {
|
|
107
|
+
console.log(values.json ? JSON.stringify({ version }) : version);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (['bootstrap', 'seed-batch'].includes(command.name) && values['dry-run'] === true) {
|
|
111
|
+
const { Store } = await import('./store.js');
|
|
112
|
+
const { runOperation } = await import('./operations.js');
|
|
113
|
+
console.log(JSON.stringify(await runOperation(new Store(String(values.root ?? process.cwd())), command.operation, await input(command.name, positionals, values)), null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (command.flags.includes('approve') && values.approve !== true)
|
|
117
|
+
throw new Error('Developer approval required: inspect the proposal, then pass --approve.');
|
|
118
|
+
const root = String(values.root ?? process.cwd());
|
|
119
|
+
const { Store } = await import('./store.js');
|
|
120
|
+
const store = new Store(root);
|
|
121
|
+
if (command.name === 'serve') {
|
|
122
|
+
const { serve } = await import('./server.js');
|
|
123
|
+
await serve(store, values.profile);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const { runOperation } = await import('./operations.js');
|
|
127
|
+
let output = await runOperation(store, command.operation, await input(command.name, positionals, values));
|
|
128
|
+
if (command.name === 'schema') {
|
|
129
|
+
output = { ...output, example: payloadExamples[positionals[0]], note: 'cliPayloadSchema describes the CLI JSON payload; --both includes inputSchema for MCP arguments. CLI seed/admit/propose-facts payloads contain only the facts array; approval is supplied with --approve.' };
|
|
130
|
+
}
|
|
131
|
+
if (['check', 'validate'].includes(command.name)) {
|
|
132
|
+
let result = output;
|
|
133
|
+
if (result.cleanupPrompt) {
|
|
134
|
+
const { staleMessage, checkKnowledge } = await import('./maintenance.js');
|
|
135
|
+
console.error(staleMessage(result));
|
|
136
|
+
console.error('Source drift requires review; it does not prove the stored claim is false.');
|
|
137
|
+
if (values.cleanup === undefined && process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY && !values.json) {
|
|
138
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
139
|
+
let answer;
|
|
140
|
+
try {
|
|
141
|
+
answer = await prompt.question('Start automatic cleanup? [y/N] ');
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
prompt.close();
|
|
145
|
+
}
|
|
146
|
+
if (['y', 'yes'].includes(answer.trim().toLowerCase()))
|
|
147
|
+
result = await checkKnowledge(store, result.target, true, values.cursor, values.limit === undefined ? undefined : Number(values.limit), result.affected, values['all-results'] === true);
|
|
148
|
+
}
|
|
149
|
+
else if (values.cleanup === undefined)
|
|
150
|
+
console.error('Start automatic cleanup? Use --cleanup y or --cleanup n (agent verification required).');
|
|
151
|
+
}
|
|
152
|
+
if (!result.valid)
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
output = process.stdout.isTTY && !values.json ? checkText(result) : result;
|
|
155
|
+
}
|
|
156
|
+
if (command.name === 'review' && !values.json) {
|
|
157
|
+
const { reviewText } = await import('./review.js');
|
|
158
|
+
output = reviewText(output);
|
|
159
|
+
}
|
|
160
|
+
if (command.name === 'doctor' && !output.valid)
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
if (process.stdout.isTTY && !values.json) {
|
|
163
|
+
if (command.name === 'doctor') {
|
|
164
|
+
const result = output;
|
|
165
|
+
const missing = Object.entries(result).filter(([, value]) => value === 'missing').map(([file]) => file);
|
|
166
|
+
output = result.valid ? 'Common Ground setup checks passed.' : `Common Ground setup needs attention.\n${missing.map(file => `Missing: ${file}`).join('\n')}${result.registryError ? `\nRegistry: ${result.registryError}` : ''}\nGuidance: ${result.guidance.status}. Run cground refresh-guidance for stale instructions; run cground init for missing setup. Review any registry errors with your agent.`;
|
|
167
|
+
}
|
|
168
|
+
if (command.name === 'export') {
|
|
169
|
+
const result = output;
|
|
170
|
+
output = `Markdown ${result.changed ? 'refreshed' : 'already current'}: ${path.resolve(root, result.path)}`;
|
|
171
|
+
}
|
|
172
|
+
if (command.name === 'tidy') {
|
|
173
|
+
const result = output;
|
|
174
|
+
output = [`Cleanup scope: ${result.target} (${result.requiredChapterCount} chapters).`, ...result.chapters.items.map(chapter => ` ${chapter.chapterId}`), ...('tidyId' in result && result.tidyId ? [`Tidy ID: ${result.tidyId}`] : []), ...(result.chapters.nextCursor ? [`More chapters: rerun with --cursor ${result.chapters.nextCursor}`] : []), 'Ask your agent to read all required chapters and source, then submit verified corrections.'].join('\n');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (command.name === 'hook check' && !values.json) {
|
|
178
|
+
const { message } = output;
|
|
179
|
+
if (message)
|
|
180
|
+
console.error(message);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (command.name === 'init' && !values.json)
|
|
184
|
+
output = initText(output, root);
|
|
185
|
+
console.log(typeof output === 'string' && !values.json ? output : JSON.stringify(output, null, 2));
|
|
186
|
+
}
|
|
187
|
+
main().catch(async (error) => {
|
|
188
|
+
// Schema issues are useful, but a raw Zod stack/dump is not a CLI explanation.
|
|
189
|
+
const message = Array.isArray(error.issues) ? error.issues.map((issue) => `${issue.path.join('.') || 'input'}: ${issue.message}`).join('; ') : error.message;
|
|
190
|
+
const args = process.argv.slice(2), end = args.indexOf('--');
|
|
191
|
+
if ((end < 0 ? args : args.slice(0, end)).includes('--json')) {
|
|
192
|
+
const { errorPayload } = await import('./errors.js');
|
|
193
|
+
console.error(JSON.stringify({ error: errorPayload(error) }));
|
|
194
|
+
}
|
|
195
|
+
else
|
|
196
|
+
console.error(`Common Ground: ${message}\nRun cground <command> --help for usage.`);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export declare const options: {
|
|
2
|
+
readonly 'skip-hook': {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
readonly label: "--skip-hook";
|
|
5
|
+
readonly description: "Finish repository setup without installing the advisory Git hook; install it later with cground hook install";
|
|
6
|
+
};
|
|
7
|
+
readonly both: {
|
|
8
|
+
readonly type: "boolean";
|
|
9
|
+
readonly label: "--both";
|
|
10
|
+
readonly description: "Include both CLI payload and MCP operation schemas";
|
|
11
|
+
};
|
|
12
|
+
readonly exclude: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly label: "--exclude <paths>";
|
|
15
|
+
readonly description: "Comma-separated repository-relative paths to exclude from discovery";
|
|
16
|
+
};
|
|
17
|
+
readonly verify: {
|
|
18
|
+
readonly type: "boolean";
|
|
19
|
+
readonly label: "--verify";
|
|
20
|
+
readonly description: "Check selected facts and upstream source freshness";
|
|
21
|
+
};
|
|
22
|
+
readonly review: {
|
|
23
|
+
readonly type: "boolean";
|
|
24
|
+
readonly label: "--review";
|
|
25
|
+
readonly description: "Include complete chapters and source/documentation paths before knowledge correction";
|
|
26
|
+
};
|
|
27
|
+
readonly stdin: {
|
|
28
|
+
readonly type: "boolean";
|
|
29
|
+
readonly label: "--stdin";
|
|
30
|
+
readonly description: "Read the JSON payload from stdin instead of a file";
|
|
31
|
+
};
|
|
32
|
+
readonly example: {
|
|
33
|
+
readonly type: "boolean";
|
|
34
|
+
readonly label: "--example";
|
|
35
|
+
readonly description: "Show a copyable JSON payload with help";
|
|
36
|
+
};
|
|
37
|
+
readonly verbose: {
|
|
38
|
+
readonly type: "boolean";
|
|
39
|
+
readonly label: "--verbose";
|
|
40
|
+
readonly description: "Include lookup matching diagnostics or complete mutation objects";
|
|
41
|
+
};
|
|
42
|
+
readonly 'all-results': {
|
|
43
|
+
readonly type: "boolean";
|
|
44
|
+
readonly label: "--all-results";
|
|
45
|
+
readonly description: "Include passing validation rows";
|
|
46
|
+
};
|
|
47
|
+
readonly 'dry-run': {
|
|
48
|
+
readonly type: "boolean";
|
|
49
|
+
readonly label: "--dry-run";
|
|
50
|
+
readonly description: "Preflight without writing knowledge or requiring approval";
|
|
51
|
+
};
|
|
52
|
+
readonly preflight: {
|
|
53
|
+
readonly type: "string";
|
|
54
|
+
readonly label: "--preflight <token>";
|
|
55
|
+
readonly description: "Token from the reviewed dry run; rejects source or payload changes";
|
|
56
|
+
};
|
|
57
|
+
readonly help: {
|
|
58
|
+
readonly type: "boolean";
|
|
59
|
+
readonly short: "h";
|
|
60
|
+
readonly label: "-h, --help";
|
|
61
|
+
readonly description: "Show help without running the command";
|
|
62
|
+
};
|
|
63
|
+
readonly version: {
|
|
64
|
+
readonly type: "boolean";
|
|
65
|
+
readonly short: "V";
|
|
66
|
+
readonly label: "-V, --version";
|
|
67
|
+
readonly description: "Show the installed version";
|
|
68
|
+
};
|
|
69
|
+
readonly root: {
|
|
70
|
+
readonly type: "string";
|
|
71
|
+
readonly label: "--root <path>";
|
|
72
|
+
readonly description: "Repository root (default: current directory)";
|
|
73
|
+
};
|
|
74
|
+
readonly json: {
|
|
75
|
+
readonly type: "boolean";
|
|
76
|
+
readonly label: "--json";
|
|
77
|
+
readonly description: "Return JSON; never prompt for input";
|
|
78
|
+
};
|
|
79
|
+
readonly cursor: {
|
|
80
|
+
readonly type: "string";
|
|
81
|
+
readonly label: "--cursor <token>";
|
|
82
|
+
readonly description: "Continue a paginated result";
|
|
83
|
+
};
|
|
84
|
+
readonly limit: {
|
|
85
|
+
readonly type: "string";
|
|
86
|
+
readonly label: "--limit <1..20>";
|
|
87
|
+
readonly description: "Maximum records per page (default: 10; search: 8)";
|
|
88
|
+
};
|
|
89
|
+
readonly staged: {
|
|
90
|
+
readonly type: "boolean";
|
|
91
|
+
readonly label: "--staged";
|
|
92
|
+
readonly description: "Review staged knowledge instead of the working file";
|
|
93
|
+
};
|
|
94
|
+
readonly evidence: {
|
|
95
|
+
readonly type: "boolean";
|
|
96
|
+
readonly label: "--evidence";
|
|
97
|
+
readonly description: "Include exact changed evidence quotes in the review";
|
|
98
|
+
};
|
|
99
|
+
readonly cleanup: {
|
|
100
|
+
readonly type: "string";
|
|
101
|
+
readonly label: "--cleanup <y|n>";
|
|
102
|
+
readonly description: "Start or decline agent-led cleanup when stale";
|
|
103
|
+
};
|
|
104
|
+
readonly approve: {
|
|
105
|
+
readonly type: "boolean";
|
|
106
|
+
readonly label: "--approve";
|
|
107
|
+
readonly description: "Declare explicit developer approval of the proposed content";
|
|
108
|
+
};
|
|
109
|
+
readonly path: {
|
|
110
|
+
readonly type: "string";
|
|
111
|
+
readonly label: "--path <path>";
|
|
112
|
+
readonly description: "Repository-relative source path for ownership routing";
|
|
113
|
+
};
|
|
114
|
+
readonly signal: {
|
|
115
|
+
readonly type: "string";
|
|
116
|
+
readonly label: "--signal <text>";
|
|
117
|
+
readonly description: "Short symptom or routing hint";
|
|
118
|
+
};
|
|
119
|
+
readonly touched: {
|
|
120
|
+
readonly type: "string";
|
|
121
|
+
readonly label: "--touched <paths>";
|
|
122
|
+
readonly description: "Comma-separated paths touched by this task; empty means none";
|
|
123
|
+
};
|
|
124
|
+
readonly chapter: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
readonly label: "--chapter <id>";
|
|
127
|
+
readonly description: "Restrict search to pillar/chapter";
|
|
128
|
+
};
|
|
129
|
+
readonly facts: {
|
|
130
|
+
readonly type: "string";
|
|
131
|
+
readonly label: "--facts <ids>";
|
|
132
|
+
readonly description: "Comma-separated fact IDs to review";
|
|
133
|
+
};
|
|
134
|
+
readonly profile: {
|
|
135
|
+
readonly type: "string";
|
|
136
|
+
readonly label: "--profile <compact|full>";
|
|
137
|
+
readonly description: "MCP tool profile (default: compact)";
|
|
138
|
+
};
|
|
139
|
+
readonly 'standalone-reason': {
|
|
140
|
+
readonly type: "string";
|
|
141
|
+
readonly label: "--standalone-reason <text>";
|
|
142
|
+
readonly description: "Why an additional pillar owns an uncovered responsibility";
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
type Flag = keyof typeof options;
|
|
146
|
+
type Group = 'Everyday' | 'Navigation' | 'Agent workflows';
|
|
147
|
+
export type CommandSpec = {
|
|
148
|
+
name: string;
|
|
149
|
+
operation: string;
|
|
150
|
+
arguments: string;
|
|
151
|
+
description: string;
|
|
152
|
+
flags: Flag[];
|
|
153
|
+
example: string;
|
|
154
|
+
group: Group;
|
|
155
|
+
};
|
|
156
|
+
export declare const commands: CommandSpec[];
|
|
157
|
+
export declare const factExample: {
|
|
158
|
+
id: string;
|
|
159
|
+
statement: string;
|
|
160
|
+
evidence: {
|
|
161
|
+
path: string;
|
|
162
|
+
quote: string;
|
|
163
|
+
}[];
|
|
164
|
+
sourceScope: string[];
|
|
165
|
+
dependsOn: never[];
|
|
166
|
+
};
|
|
167
|
+
export declare const payloadExamples: Record<string, unknown>;
|
|
168
|
+
export type Values = Partial<Record<Flag, string | boolean>>;
|
|
169
|
+
export declare function parseCommand(argv: string[]): {
|
|
170
|
+
values: Partial<Record<"path" | "evidence" | "facts" | "chapter" | "preflight" | "limit" | "verify" | "verbose" | "cursor" | "review" | "skip-hook" | "both" | "exclude" | "stdin" | "example" | "all-results" | "dry-run" | "help" | "version" | "root" | "json" | "staged" | "cleanup" | "approve" | "signal" | "touched" | "profile" | "standalone-reason", string | boolean>>;
|
|
171
|
+
help: boolean;
|
|
172
|
+
command: undefined;
|
|
173
|
+
positionals: never[];
|
|
174
|
+
group: undefined;
|
|
175
|
+
} | {
|
|
176
|
+
values: Partial<Record<"path" | "evidence" | "facts" | "chapter" | "preflight" | "limit" | "verify" | "verbose" | "cursor" | "review" | "skip-hook" | "both" | "exclude" | "stdin" | "example" | "all-results" | "dry-run" | "help" | "version" | "root" | "json" | "staged" | "cleanup" | "approve" | "signal" | "touched" | "profile" | "standalone-reason", string | boolean>>;
|
|
177
|
+
help: boolean;
|
|
178
|
+
command: undefined;
|
|
179
|
+
positionals: never[];
|
|
180
|
+
group: string;
|
|
181
|
+
} | {
|
|
182
|
+
values: Partial<Record<"path" | "evidence" | "facts" | "chapter" | "preflight" | "limit" | "verify" | "verbose" | "cursor" | "review" | "skip-hook" | "both" | "exclude" | "stdin" | "example" | "all-results" | "dry-run" | "help" | "version" | "root" | "json" | "staged" | "cleanup" | "approve" | "signal" | "touched" | "profile" | "standalone-reason", string | boolean>>;
|
|
183
|
+
help: boolean;
|
|
184
|
+
command: CommandSpec;
|
|
185
|
+
positionals: string[];
|
|
186
|
+
group: undefined;
|
|
187
|
+
};
|
|
188
|
+
export declare function helpText(command?: CommandSpec, group?: string): string;
|
|
189
|
+
export {};
|