@descix/cli 1.0.1 → 1.0.3
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.md +671 -0
- package/README.md +43 -4
- package/agent-assets/guides/README.md +2 -2
- package/agent-assets/guides/dynamic-site-pattern.md +29 -14
- package/agent-assets/guides/knowledge-only-pattern.md +5 -5
- package/agent-assets/guides/microservice-pattern.md +48 -4
- package/agent-assets/guides/static-site-pattern.md +8 -0
- package/agent-assets/instructions/app-asset-validation.md +5 -5
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/instructions/standard-folder-structure.md +20 -11
- package/agent-assets/reference/app-creation.md +42 -20
- package/agent-assets/reference/architecture.md +24 -22
- package/agent-assets/reference/local-dev.md +230 -67
- package/agent-assets/reference/mcp-integration.md +9 -9
- package/agent-assets/reference/platform-api-consumer.md +244 -0
- package/agent-assets/reference/workspace-config.md +7 -7
- package/agent-assets/rules/descix_mcp.template.mdc +8 -8
- package/bin/descix.js +2559 -1435
- package/bin/kb-deny-lint.mjs +91 -0
- package/bin/mcp-server.js +175 -91
- package/lib/agent-files.js +37 -25
- package/lib/api-client.js +223 -56
- package/lib/auth-guard.js +13 -3
- package/lib/chat-session.js +187 -0
- package/lib/cli-version.js +59 -0
- package/lib/commands/airdrop.js +251 -0
- package/lib/commands/auth.js +254 -72
- package/lib/commands/briefer/errors.js +88 -0
- package/lib/commands/briefer/index.js +319 -0
- package/lib/commands/briefer/sources/canonical-sources.js +129 -0
- package/lib/commands/briefer/sources/entitlements.js +209 -0
- package/lib/commands/briefer/sources/environments.js +205 -0
- package/lib/commands/briefer/sources/identifiers.js +214 -0
- package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
- package/lib/commands/briefer/sources/routing.js +275 -0
- package/lib/commands/briefer/sources/what-is-not.js +223 -0
- package/lib/commands/briefer/stitcher.js +134 -0
- package/lib/commands/briefer/util/source-reader.js +502 -0
- package/lib/commands/config.js +210 -90
- package/lib/commands/corpus.js +978 -0
- package/lib/commands/credits.js +122 -0
- package/lib/commands/health.js +627 -0
- package/lib/commands/kb-list-render.js +32 -0
- package/lib/commands/kb.js +190 -413
- package/lib/commands/mcp.js +5 -5
- package/lib/commands/microservice-restart.js +359 -0
- package/lib/commands/model-config.js +313 -0
- package/lib/commands/retired-kb-sync.js +134 -0
- package/lib/commands/serve.js +26 -1
- package/lib/commands/status.js +23 -51
- package/lib/commands/update.js +36 -107
- package/lib/core/Chunker.js +491 -17
- package/lib/core/CorpusDenyLint.js +326 -0
- package/lib/core/CorpusWalker.js +357 -0
- package/lib/core/Hydrator.js +75 -29
- package/lib/core/ManifestLoader.js +533 -0
- package/lib/core/SiteWalker.js +218 -0
- package/lib/core/Syncer.js +163 -11
- package/lib/diagnostic-log.js +41 -0
- package/lib/environment-report.js +100 -0
- package/lib/global-config.js +4 -1
- package/lib/oauth-client.js +164 -0
- package/lib/origin.js +143 -0
- package/lib/service-api-client.js +95 -0
- package/lib/wallet-environment.js +49 -0
- package/lib/wallet-file.js +16 -2
- package/lib/wizard/setup.js +10 -9
- package/lib/workspace-config.js +380 -235
- package/lib/workspace-identity.js +92 -0
- package/lib/workspace-utils.js +41 -61
- package/package.json +19 -6
- package/templates/agent-claude.md +157 -9
- package/templates/agent-cline.md +68 -8
- package/templates/agent-copilot.md +68 -8
- package/templates/agent-cursor.md +68 -8
- package/templates/alignment-schema.json +20 -0
- package/templates/cloud-brain-hook.sh +20 -0
- package/templates/drive/README.md +1 -1
- package/templates/drive_templates/README.md +2 -2
- package/templates/scaffolds/README.md +6 -10
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +30 -0
- package/templates/scaffolds/microservice/app.js +51 -10
- package/templates/scaffolds/microservice/app.yaml +2 -2
- package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
- package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
- package/templates/scaffolds/microservice/manifest.json +0 -1
- package/templates/scaffolds/microservice/package.json +3 -3
- package/templates/scaffolds/microservice/scripts/register.js +56 -41
- package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
- package/templates/scaffolds/microservice/services/utils.js +20 -238
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
- package/lib/commands/app-wizard.js +0 -627
- package/lib/commands/folder.js +0 -291
- package/lib/core/PathContext.js +0 -335
- package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
- package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
- package/templates/scaffolds/site/README.md +0 -51
- package/templates/scaffolds/site/app.js +0 -22
- package/templates/scaffolds/site/index.html +0 -42
- package/templates/scaffolds/site/styles.css +0 -123
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CorpusDenyLint — the single canonical deny-class lint for Tier-P KB publishing.
|
|
3
|
+
*
|
|
4
|
+
* Ratified: CEO-D-2026-07-11-KB-CURATION-RATIFIED.
|
|
5
|
+
* Spec: docs/audit/kb-curation-2026-07-11/redteam-AUDIT.md §"PROPOSED DENY-LINT
|
|
6
|
+
* PATTERN SET" (15 path patterns + 4 content patterns + lint_exempt) and
|
|
7
|
+
* docs/design/kb-publishing-model-2026-07-11.md §4 (doc_class taxonomy).
|
|
8
|
+
*
|
|
9
|
+
* This module is the ONE definition of:
|
|
10
|
+
* - the publishable/deny doc_class sets (imported by ManifestLoader — do NOT re-list),
|
|
11
|
+
* - the deny path/content pattern set,
|
|
12
|
+
* - the lint that a Tier-P (`publish_tier:"P"`) manifest MUST pass before any
|
|
13
|
+
* walk/push (corpus.js runCorpusSync) and, via the git hook, before commit.
|
|
14
|
+
*
|
|
15
|
+
* The lint itself never throws — it returns a report. The CALLER decides to throw
|
|
16
|
+
* (assertNoViolations) so exemptions + warnings can be printed as the review
|
|
17
|
+
* artifact first (model §5.3). No hardcoded fallbacks; the manifest is the
|
|
18
|
+
* curation instrument.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as fs from 'fs/promises';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { createHash } from 'crypto';
|
|
24
|
+
import { minimatch } from 'minimatch';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Doc-class taxonomy (model §4). ONE canonical definition — ManifestLoader
|
|
28
|
+
* imports these; nothing re-enumerates the field list by hand.
|
|
29
|
+
*/
|
|
30
|
+
export const PUBLISHABLE_DOC_CLASSES = new Set([
|
|
31
|
+
'guide', 'primary-source', 'paper', 'proof', 'code', 'debate-record', 'patent'
|
|
32
|
+
]);
|
|
33
|
+
export const DENY_DOC_CLASSES = new Set([
|
|
34
|
+
'manuscript-draft', 'archive', 'internal-note'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* PATH patterns (hard deny — sync fails loud). redteam-AUDIT §"Path patterns".
|
|
39
|
+
* Each entry: { id, class, patterns:[glob...], caseFold?, excludeGlobs?, zeroByte?, reason }.
|
|
40
|
+
* `class` is the stable string a manifest source's `lint_exempt` names to exempt a match.
|
|
41
|
+
*/
|
|
42
|
+
export const DENY_PATH_PATTERNS = [
|
|
43
|
+
{ id: 1, class: 'archive', patterns: ['**/archive/**', '**/Archive/**'], reason: 'archive/graveyard directory — never a public surface' },
|
|
44
|
+
{ id: 2, class: 'lean-dev', patterns: ['**/Complexity/Dev/**'], reason: 'Lean non-load-bearing dev tree (explicitly non-load-bearing)' },
|
|
45
|
+
{ id: 3, class: 'draft-filename', patterns: ['*DRAFT*', '*Draft*'], caseFold: true, reason: 'draft in path/filename' },
|
|
46
|
+
{ id: 4, class: 'nature-draft', patterns: ['**/Nature_PeqNP/**'], reason: 'Nature_PeqNP draft lineage (v1/v2/v3/root) — deny the whole tree' },
|
|
47
|
+
{ id: 5, class: 'jsonl-working-log', patterns: ['*.jsonl'], reason: 'working log format (.jsonl) — exempt with lint_exempt:["jsonl-working-log"] when public-record-by-intent' },
|
|
48
|
+
{ id: 6, class: 'dot-claude', patterns: ['**/.claude/**'], reason: 'agent/internal .claude tree' },
|
|
49
|
+
{ id: 7, class: 'private', patterns: ['**/PRIVATE/**'], reason: 'PRIVATE directory' },
|
|
50
|
+
{ id: 8, class: 'handoff', patterns: ['**/HANDOFF*', '**/handoff*'], caseFold: true, reason: 'internal handoff document' },
|
|
51
|
+
{ id: 9, class: 'test-dump', patterns: ['**/*_TEST_RESULTS*', '**/test/**', '**/tests/**', '**/TheoremTests/**', '**/benchmarks/**'], excludeGlobs: ['*_README.md'], reason: 'raw test/benchmark suite dump or bloat (named *_README.md summaries excluded)' },
|
|
52
|
+
{ id: 10, class: 'internal-pm', patterns: ['**/session_transcript/**', '**/debug-*/**', '**/tasks/**'], reason: 'internal PM/debug/session material' },
|
|
53
|
+
{ id: 11, class: 'status-file', patterns: ['STATUS.md'], reason: 'internal workstream-status file (STATUS.md) — never a public surface' },
|
|
54
|
+
{ id: 12, class: 'investor-material', patterns: ['*Business_Plan*', '*Executive_Summary*', '*Market_Research*', '**/Faster Abadir Transform BP/**'], reason: 'investor/fundraising material — not public KB content' },
|
|
55
|
+
{ id: 13, class: 'patent-application', patterns: ['*Patent*Application*', '*Patent_Application*'], reason: 'patent application (unpublished filing)' },
|
|
56
|
+
{ id: 14, class: 'wip', patterns: ['**/PM/wip/**', '**/wip/**'], reason: 'work-in-progress notes carryover' },
|
|
57
|
+
{ id: 15, class: 'zero-byte', patterns: [], zeroByte: true, reason: 'zero-byte file (produces no content)' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* CONTENT patterns (soft-block / warn). redteam-AUDIT §"Content patterns".
|
|
62
|
+
* Each entry: { id, class, kind, ... , reason }.
|
|
63
|
+
* kind 'header' — first-10-lines regex; requires synced_from_edit (raw_path).
|
|
64
|
+
* kind 'body' — body regex(es); hard deny unless exempted.
|
|
65
|
+
* kind 'warn' — body regex; WARN only (collect, do not fail).
|
|
66
|
+
* kind 'license' — primary-source doc_class with empty/missing license_basis; hard deny.
|
|
67
|
+
*/
|
|
68
|
+
export const DENY_CONTENT_PATTERNS = [
|
|
69
|
+
{
|
|
70
|
+
id: 16, class: 'debate-context-header', kind: 'header',
|
|
71
|
+
headerRegex: /@\[debate_context\]|@\[debate_system\]/,
|
|
72
|
+
reason: '@[debate_context]/@[debate_system] header requires a reviewer-edited synced copy (set raw_path / synced_from_edit)'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 17, class: 'active-draft-marker', kind: 'body',
|
|
76
|
+
bodyRegexes: [/status:\s*"?draft/i, /ACTIVE DRAFTING/, /Project Status:\s*ACTIVE/],
|
|
77
|
+
reason: 'active-draft marker in body (status: draft / ACTIVE DRAFTING / Project Status: ACTIVE)'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 18, class: 'mailto-in-doc', kind: 'warn',
|
|
81
|
+
bodyRegex: /mailto:[\w.+-]+@[\w.-]+/,
|
|
82
|
+
reason: 'mailto: link embedded in a non-PDF paper/guide doc — reviewer accepts or strips'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 19, class: 'missing-license-basis', kind: 'license',
|
|
86
|
+
reason: 'primary-source doc_class with empty/missing license_basis'
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
// Matching helpers
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* True if `relPath` matches `glob` as a full path, as its basename, or as any
|
|
96
|
+
* single path segment (so segment-anywhere globs like `*DRAFT*` catch a middle
|
|
97
|
+
* directory such as `ReimannHypothesis Rota Draft/`).
|
|
98
|
+
*/
|
|
99
|
+
function pathMatchesGlob(relPath, glob, opts) {
|
|
100
|
+
if (minimatch(relPath, glob, opts)) return true;
|
|
101
|
+
const base = path.basename(relPath);
|
|
102
|
+
if (minimatch(base, glob, opts)) return true;
|
|
103
|
+
for (const seg of relPath.split('/')) {
|
|
104
|
+
if (seg && minimatch(seg, glob, opts)) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Test a resolved source path against a single DENY_PATH_PATTERN.
|
|
111
|
+
* Returns the matched glob string, or `false` (no match), or `null` (explicitly
|
|
112
|
+
* excluded, e.g. a `*_README.md` summary inside a tests/ tree).
|
|
113
|
+
*/
|
|
114
|
+
export function matchPathPattern(relPath, pattern) {
|
|
115
|
+
if (pattern.zeroByte) return false; // stat-based, handled by the lint, not a glob
|
|
116
|
+
if (pattern.excludeGlobs) {
|
|
117
|
+
const base = path.basename(relPath);
|
|
118
|
+
for (const ex of pattern.excludeGlobs) {
|
|
119
|
+
if (minimatch(base, ex, { dot: true, nocase: true })) return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const opts = { dot: true, nocase: !!pattern.caseFold };
|
|
123
|
+
for (const g of pattern.patterns) {
|
|
124
|
+
if (pathMatchesGlob(relPath, g, opts)) return g;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
// Report record builders (each carries a pre-rendered `line` for printing)
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function mkViolation(p, id, matched, reason) {
|
|
134
|
+
const pat = typeof id === 'number' ? `#${id}` : id;
|
|
135
|
+
const matchStr = matched ? ` [${matched}]` : '';
|
|
136
|
+
return {
|
|
137
|
+
path: p,
|
|
138
|
+
pattern: pat,
|
|
139
|
+
reason,
|
|
140
|
+
line: `VIOLATION: ${p} — pattern ${pat}: ${reason}${matchStr}`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mkExemption(p, patternId, cls, matched, exemptReason) {
|
|
145
|
+
const pat = `#${patternId}`;
|
|
146
|
+
const why = exemptReason || '(no exempt_reason given)';
|
|
147
|
+
return {
|
|
148
|
+
path: p,
|
|
149
|
+
pattern: pat,
|
|
150
|
+
class: cls,
|
|
151
|
+
reason: why,
|
|
152
|
+
line: `EXEMPTED: ${p} matched pattern ${pat} ${matched} — exempted for "${cls}": ${why}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function mkWarning(p, id, matched, reason) {
|
|
157
|
+
const pat = `#${id}`;
|
|
158
|
+
return {
|
|
159
|
+
path: p,
|
|
160
|
+
pattern: pat,
|
|
161
|
+
reason,
|
|
162
|
+
line: `WARNING: ${p} — pattern ${pat}: ${reason} ${matched}`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
// The lint
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Lint a resolved manifest for deny-class violations. Never throws — returns a
|
|
172
|
+
* report. The caller prints exemptions/warnings then calls assertNoViolations.
|
|
173
|
+
*
|
|
174
|
+
* A source may point at a file or a directory. Tier-P manifests are file-level
|
|
175
|
+
* allowlists (the common case): for a file source every check runs. For a dir
|
|
176
|
+
* source, only the source-entry-level checks run (path pattern on `source.path`,
|
|
177
|
+
* doc_class deny re-check, license #19, and the raw_path byte-hash) — content
|
|
178
|
+
* reads are skipped since a directory cannot be read as a document.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} manifest - Validated manifest with `_resolvedSources`.
|
|
181
|
+
* @param {string} workspaceRoot - Absolute workspace root (resolves raw_path).
|
|
182
|
+
* @returns {Promise<{violations:Array, exemptions:Array, warnings:Array}>}
|
|
183
|
+
*/
|
|
184
|
+
export async function lintManifestForDenyClasses(manifest, workspaceRoot) {
|
|
185
|
+
const report = { violations: [], exemptions: [], warnings: [] };
|
|
186
|
+
const sources = manifest?._resolvedSources || [];
|
|
187
|
+
|
|
188
|
+
for (const source of sources) {
|
|
189
|
+
const relPath = source.path;
|
|
190
|
+
const absPath = source.absolutePath || path.resolve(workspaceRoot, relPath);
|
|
191
|
+
const exemptSet = new Set(source.lint_exempt || []);
|
|
192
|
+
const exemptReason = source.exempt_reason || null;
|
|
193
|
+
|
|
194
|
+
let stat = null;
|
|
195
|
+
try { stat = await fs.stat(absPath); } catch { stat = null; }
|
|
196
|
+
const isFileSource = !!(stat && stat.isFile());
|
|
197
|
+
|
|
198
|
+
// (a) PATH patterns — evaluated on source.path (works for file, dir, missing).
|
|
199
|
+
for (const pattern of DENY_PATH_PATTERNS) {
|
|
200
|
+
const matched = matchPathPattern(relPath, pattern);
|
|
201
|
+
if (matched) {
|
|
202
|
+
if (exemptSet.has(pattern.class)) {
|
|
203
|
+
report.exemptions.push(mkExemption(relPath, pattern.id, pattern.class, matched, exemptReason));
|
|
204
|
+
} else {
|
|
205
|
+
report.violations.push(mkViolation(relPath, pattern.id, matched, pattern.reason));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// (#15) zero-byte — stat-based, only meaningful for a real file.
|
|
211
|
+
if (isFileSource && stat.size === 0) {
|
|
212
|
+
const zb = DENY_PATH_PATTERNS.find(p => p.class === 'zero-byte');
|
|
213
|
+
if (exemptSet.has('zero-byte')) {
|
|
214
|
+
report.exemptions.push(mkExemption(relPath, zb.id, zb.class, '(zero-byte file)', exemptReason));
|
|
215
|
+
} else {
|
|
216
|
+
report.violations.push(mkViolation(relPath, zb.id, '(zero-byte file)', zb.reason));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// (f) doc_class deny-class re-check (defense-in-depth; ManifestLoader also blocks).
|
|
221
|
+
if (source.doc_class && DENY_DOC_CLASSES.has(source.doc_class)) {
|
|
222
|
+
report.violations.push(mkViolation(
|
|
223
|
+
relPath, 'doc_class', source.doc_class,
|
|
224
|
+
`doc_class "${source.doc_class}" is a deny-class — structurally invalid in a Tier-P manifest`
|
|
225
|
+
));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// (g) content #19 — primary-source must carry a license_basis (source-level).
|
|
229
|
+
if (source.doc_class === 'primary-source' && !source.license_basis) {
|
|
230
|
+
report.violations.push(mkViolation(
|
|
231
|
+
relPath, 19, 'primary-source',
|
|
232
|
+
DENY_CONTENT_PATTERNS.find(p => p.id === 19).reason
|
|
233
|
+
));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// (c) synced_from_edit byte-hash — whenever raw_path is set (file OR dir source).
|
|
237
|
+
if (source.raw_path) {
|
|
238
|
+
const rawAbs = path.resolve(workspaceRoot, source.raw_path);
|
|
239
|
+
let rawBytes = null;
|
|
240
|
+
try { rawBytes = await fs.readFile(rawAbs); } catch { rawBytes = null; }
|
|
241
|
+
if (rawBytes === null) {
|
|
242
|
+
report.violations.push(mkViolation(
|
|
243
|
+
relPath, 'synced_from_edit', source.raw_path,
|
|
244
|
+
`raw_path "${source.raw_path}" is missing on disk`
|
|
245
|
+
));
|
|
246
|
+
} else if (isFileSource) {
|
|
247
|
+
let editBytes = null;
|
|
248
|
+
try { editBytes = await fs.readFile(absPath); } catch { editBytes = null; }
|
|
249
|
+
if (editBytes !== null) {
|
|
250
|
+
const rawHash = createHash('sha256').update(rawBytes).digest('hex');
|
|
251
|
+
const editHash = createHash('sha256').update(editBytes).digest('hex');
|
|
252
|
+
if (rawHash === editHash) {
|
|
253
|
+
report.violations.push(mkViolation(
|
|
254
|
+
relPath, 16, source.raw_path,
|
|
255
|
+
'edited copy byte-identical to raw source — the edit was not applied'
|
|
256
|
+
));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Content-body checks require reading the file (file sources only).
|
|
263
|
+
if (isFileSource) {
|
|
264
|
+
let content = null;
|
|
265
|
+
try { content = await fs.readFile(absPath, 'utf-8'); } catch { content = null; }
|
|
266
|
+
if (content !== null) {
|
|
267
|
+
// (b) content #16 — debate-context header in first 10 lines requires raw_path.
|
|
268
|
+
const p16 = DENY_CONTENT_PATTERNS.find(p => p.id === 16);
|
|
269
|
+
const first10 = content.split('\n').slice(0, 10).join('\n');
|
|
270
|
+
if (p16.headerRegex.test(first10) && !source.raw_path) {
|
|
271
|
+
report.violations.push(mkViolation(
|
|
272
|
+
relPath, 16, '@[debate_context]/@[debate_system]', p16.reason
|
|
273
|
+
));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// (d) content #17 — active-draft marker in body → hard deny unless exempted.
|
|
277
|
+
const p17 = DENY_CONTENT_PATTERNS.find(p => p.id === 17);
|
|
278
|
+
if (p17.bodyRegexes.some(re => re.test(content))) {
|
|
279
|
+
if (exemptSet.has(p17.class)) {
|
|
280
|
+
report.exemptions.push(mkExemption(relPath, p17.id, p17.class, '(active-draft marker)', exemptReason));
|
|
281
|
+
} else {
|
|
282
|
+
report.violations.push(mkViolation(relPath, p17.id, '(active-draft marker)', p17.reason));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// (e) content #18 — mailto: in a non-PDF paper/guide doc → WARN only.
|
|
287
|
+
const p18 = DENY_CONTENT_PATTERNS.find(p => p.id === 18);
|
|
288
|
+
const dc = source.doc_class;
|
|
289
|
+
const isPaperOrGuide = dc === 'paper' || dc === 'guide';
|
|
290
|
+
const isPdf = relPath.toLowerCase().endsWith('.pdf');
|
|
291
|
+
if (isPaperOrGuide && !isPdf && p18.bodyRegex.test(content)) {
|
|
292
|
+
report.warnings.push(mkWarning(relPath, p18.id, '(mailto)', p18.reason));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return report;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Throw a single aggregated error listing every violation, or return quietly if
|
|
303
|
+
* the report is clean. The caller (corpus.js / git hook) uses this to fail loud
|
|
304
|
+
* AFTER printing exemptions + warnings.
|
|
305
|
+
*
|
|
306
|
+
* @param {{violations:Array}} report
|
|
307
|
+
*/
|
|
308
|
+
export function assertNoViolations(report) {
|
|
309
|
+
const violations = report?.violations || [];
|
|
310
|
+
if (violations.length === 0) return;
|
|
311
|
+
const lines = violations.map(v => ` - ${v.line || `${v.path}: ${v.reason}`}`);
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Tier-P deny-class lint FAILED: ${violations.length} violation(s) — refusing to sync/commit.\n` +
|
|
314
|
+
lines.join('\n')
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export default {
|
|
319
|
+
PUBLISHABLE_DOC_CLASSES,
|
|
320
|
+
DENY_DOC_CLASSES,
|
|
321
|
+
DENY_PATH_PATTERNS,
|
|
322
|
+
DENY_CONTENT_PATTERNS,
|
|
323
|
+
matchPathPattern,
|
|
324
|
+
lintManifestForDenyClasses,
|
|
325
|
+
assertNoViolations,
|
|
326
|
+
};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CorpusWalker - Git-aware file walker for corpus sync
|
|
3
|
+
*
|
|
4
|
+
* Given a manifest, walks source directories, applies syncignore globs,
|
|
5
|
+
* and computes git blob SHAs for content-addressed chunk IDs.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Files stay in place — no copying or staging
|
|
9
|
+
* - Git blob SHAs provide versioning (same content = same SHA)
|
|
10
|
+
* - Untracked/modified files use `git hash-object` for working tree version
|
|
11
|
+
* - Chunk IDs are content-addressed: {app_id}:{kb_name}:{blob_sha}:{chunk_idx}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from 'fs/promises';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import { minimatch } from 'minimatch';
|
|
18
|
+
// The source-ref default is owned by ManifestLoader — consume it, never re-default.
|
|
19
|
+
import { DEFAULT_SOURCE_REF } from './ManifestLoader.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* File extensions that are processable for RAG chunking.
|
|
23
|
+
* Binary files, images, fonts, compiled assets, etc. are excluded.
|
|
24
|
+
*/
|
|
25
|
+
const PROCESSABLE_EXTENSIONS = new Set([
|
|
26
|
+
'.md', '.txt', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
27
|
+
'.py', '.rs', '.go', '.sol', '.lean', '.json', '.jsonl',
|
|
28
|
+
'.yaml', '.yml', '.toml', '.csv', '.tex', '.sh', '.bash'
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a file has a processable text extension.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} filePath - File path
|
|
35
|
+
* @returns {boolean} True if the file can be chunked for RAG
|
|
36
|
+
*/
|
|
37
|
+
function isProcessableFile(filePath) {
|
|
38
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
39
|
+
return PROCESSABLE_EXTENSIONS.has(ext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a file path matches any of the syncignore patterns.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} relativePath - Path relative to the source entry root
|
|
46
|
+
* @param {string[]} syncignore - Array of glob patterns to exclude
|
|
47
|
+
* @returns {boolean} True if the file should be ignored
|
|
48
|
+
*/
|
|
49
|
+
function isIgnored(relativePath, syncignore) {
|
|
50
|
+
for (const pattern of syncignore) {
|
|
51
|
+
if (minimatch(relativePath, pattern, { dot: true })) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// Also check just the filename for patterns like "*.test.js"
|
|
55
|
+
const basename = path.basename(relativePath);
|
|
56
|
+
if (minimatch(basename, pattern, { dot: true })) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compute the git blob SHA for a file.
|
|
65
|
+
* First tries `git rev-parse {ref}:{workspace-relative-path}` for tracked files.
|
|
66
|
+
* Falls back to `git hash-object {file}` for untracked/modified files.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} absolutePath - Absolute path to the file
|
|
69
|
+
* @param {string} workspaceRelativePath - Path relative to workspace root
|
|
70
|
+
* @param {string} ref - Git ref (branch/tag)
|
|
71
|
+
* @param {string} workspaceRoot - Workspace root directory
|
|
72
|
+
* @returns {string} Git blob SHA (40 hex chars)
|
|
73
|
+
*/
|
|
74
|
+
function computeBlobSha(absolutePath, workspaceRelativePath, ref, workspaceRoot) {
|
|
75
|
+
try {
|
|
76
|
+
// Try to get the blob SHA from the git tree at the specified ref
|
|
77
|
+
const sha = execSync(
|
|
78
|
+
`git rev-parse "${ref}:${workspaceRelativePath}"`,
|
|
79
|
+
{ cwd: workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
80
|
+
).trim();
|
|
81
|
+
return sha;
|
|
82
|
+
} catch {
|
|
83
|
+
// File is untracked or path doesn't exist in git tree at that ref
|
|
84
|
+
// Fall back to hashing the working tree version
|
|
85
|
+
try {
|
|
86
|
+
const sha = execSync(
|
|
87
|
+
`git hash-object "${absolutePath}"`,
|
|
88
|
+
{ cwd: workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
89
|
+
).trim();
|
|
90
|
+
return sha;
|
|
91
|
+
} catch {
|
|
92
|
+
// Last resort: return null — caller should skip this file
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SHA40 = /^[0-9a-f]{40}$/i;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve ONE source's provenance: which repository, which ref, and the exact commit synced.
|
|
102
|
+
*
|
|
103
|
+
* This is the single owner of "where did this content come from". `ref` may be a mutable branch,
|
|
104
|
+
* so the RESOLVED COMMIT SHA is what sync-state records — provenance stays exact after the branch
|
|
105
|
+
* moves. It FAILS LOUD rather than recording a placeholder: a source whose commit cannot be
|
|
106
|
+
* resolved has no honest provenance, and `"unknown"` in sync-state is a false record, not a gap.
|
|
107
|
+
*
|
|
108
|
+
* A CROSS-REPO source (`repo` set) is resolved against the REMOTE. It never falls back to a local
|
|
109
|
+
* read — that fallback is precisely the adjacency defect this contract exists to kill, and it would
|
|
110
|
+
* stamp a foreign repo's content with this repo's commit.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} source - a `_resolvedSources` entry from ManifestLoader
|
|
113
|
+
* @param {string} workspaceRoot - workspace root (the in-repo resolution context)
|
|
114
|
+
* @returns {Promise<{repo: string|null, ref: string, resolved_commit_sha: string, source_repo: string}>}
|
|
115
|
+
* @throws {Error} naming the repo/ref that could not be resolved
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveSourceProvenance(source, workspaceRoot) {
|
|
118
|
+
const ref = source.ref || DEFAULT_SOURCE_REF;
|
|
119
|
+
const repo = source.repo ?? null;
|
|
120
|
+
// ONE derivation of "which repo this content belongs to": the explicit slug when the source is
|
|
121
|
+
// cross-repo, else the leading path segment (the historical in-repo value, preserved verbatim so
|
|
122
|
+
// existing chunk metadata does not churn).
|
|
123
|
+
const source_repo = repo ?? (String(source.path || '').split('/')[0] || 'unknown');
|
|
124
|
+
|
|
125
|
+
if (!repo) {
|
|
126
|
+
let sha;
|
|
127
|
+
try {
|
|
128
|
+
sha = execSync(`git rev-parse "${ref}^{commit}"`, {
|
|
129
|
+
cwd: workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
130
|
+
}).trim();
|
|
131
|
+
} catch {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Cannot resolve ref "${ref}" to a commit in ${workspaceRoot} for source "${source.path}". ` +
|
|
134
|
+
`Refusing to record provenance — a sync-state commit of "unknown" is a FALSE record, not a ` +
|
|
135
|
+
`missing one. Check that the ref exists locally.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!SHA40.test(sha)) {
|
|
139
|
+
throw new Error(`Ref "${ref}" resolved to "${sha}", which is not a commit sha (source "${source.path}").`);
|
|
140
|
+
}
|
|
141
|
+
return { repo: null, ref, resolved_commit_sha: sha, source_repo };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// CROSS-REPO: resolve against the remote. Never a local read.
|
|
145
|
+
if (SHA40.test(ref)) {
|
|
146
|
+
return { repo, ref, resolved_commit_sha: ref.toLowerCase(), source_repo };
|
|
147
|
+
}
|
|
148
|
+
const remoteUrl = `https://github.com/${repo}.git`;
|
|
149
|
+
let out;
|
|
150
|
+
try {
|
|
151
|
+
out = execSync(`git ls-remote "${remoteUrl}" "${ref}" "refs/heads/${ref}" "refs/tags/${ref}"`, {
|
|
152
|
+
cwd: workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
153
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
154
|
+
}).trim();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Cannot fetch cross-repo source "${source.path}" from repo "${repo}" at ref "${ref}": ` +
|
|
158
|
+
`${String(err.stderr || err.message).trim()}. REFUSING — a repo that cannot be fetched is ` +
|
|
159
|
+
`never silently skipped and never falls back to a local/adjacent working tree. Ensure git ` +
|
|
160
|
+
`credentials for ${remoteUrl} are available to this machine.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const line = out.split('\n').map(l => l.trim()).filter(Boolean)[0];
|
|
164
|
+
const sha = line ? line.split(/\s+/)[0] : null;
|
|
165
|
+
if (!sha || !SHA40.test(sha)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Repo "${repo}" has no ref "${ref}" (source "${source.path}"). REFUSING rather than guessing.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return { repo, ref, resolved_commit_sha: sha, source_repo };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Recursively walk a directory and collect file paths.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} dir - Directory to walk
|
|
177
|
+
* @param {string} baseDir - Base directory for computing relative paths
|
|
178
|
+
* @param {string[]} syncignore - Glob patterns to exclude
|
|
179
|
+
* @returns {Promise<string[]>} Array of absolute file paths
|
|
180
|
+
*/
|
|
181
|
+
async function walkDir(dir, baseDir, syncignore) {
|
|
182
|
+
const results = [];
|
|
183
|
+
|
|
184
|
+
let entries;
|
|
185
|
+
try {
|
|
186
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
187
|
+
} catch {
|
|
188
|
+
return results; // Directory doesn't exist or unreadable
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const fullPath = path.join(dir, entry.name);
|
|
193
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
194
|
+
|
|
195
|
+
if (isIgnored(relativePath, syncignore)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
// Check directory-level ignore (patterns ending with /)
|
|
201
|
+
const dirRelative = relativePath + '/';
|
|
202
|
+
if (isIgnored(dirRelative, syncignore)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const subFiles = await walkDir(fullPath, baseDir, syncignore);
|
|
206
|
+
results.push(...subFiles);
|
|
207
|
+
} else if (entry.isFile()) {
|
|
208
|
+
// Only include text-processable files
|
|
209
|
+
if (isProcessableFile(fullPath)) {
|
|
210
|
+
results.push(fullPath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if a path is a file (not a directory).
|
|
220
|
+
*
|
|
221
|
+
* @param {string} filePath - Path to check
|
|
222
|
+
* @returns {Promise<boolean>}
|
|
223
|
+
*/
|
|
224
|
+
async function isFile(filePath) {
|
|
225
|
+
try {
|
|
226
|
+
const stat = await fs.stat(filePath);
|
|
227
|
+
return stat.isFile();
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Walk a manifest and return file entries with git metadata.
|
|
235
|
+
*
|
|
236
|
+
* For each source entry in the manifest:
|
|
237
|
+
* 1. Resolve the local directory path (workspace-relative)
|
|
238
|
+
* 2. Walk the directory, applying syncignore globs
|
|
239
|
+
* 3. Compute git blob SHA for each file
|
|
240
|
+
* 4. Return file list with metadata
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} manifest - Validated manifest from ManifestLoader (with _resolvedSources)
|
|
243
|
+
* @param {string} workspaceRoot - Absolute workspace root path
|
|
244
|
+
* @returns {Promise<Object>} { files: Array<FileEntry>, commitSha: string,
|
|
245
|
+
* provenance: Array<{path, repo, ref, resolved_commit_sha, source_repo}> }
|
|
246
|
+
*/
|
|
247
|
+
export async function walkCorpus(manifest, workspaceRoot) {
|
|
248
|
+
const files = [];
|
|
249
|
+
const provenance = [];
|
|
250
|
+
let commitSha = 'unknown';
|
|
251
|
+
|
|
252
|
+
// Nested-source dedup (WS-EVP-DESCIX-KB-CLEANUP item 4): a manifest may list both a directory
|
|
253
|
+
// source AND a more-specific source nested inside it (e.g. `.../services` at tier 3 AND
|
|
254
|
+
// `.../services/commandHandlers` at tier 2 to override the tier for that subtree). Without dedup
|
|
255
|
+
// the walker collects the nested files TWICE — once under each source — producing duplicate
|
|
256
|
+
// content-addressed chunk IDs and a deterministic upsert-vs-live dedup delta every full sync
|
|
257
|
+
// (the observed −175 on unk-beast/Corpus). Fix at the walker level so the class is killed for
|
|
258
|
+
// every manifest: each physical file belongs to EXACTLY ONE source — the DEEPEST (most-specific)
|
|
259
|
+
// source root that contains it. Walking an ancestor source skips a file owned by a descendant
|
|
260
|
+
// source, preserving the author's override intent (the nested source's tier/doc_type/syncignore
|
|
261
|
+
// win for its subtree). Order-independent and deterministic; a single-file source root === the
|
|
262
|
+
// file path, so an explicit file source always out-specifies any enclosing directory source.
|
|
263
|
+
const sourceRoots = manifest._resolvedSources.map(s => path.resolve(s.absolutePath));
|
|
264
|
+
const ownerSourceRoot = (fileAbs) => {
|
|
265
|
+
const fileResolved = path.resolve(fileAbs);
|
|
266
|
+
let deepest = null;
|
|
267
|
+
for (const root of sourceRoots) {
|
|
268
|
+
if (fileResolved === root || fileResolved.startsWith(root + path.sep)) {
|
|
269
|
+
if (deepest === null || root.length > deepest.length) deepest = root;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return deepest;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
for (const source of manifest._resolvedSources) {
|
|
276
|
+
const { absolutePath, ref, tier, doc_type, syncignore } = source;
|
|
277
|
+
const thisSourceRoot = path.resolve(absolutePath);
|
|
278
|
+
|
|
279
|
+
// Resolve this source's exact provenance BEFORE reading anything. A cross-repo source that
|
|
280
|
+
// cannot be fetched REFUSES here, naming the repo — it is never silently skipped and never
|
|
281
|
+
// falls back to reading an adjacent working tree.
|
|
282
|
+
const prov = await resolveSourceProvenance(source, workspaceRoot);
|
|
283
|
+
provenance.push({ path: source.path, ...prov });
|
|
284
|
+
|
|
285
|
+
// A cross-repo source's CONTENT still lives in another repository. Reading `absolutePath`
|
|
286
|
+
// would resolve inside THIS workspace — the exact adjacency defect this contract kills — so
|
|
287
|
+
// refuse loudly rather than walk the wrong tree and stamp it with a foreign repo's sha.
|
|
288
|
+
if (prov.repo) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Cross-repo source "${source.path}" (repo "${prov.repo}", ref "${ref}" @ ` +
|
|
291
|
+
`${prov.resolved_commit_sha.substring(0, 8)}) cannot be walked: cross-repo CONTENT FETCH is ` +
|
|
292
|
+
`not implemented in this CLI. Refusing rather than reading the local working tree at ` +
|
|
293
|
+
`${absolutePath}, which would attribute this workspace's files to "${prov.repo}".`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The sync's headline commit is the FIRST source's resolved commit — read from the one owner
|
|
298
|
+
// (resolveSourceProvenance) rather than re-deriving it, so the headline and the per-source
|
|
299
|
+
// provenance records can never disagree.
|
|
300
|
+
if (commitSha === 'unknown') {
|
|
301
|
+
commitSha = prov.resolved_commit_sha;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check if source path is a file or directory
|
|
305
|
+
const sourceIsFile = await isFile(absolutePath);
|
|
306
|
+
|
|
307
|
+
let filePaths;
|
|
308
|
+
if (sourceIsFile) {
|
|
309
|
+
// Single file source entry — check if processable
|
|
310
|
+
filePaths = isProcessableFile(absolutePath) ? [absolutePath] : [];
|
|
311
|
+
} else {
|
|
312
|
+
// Directory: walk recursively with syncignore
|
|
313
|
+
filePaths = await walkDir(absolutePath, absolutePath, syncignore);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const filePath of filePaths) {
|
|
317
|
+
// Nested-source dedup: skip any file owned by a MORE-SPECIFIC (deeper) source root.
|
|
318
|
+
if (ownerSourceRoot(filePath) !== thisSourceRoot) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
// Compute workspace-relative path for git operations
|
|
322
|
+
const workspaceRelativePath = path.relative(workspaceRoot, filePath);
|
|
323
|
+
|
|
324
|
+
// Compute blob SHA
|
|
325
|
+
const blobSha = computeBlobSha(filePath, workspaceRelativePath, ref, workspaceRoot);
|
|
326
|
+
if (!blobSha) {
|
|
327
|
+
continue; // Skip files that can't be hashed
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// "Which repo does this content belong to" has ONE owner: resolveSourceProvenance.
|
|
331
|
+
const sourceRepo = prov.source_repo;
|
|
332
|
+
|
|
333
|
+
files.push({
|
|
334
|
+
absolute_path: filePath,
|
|
335
|
+
relative_path: workspaceRelativePath,
|
|
336
|
+
blob_sha: blobSha,
|
|
337
|
+
source_entry: {
|
|
338
|
+
path: source.path,
|
|
339
|
+
ref,
|
|
340
|
+
tier,
|
|
341
|
+
doc_type,
|
|
342
|
+
syncignore,
|
|
343
|
+
// KB-curation metadata (rides into the chunk record → Pinecone).
|
|
344
|
+
doc_class: source.doc_class,
|
|
345
|
+
license_basis: source.license_basis ?? null,
|
|
346
|
+
raw_path: source.raw_path ?? null,
|
|
347
|
+
synced_from_edit: (source.raw_path ?? null) != null
|
|
348
|
+
},
|
|
349
|
+
source_repo: sourceRepo
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { files, commitSha, provenance };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export default { walkCorpus, resolveSourceProvenance };
|