@descix/cli 1.0.7 → 1.0.9
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/README.md +1 -1
- package/agent-assets/guides/README.md +1 -1
- package/agent-assets/guides/dynamic-site-pattern.md +7 -16
- package/agent-assets/instructions/app-asset-validation.md +1 -1
- package/agent-assets/reference/architecture.md +20 -25
- package/agent-assets/reference/local-dev.md +1 -6
- package/agent-assets/reference/mcp-integration.md +15 -9
- package/agent-assets/reference/platform-api-consumer.md +11 -5
- package/agent-assets/reference/workspace-config.md +2 -2
- package/bin/descix.js +211 -688
- package/lib/command-visibility.js +213 -0
- package/lib/commands/corpus.js +2 -5
- package/lib/commands/kb.js +2 -2
- package/lib/commands/media-upload.js +176 -0
- package/lib/commands/retired-kb-sync.js +5 -27
- package/lib/commands/status.js +0 -9
- package/lib/output.js +31 -0
- package/lib/sdk-assets.js +28 -0
- package/lib/verb-invokes.generated.json +233 -0
- package/lib/workspace-config.js +9 -44
- package/package.json +6 -2
- package/templates/agent-claude.md +1 -3
- package/templates/agent-cline.md +1 -2
- package/templates/agent-copilot.md +1 -2
- package/templates/agent-cursor.md +1 -2
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
- package/agent-assets/rules/descix_mcp.template.mdc +0 -243
- package/lib/commands/briefer/errors.js +0 -88
- package/lib/commands/briefer/index.js +0 -322
- package/lib/commands/briefer/sources/canonical-sources.js +0 -129
- package/lib/commands/briefer/sources/entitlements.js +0 -220
- package/lib/commands/briefer/sources/environments.js +0 -205
- package/lib/commands/briefer/sources/identifiers.js +0 -214
- package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
- package/lib/commands/briefer/sources/routing.js +0 -275
- package/lib/commands/briefer/sources/what-is-not.js +0 -223
- package/lib/commands/briefer/stitcher.js +0 -134
- package/lib/commands/briefer/util/source-reader.js +0 -502
- package/lib/commands/mcp.js +0 -273
- package/lib/commands/update.js +0 -581
- package/lib/wizard/setup.js +0 -585
- package/templates/descix_mcp.mdc +0 -122
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* source-reader.js — shared helpers for extractors that read source files,
|
|
3
|
-
* compute citation SHAs, and verify regex / grep results.
|
|
4
|
-
*
|
|
5
|
-
* Per WS-DESCIX-BRIEFER-CLI M2 design (regex-with-checksum, not AST):
|
|
6
|
-
* - Read a file from disk.
|
|
7
|
-
* - Verify a regex matches in the expected line range.
|
|
8
|
-
* - Compute a SHA-256 (first 12 hex chars) of the cited line range.
|
|
9
|
-
* - HARD-FAIL via BrieferExtractorError if the file is missing or the
|
|
10
|
-
* regex doesn't match — never fall back to a default string.
|
|
11
|
-
*
|
|
12
|
-
* Why regex+SHA over AST (decided 2026-05-26):
|
|
13
|
-
* - No new npm dep (avoids @babel/parser, ~3MB of churn).
|
|
14
|
-
* - All M2 extractions are simple literals, regex-friendly constructs,
|
|
15
|
-
* or grep-style negative-claim checks.
|
|
16
|
-
* - SHA-of-line-range captures "construct moved or changed shape" with
|
|
17
|
-
* the same fidelity AST would give for these targets.
|
|
18
|
-
* - AST can be added later as a per-extractor opt-in if a target needs it.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import fs from 'fs/promises';
|
|
22
|
-
import path from 'path';
|
|
23
|
-
import crypto from 'crypto';
|
|
24
|
-
import { execFileSync, execFile } from 'child_process';
|
|
25
|
-
import https from 'https';
|
|
26
|
-
import { promisify } from 'util';
|
|
27
|
-
|
|
28
|
-
const execFileP = promisify(execFile);
|
|
29
|
-
import { BrieferExtractorError, BRIEFER_ERROR_CODES } from '../errors.js';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolve an absolute path inside the repo root for an extractor target.
|
|
33
|
-
*
|
|
34
|
-
* @param {object} cliPaths { repoRoot, ... } from index.js::resolveCliPaths()
|
|
35
|
-
* @param {string} relPath Repo-root-relative path (e.g., "DeSciX/DeSciX_Cloud/.../provision-platform-lb.js")
|
|
36
|
-
*/
|
|
37
|
-
export function resolveRepoPath(cliPaths, relPath) {
|
|
38
|
-
if (!cliPaths || !cliPaths.repoRoot) {
|
|
39
|
-
throw new Error('resolveRepoPath: cliPaths.repoRoot is required');
|
|
40
|
-
}
|
|
41
|
-
return path.join(cliPaths.repoRoot, relPath);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Read a source file, HARD-FAIL with BRIEFER-SRC-NOT-FOUND if missing.
|
|
46
|
-
*
|
|
47
|
-
* @returns {Promise<{absPath: string, lines: string[], raw: string}>}
|
|
48
|
-
*/
|
|
49
|
-
export async function readSourceFile({ cliPaths, relPath, section }) {
|
|
50
|
-
const absPath = resolveRepoPath(cliPaths, relPath);
|
|
51
|
-
let raw;
|
|
52
|
-
try {
|
|
53
|
-
raw = await fs.readFile(absPath, 'utf-8');
|
|
54
|
-
} catch (err) {
|
|
55
|
-
if (err.code === 'ENOENT') {
|
|
56
|
-
throw new BrieferExtractorError({
|
|
57
|
-
code: BRIEFER_ERROR_CODES.SRC_NOT_FOUND,
|
|
58
|
-
section,
|
|
59
|
-
source: relPath,
|
|
60
|
-
expected: `file present at ${absPath}`,
|
|
61
|
-
recovery: `Restore the file at ${relPath} or update the extractor's relPath if the file was intentionally moved.`,
|
|
62
|
-
detail: err.message
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
throw err;
|
|
66
|
-
}
|
|
67
|
-
return { absPath, lines: raw.split(/\r?\n/), raw };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* SHA-256 (first 12 hex chars) of a string. Used for citation SHA stamps.
|
|
72
|
-
*/
|
|
73
|
-
export function sha12(text) {
|
|
74
|
-
return crypto.createHash('sha256').update(text, 'utf-8').digest('hex').slice(0, 12);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Compute SHA-12 of a line range from a file's split lines array.
|
|
79
|
-
* Line numbers are 1-indexed inclusive (matches how lines are quoted in citations).
|
|
80
|
-
*
|
|
81
|
-
* @param {string[]} lines file split by /\r?\n/
|
|
82
|
-
* @param {number} start 1-indexed start line (inclusive)
|
|
83
|
-
* @param {number} end 1-indexed end line (inclusive)
|
|
84
|
-
*/
|
|
85
|
-
export function sha12OfRange(lines, start, end) {
|
|
86
|
-
if (start < 1) start = 1;
|
|
87
|
-
if (end > lines.length) end = lines.length;
|
|
88
|
-
// Re-join with \n — we hash the canonical newline form, not the on-disk form.
|
|
89
|
-
return sha12(lines.slice(start - 1, end).join('\n'));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Find a regex in a file and verify it lands inside an expected line range.
|
|
94
|
-
*
|
|
95
|
-
* Returns { lineNumber, match, range, sha } if found AND inside range.
|
|
96
|
-
* HARD-FAILS with BRIEFER-SRC-NOT-FOUND if the regex doesn't match at all.
|
|
97
|
-
* HARD-FAILS with BRIEFER-PARSE-FAIL if it matches outside the expected range.
|
|
98
|
-
*
|
|
99
|
-
* @param {object} args
|
|
100
|
-
* @param {string[]} args.lines file split by /\r?\n/
|
|
101
|
-
* @param {RegExp} args.regex
|
|
102
|
-
* @param {string} args.section "§3 routing" etc.
|
|
103
|
-
* @param {string} args.source "provision-platform-lb.js:120" etc.
|
|
104
|
-
* @param {string} args.expected Description of the construct shape expected
|
|
105
|
-
* @param {string} args.recovery What the operator should do
|
|
106
|
-
* @param {[number,number]} [args.expectedRange] 1-indexed inclusive expected line range
|
|
107
|
-
*/
|
|
108
|
-
export function findInLines({ lines, regex, section, source, expected, recovery, expectedRange = null }) {
|
|
109
|
-
for (let i = 0; i < lines.length; i++) {
|
|
110
|
-
const m = lines[i].match(regex);
|
|
111
|
-
if (m) {
|
|
112
|
-
const lineNumber = i + 1;
|
|
113
|
-
if (expectedRange) {
|
|
114
|
-
const [lo, hi] = expectedRange;
|
|
115
|
-
if (lineNumber < lo || lineNumber > hi) {
|
|
116
|
-
throw new BrieferExtractorError({
|
|
117
|
-
code: BRIEFER_ERROR_CODES.PARSE_FAIL,
|
|
118
|
-
section,
|
|
119
|
-
source,
|
|
120
|
-
expected: `${expected} inside lines ${lo}-${hi}`,
|
|
121
|
-
recovery,
|
|
122
|
-
detail: `Regex matched at line ${lineNumber}, outside expected range.`
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return { lineNumber, match: m, line: lines[i] };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
throw new BrieferExtractorError({
|
|
130
|
-
code: BRIEFER_ERROR_CODES.SRC_NOT_FOUND,
|
|
131
|
-
section,
|
|
132
|
-
source,
|
|
133
|
-
expected,
|
|
134
|
-
recovery,
|
|
135
|
-
detail: `Regex ${regex} did not match any line.`
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Render a citation object with computed SHA for a known line range.
|
|
141
|
-
*
|
|
142
|
-
* @param {object} args
|
|
143
|
-
* @param {string} args.file Repo-relative path
|
|
144
|
-
* @param {string} args.lines "82-202" or "120"
|
|
145
|
-
* @param {string} args.anchor Short human label
|
|
146
|
-
* @param {string[]} args.fileLines The file's split-lines array
|
|
147
|
-
* @returns {{file, lines, sha, anchor}}
|
|
148
|
-
*/
|
|
149
|
-
export function makeCitation({ file, lines: lineSpec, anchor, fileLines }) {
|
|
150
|
-
const [startStr, endStr] = lineSpec.includes('-') ? lineSpec.split('-') : [lineSpec, lineSpec];
|
|
151
|
-
const start = parseInt(startStr, 10);
|
|
152
|
-
const end = parseInt(endStr, 10);
|
|
153
|
-
const sha = sha12OfRange(fileLines, start, end);
|
|
154
|
-
return { file, lines: lineSpec, anchor, sha };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Slice a line range from a file's split-lines array into a verbatim block.
|
|
159
|
-
* 1-indexed inclusive.
|
|
160
|
-
*/
|
|
161
|
-
export function sliceRange(lines, start, end) {
|
|
162
|
-
if (start < 1) start = 1;
|
|
163
|
-
if (end > lines.length) end = lines.length;
|
|
164
|
-
return lines.slice(start - 1, end).join('\n');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Run a grep-style search across multiple files. Returns array of
|
|
169
|
-
* { file, lineNumber, line } for every match. Uses Node fs (no shell).
|
|
170
|
-
*
|
|
171
|
-
* Per scope §6 "Negative-claim grep gates" — if matches.length > 0 the caller
|
|
172
|
-
* is expected to throw BRIEFER-NEGATIVE-CLAIM.
|
|
173
|
-
*
|
|
174
|
-
* @param {object} args
|
|
175
|
-
* @param {string} args.repoRoot
|
|
176
|
-
* @param {string[]} args.relPaths files OR directories (dirs are walked recursively, .js only)
|
|
177
|
-
* @param {RegExp} args.regex
|
|
178
|
-
* @param {Set<string>} [args.excludePaths] repo-relative path substrings to skip
|
|
179
|
-
*/
|
|
180
|
-
export async function grepFiles({ repoRoot, relPaths, regex, excludePaths = new Set() }) {
|
|
181
|
-
const matches = [];
|
|
182
|
-
for (const rel of relPaths) {
|
|
183
|
-
const abs = path.join(repoRoot, rel);
|
|
184
|
-
let stat;
|
|
185
|
-
try { stat = await fs.stat(abs); }
|
|
186
|
-
catch { continue; }
|
|
187
|
-
if (stat.isDirectory()) {
|
|
188
|
-
await walkAndGrep(abs, repoRoot, regex, matches, excludePaths);
|
|
189
|
-
} else if (stat.isFile()) {
|
|
190
|
-
await grepOneFile(abs, repoRoot, regex, matches, excludePaths);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return matches;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function walkAndGrep(absDir, repoRoot, regex, matches, excludePaths) {
|
|
197
|
-
let entries;
|
|
198
|
-
try { entries = await fs.readdir(absDir, { withFileTypes: true }); }
|
|
199
|
-
catch { return; }
|
|
200
|
-
for (const e of entries) {
|
|
201
|
-
if (e.name === 'node_modules' || e.name === '.git' || e.name.startsWith('.')) continue;
|
|
202
|
-
const full = path.join(absDir, e.name);
|
|
203
|
-
const rel = path.relative(repoRoot, full);
|
|
204
|
-
if ([...excludePaths].some(x => rel.includes(x))) continue;
|
|
205
|
-
if (e.isDirectory()) {
|
|
206
|
-
await walkAndGrep(full, repoRoot, regex, matches, excludePaths);
|
|
207
|
-
} else if (e.isFile() && (e.name.endsWith('.js') || e.name.endsWith('.mjs') || e.name.endsWith('.sh'))) {
|
|
208
|
-
await grepOneFile(full, repoRoot, regex, matches, excludePaths);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function grepOneFile(absPath, repoRoot, regex, matches, excludePaths) {
|
|
214
|
-
const rel = path.relative(repoRoot, absPath);
|
|
215
|
-
if ([...excludePaths].some(x => rel.includes(x))) return;
|
|
216
|
-
let raw;
|
|
217
|
-
try { raw = await fs.readFile(absPath, 'utf-8'); }
|
|
218
|
-
catch { return; }
|
|
219
|
-
const lines = raw.split(/\r?\n/);
|
|
220
|
-
for (let i = 0; i < lines.length; i++) {
|
|
221
|
-
if (regex.test(lines[i])) {
|
|
222
|
-
matches.push({ file: rel, lineNumber: i + 1, line: lines[i] });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Optional gcloud probe. Returns null if gcloud is unavailable OR command
|
|
229
|
-
* fails — callers decide whether to hard-fail or render a "skipped" note.
|
|
230
|
-
*
|
|
231
|
-
* Per scope §3 routing extractor: gcloud is BEST-EFFORT for the verification
|
|
232
|
-
* block, MANDATORY only when --env=demo|prod AND scope says hard-fail.
|
|
233
|
-
*
|
|
234
|
-
* Returns: { ok: true, json } | { ok: false, error }
|
|
235
|
-
*/
|
|
236
|
-
export function tryGcloudJson(args) {
|
|
237
|
-
try {
|
|
238
|
-
const out = execFileSync('gcloud', args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
239
|
-
return { ok: true, json: JSON.parse(out) };
|
|
240
|
-
} catch (err) {
|
|
241
|
-
return { ok: false, error: err.message || String(err) };
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
247
|
-
// M3 — env-aware live-state probes (gcloud + Firestore REST)
|
|
248
|
-
//
|
|
249
|
-
// These helpers replace the M2 best-effort tryGcloudJson() pattern for sections
|
|
250
|
-
// that NEED live state to be authoritative (§2 environments, §3 routing, §4
|
|
251
|
-
// microservice-deploy, §5 entitlements).
|
|
252
|
-
//
|
|
253
|
-
// Contract per scope §M3 / feedback_no-hardcoded-fallbacks:
|
|
254
|
-
// - --env=demo|prod → gcloud probe MUST succeed; failure → HARD-FAIL with
|
|
255
|
-
// code BRIEFER-GCLOUD-FAIL and an actionable remediation message.
|
|
256
|
-
// - --env=dev → gcloud probe is SKIPPED (DEV is local; no LB / no
|
|
257
|
-
// cloud Firestore). Caller emits a "DEV: gcloud probes skipped" stanza.
|
|
258
|
-
// - Each successful probe yields a citation with source=gcloud and a
|
|
259
|
-
// sha-of-output-slice so --check detects drift in LIVE STATE, not just
|
|
260
|
-
// code state. Two-sided drift detection: code OR cloud changes both
|
|
261
|
-
// trigger.
|
|
262
|
-
//
|
|
263
|
-
// Spawn pattern: child_process.execFile (NOT exec) for safety — arguments are
|
|
264
|
-
// passed as an array, never shell-interpolated. Timeout 30s; on timeout we
|
|
265
|
-
// HARD-FAIL (not fallback), per the no-hardcoded-fallbacks rule.
|
|
266
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
const GCLOUD_TIMEOUT_MS = 30_000;
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Run a gcloud command and return parsed JSON, with env-aware HARD-FAIL
|
|
272
|
-
* semantics.
|
|
273
|
-
*
|
|
274
|
-
* @param {object} args
|
|
275
|
-
* @param {string[]} args.command gcloud argv (e.g., ['compute','url-maps','describe','foo','--format=json']).
|
|
276
|
-
* @param {string} args.env 'dev' | 'demo' | 'prod' (REQUIRED — controls failure semantics).
|
|
277
|
-
* @param {string} args.section Briefer section label for error messages.
|
|
278
|
-
* @param {string} args.anchor Short label embedded in the citation (e.g., 'lb-url-map').
|
|
279
|
-
* @param {string} [args.expected] Construct shape description on failure.
|
|
280
|
-
* @param {string} [args.recovery] Operator recovery hint on failure.
|
|
281
|
-
*
|
|
282
|
-
* @returns {Promise<null | {json:any, raw:string, citation:object}>}
|
|
283
|
-
* - null when env==='dev' (probe intentionally skipped — caller emits stanza).
|
|
284
|
-
* - { json, raw, citation } on success. citation has shape:
|
|
285
|
-
* { source:'gcloud', probe:<argv-as-string>, env, anchor, sha, lines }
|
|
286
|
-
*
|
|
287
|
-
* HARD-FAILS with BrieferExtractorError code=GCLOUD_FAIL when env!=='dev' AND
|
|
288
|
-
* the probe fails (auth, empty output, JSON parse fail, timeout).
|
|
289
|
-
*/
|
|
290
|
-
export async function probeGcloudJson({ command, env, section, anchor, expected, recovery }) {
|
|
291
|
-
if (!Array.isArray(command) || command.length === 0) {
|
|
292
|
-
throw new Error('probeGcloudJson: command must be a non-empty argv array');
|
|
293
|
-
}
|
|
294
|
-
if (!env) {
|
|
295
|
-
throw new Error('probeGcloudJson: env is required');
|
|
296
|
-
}
|
|
297
|
-
if (!section || !anchor) {
|
|
298
|
-
throw new Error('probeGcloudJson: section and anchor are required');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (env === 'dev') {
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const probeStr = 'gcloud ' + command.join(' ');
|
|
306
|
-
let raw;
|
|
307
|
-
try {
|
|
308
|
-
const { stdout } = await execFileP('gcloud', command, {
|
|
309
|
-
encoding: 'utf-8',
|
|
310
|
-
timeout: GCLOUD_TIMEOUT_MS,
|
|
311
|
-
maxBuffer: 32 * 1024 * 1024
|
|
312
|
-
});
|
|
313
|
-
raw = stdout;
|
|
314
|
-
} catch (err) {
|
|
315
|
-
const timedOut = err.killed && err.signal === 'SIGTERM';
|
|
316
|
-
throw new BrieferExtractorError({
|
|
317
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
318
|
-
section,
|
|
319
|
-
source: 'gcloud',
|
|
320
|
-
expected: expected || `successful response from: ${probeStr}`,
|
|
321
|
-
recovery: recovery || `Run 'gcloud auth login' or 'gcloud config set project descix', then re-run \`descix briefer --env=${env}\`. If the auth is fine but the resource doesn't exist for env=${env}, that is a real platform-state issue — investigate before regenerating the briefer.`,
|
|
322
|
-
detail: `probe: ${probeStr}\n env: ${env}\n ${timedOut ? 'timed out after ' + GCLOUD_TIMEOUT_MS + 'ms' : 'failed: ' + (err.stderr || err.message || String(err)).split('\n')[0].slice(0, 240)}`
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (!raw || raw.trim() === '') {
|
|
327
|
-
throw new BrieferExtractorError({
|
|
328
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
329
|
-
section,
|
|
330
|
-
source: 'gcloud',
|
|
331
|
-
expected: expected || `non-empty JSON from: ${probeStr}`,
|
|
332
|
-
recovery: recovery || `The probe returned empty output. Verify the resource exists for env=${env} (e.g., 'gcloud projects describe descix').`,
|
|
333
|
-
detail: `probe: ${probeStr}\n env: ${env}\n empty stdout`
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
let json;
|
|
338
|
-
try {
|
|
339
|
-
json = JSON.parse(raw);
|
|
340
|
-
} catch (parseErr) {
|
|
341
|
-
throw new BrieferExtractorError({
|
|
342
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
343
|
-
section,
|
|
344
|
-
source: 'gcloud',
|
|
345
|
-
expected: expected || `parseable JSON from: ${probeStr}`,
|
|
346
|
-
recovery: recovery || `The probe returned non-JSON output. Ensure '--format=json' is in the command argv.`,
|
|
347
|
-
detail: `probe: ${probeStr}\n env: ${env}\n parse error: ${parseErr.message}\n first 240 chars: ${raw.slice(0, 240)}`
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const sha = sha12(raw);
|
|
352
|
-
return {
|
|
353
|
-
json,
|
|
354
|
-
raw,
|
|
355
|
-
citation: {
|
|
356
|
-
source: 'gcloud',
|
|
357
|
-
probe: probeStr,
|
|
358
|
-
env,
|
|
359
|
-
anchor,
|
|
360
|
-
sha,
|
|
361
|
-
// For the citation comment line: 'lines' field convention is reused so
|
|
362
|
-
// the existing extractCitationTrail regex captures these uniformly.
|
|
363
|
-
lines: `out:${raw.length}b`
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Probe a Firestore REST endpoint using gcloud-derived auth token, with the
|
|
370
|
-
* same env-aware HARD-FAIL semantics as probeGcloudJson().
|
|
371
|
-
*
|
|
372
|
-
* Per AC-7 anti-regression: this does NOT import @google-cloud/firestore. It
|
|
373
|
-
* uses the gcloud-CLI-derived access token + a raw HTTPS GET. This is a
|
|
374
|
-
* separate API surface from the Node SDK and is permitted.
|
|
375
|
-
*
|
|
376
|
-
* @param {object} args
|
|
377
|
-
* @param {string} args.dbPath e.g., 'descix-demo/documents/Products'
|
|
378
|
-
* (the part after 'projects/descix/databases/').
|
|
379
|
-
* @param {object} [args.query] query params (e.g., { pageSize: 50, 'mask.fieldPaths': 'app_id' }).
|
|
380
|
-
* @param {string} args.env
|
|
381
|
-
* @param {string} args.section
|
|
382
|
-
* @param {string} args.anchor
|
|
383
|
-
* @param {string} [args.expected]
|
|
384
|
-
* @param {string} [args.recovery]
|
|
385
|
-
*
|
|
386
|
-
* @returns {Promise<null | {json:any, raw:string, citation:object}>}
|
|
387
|
-
*/
|
|
388
|
-
export async function probeFirestoreRest({ dbPath, query, env, section, anchor, expected, recovery }) {
|
|
389
|
-
if (!dbPath) throw new Error('probeFirestoreRest: dbPath is required');
|
|
390
|
-
if (!env) throw new Error('probeFirestoreRest: env is required');
|
|
391
|
-
if (!section || !anchor) throw new Error('probeFirestoreRest: section and anchor are required');
|
|
392
|
-
|
|
393
|
-
if (env === 'dev') return null;
|
|
394
|
-
|
|
395
|
-
// Resolve gcloud access token. This is itself a gcloud probe — re-use the
|
|
396
|
-
// env-aware HARD-FAIL surface so the same operator-recovery path applies.
|
|
397
|
-
let token;
|
|
398
|
-
try {
|
|
399
|
-
const { stdout } = await execFileP('gcloud', ['auth', 'print-access-token'], {
|
|
400
|
-
encoding: 'utf-8',
|
|
401
|
-
timeout: GCLOUD_TIMEOUT_MS
|
|
402
|
-
});
|
|
403
|
-
token = stdout.trim();
|
|
404
|
-
} catch (err) {
|
|
405
|
-
throw new BrieferExtractorError({
|
|
406
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
407
|
-
section,
|
|
408
|
-
source: 'gcloud auth print-access-token',
|
|
409
|
-
expected: 'access token for Firestore REST',
|
|
410
|
-
recovery: recovery || "Run 'gcloud auth login' and retry.",
|
|
411
|
-
detail: `probe: gcloud auth print-access-token\n env: ${env}\n error: ${(err.stderr || err.message || String(err)).split('\n')[0].slice(0, 240)}`
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
if (!token) {
|
|
415
|
-
throw new BrieferExtractorError({
|
|
416
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
417
|
-
section,
|
|
418
|
-
source: 'gcloud auth print-access-token',
|
|
419
|
-
expected: 'non-empty access token',
|
|
420
|
-
recovery: recovery || "Run 'gcloud auth login' and retry.",
|
|
421
|
-
detail: `probe: gcloud auth print-access-token returned empty\n env: ${env}`
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const qs = query
|
|
426
|
-
? '?' + Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&')
|
|
427
|
-
: '';
|
|
428
|
-
const url = `https://firestore.googleapis.com/v1/projects/descix/databases/${dbPath}${qs}`;
|
|
429
|
-
const probeStr = `GET ${url}`;
|
|
430
|
-
|
|
431
|
-
const raw = await new Promise((resolve, reject) => {
|
|
432
|
-
const req = https.request(url, {
|
|
433
|
-
method: 'GET',
|
|
434
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
435
|
-
timeout: GCLOUD_TIMEOUT_MS
|
|
436
|
-
}, res => {
|
|
437
|
-
let chunks = '';
|
|
438
|
-
res.on('data', c => { chunks += c.toString('utf-8'); });
|
|
439
|
-
res.on('end', () => {
|
|
440
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
441
|
-
resolve(chunks);
|
|
442
|
-
} else {
|
|
443
|
-
reject(new Error(`HTTP ${res.statusCode}: ${chunks.slice(0, 240)}`));
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
req.on('timeout', () => { req.destroy(new Error('timeout after ' + GCLOUD_TIMEOUT_MS + 'ms')); });
|
|
448
|
-
req.on('error', reject);
|
|
449
|
-
req.end();
|
|
450
|
-
}).catch(err => {
|
|
451
|
-
throw new BrieferExtractorError({
|
|
452
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
453
|
-
section,
|
|
454
|
-
source: 'firestore-rest',
|
|
455
|
-
expected: expected || `200 OK from: ${probeStr}`,
|
|
456
|
-
recovery: recovery || `Verify the gcloud token has Firestore read scope and the database exists for env=${env}.`,
|
|
457
|
-
detail: `probe: ${probeStr}\n env: ${env}\n error: ${err.message.split('\n')[0].slice(0, 240)}`
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
let json;
|
|
462
|
-
try { json = JSON.parse(raw); }
|
|
463
|
-
catch (parseErr) {
|
|
464
|
-
throw new BrieferExtractorError({
|
|
465
|
-
code: BRIEFER_ERROR_CODES.GCLOUD_FAIL,
|
|
466
|
-
section,
|
|
467
|
-
source: 'firestore-rest',
|
|
468
|
-
expected: expected || `parseable JSON from: ${probeStr}`,
|
|
469
|
-
recovery: recovery || 'Inspect the raw response.',
|
|
470
|
-
detail: `probe: ${probeStr}\n env: ${env}\n parse error: ${parseErr.message}`
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const sha = sha12(raw);
|
|
475
|
-
return {
|
|
476
|
-
json,
|
|
477
|
-
raw,
|
|
478
|
-
citation: {
|
|
479
|
-
source: 'firestore-rest',
|
|
480
|
-
probe: probeStr,
|
|
481
|
-
env,
|
|
482
|
-
anchor,
|
|
483
|
-
sha,
|
|
484
|
-
lines: `out:${raw.length}b`
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Render a gcloud / firestore-rest citation as a hidden HTML comment line for
|
|
491
|
-
* embedding into the stitched briefer markdown. Mirrors makeCitation()'s shape
|
|
492
|
-
* but uses 'source=...' + 'probe=...' instead of 'file=...'.
|
|
493
|
-
*
|
|
494
|
-
* @param {object} c Citation object from probeGcloudJson / probeFirestoreRest.
|
|
495
|
-
*/
|
|
496
|
-
export function renderProbeCitationComment(c) {
|
|
497
|
-
if (!c) return '';
|
|
498
|
-
// Probe string can contain spaces and special chars — wrap in single quotes
|
|
499
|
-
// and escape any embedded single quote with a printable sentinel.
|
|
500
|
-
const probeEsc = (c.probe || '').replace(/'/g, "\\'");
|
|
501
|
-
return `<!-- briefer-cite: source=${c.source} env=${c.env} probe='${probeEsc}' sha=${c.sha} anchor=${c.anchor} lines=${c.lines} -->`;
|
|
502
|
-
}
|