@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,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* §3 — Request routing extractor (M2 implementation).
|
|
3
|
-
*
|
|
4
|
-
* This is THE highest-scrutiny section per the 2026-05-25 audit. The earlier
|
|
5
|
-
* cruft about "Gateway extracts productId + Firestore lookup" lives in V2_docs
|
|
6
|
-
* prose; this extractor pulls the CANONICAL routing model from code:
|
|
7
|
-
* - provision-platform-lb.js — platform LB (core NEG + apex/peer hosts)
|
|
8
|
-
* - serviceManifestManager.js — proxyToExternalService dispatch
|
|
9
|
-
* - apiFront.js — invoke() dispatch (internal + external command split)
|
|
10
|
-
* - app-sdk dev/gateway.js + createViteProxyConfig.js — local dev mirror
|
|
11
|
-
* - Live `gcloud compute url-maps describe descix-discord-app-lb`
|
|
12
|
-
* when gcloud is available and --env=demo|prod
|
|
13
|
-
*
|
|
14
|
-
* --env=dev is HARD-REJECTED in this extractor (per scope §2.1) — DEV has no
|
|
15
|
-
* LB URL map, so routing facts from gcloud cannot be verified.
|
|
16
|
-
*/
|
|
17
|
-
import {
|
|
18
|
-
readSourceFile,
|
|
19
|
-
findInLines,
|
|
20
|
-
makeCitation,
|
|
21
|
-
sliceRange,
|
|
22
|
-
probeGcloudJson
|
|
23
|
-
} from '../util/source-reader.js';
|
|
24
|
-
import { BrieferExtractorError, BRIEFER_ERROR_CODES } from '../errors.js';
|
|
25
|
-
|
|
26
|
-
export const SECTION = {
|
|
27
|
-
number: 3,
|
|
28
|
-
heading: '3. Request routing — the single canonical model',
|
|
29
|
-
sourceFiles: [
|
|
30
|
-
'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js',
|
|
31
|
-
'DeSciX/DeSciX_Cloud/microservice/services/serviceManifestManager.js',
|
|
32
|
-
'DeSciX/DeSciX_Cloud/microservice/services/apiFront.js',
|
|
33
|
-
'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/gateway.js',
|
|
34
|
-
'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/createViteProxyConfig.js'
|
|
35
|
-
]
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const LB_FILE = 'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js';
|
|
39
|
-
const SMM_FILE = 'DeSciX/DeSciX_Cloud/microservice/services/serviceManifestManager.js';
|
|
40
|
-
const APIFRONT_FILE = 'DeSciX/DeSciX_Cloud/microservice/services/apiFront.js';
|
|
41
|
-
const GATEWAY_FILE = 'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/gateway.js';
|
|
42
|
-
const PROXY_CONFIG_FILE = 'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/createViteProxyConfig.js';
|
|
43
|
-
|
|
44
|
-
export async function extract({ env, cliPaths } = {}) {
|
|
45
|
-
if (!env) {
|
|
46
|
-
throw new BrieferExtractorError({
|
|
47
|
-
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
48
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
49
|
-
source: 'extractor argument',
|
|
50
|
-
expected: 'env (demo|prod) provided by caller',
|
|
51
|
-
recovery: 'Pass {env} from the briefer entry point.'
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
// M2 hard-reject --env=dev for the routing extractor specifically.
|
|
55
|
-
// Per scope §2.1: DEV does not have an LB URL map, querying it would
|
|
56
|
-
// produce a malformed briefer. Other extractors that don't need gcloud
|
|
57
|
-
// may still accept dev; this rejection lives in routing.js.
|
|
58
|
-
if (env === 'dev') {
|
|
59
|
-
throw new BrieferExtractorError({
|
|
60
|
-
code: BRIEFER_ERROR_CODES.PARSE_FAIL,
|
|
61
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
62
|
-
source: 'env argument',
|
|
63
|
-
expected: 'demo or prod (the envs with a live LB URL map)',
|
|
64
|
-
recovery: 'DEV has no LB URL map; use --env=demo or --env=prod for routing extraction. Other sections may run with --env=dev independently.',
|
|
65
|
-
detail: `Got --env=dev. DEV has no descix-discord-app-lb URL map to verify routing against.`
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
if (!cliPaths) {
|
|
69
|
-
throw new BrieferExtractorError({
|
|
70
|
-
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
71
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
72
|
-
source: 'extractor argument',
|
|
73
|
-
expected: 'cliPaths provided by caller',
|
|
74
|
-
recovery: 'Pass {cliPaths} from the briefer entry point.'
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Source 1: provision-platform-lb.js — platform LB standup ──
|
|
79
|
-
const lb = await readSourceFile({
|
|
80
|
-
cliPaths,
|
|
81
|
-
relPath: LB_FILE,
|
|
82
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const ensureCoreNegMatch = findInLines({
|
|
86
|
-
lines: lb.lines,
|
|
87
|
-
regex: /function\s+ensureCoreNeg\s*\(/,
|
|
88
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
89
|
-
source: `${LB_FILE} (ensureCoreNeg)`,
|
|
90
|
-
expected: 'function ensureCoreNeg(envCfg, dryRun)',
|
|
91
|
-
recovery: `Re-locate ensureCoreNeg in ${LB_FILE}.`,
|
|
92
|
-
expectedRange: [80, 140]
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const singletonMatcherMatch = findInLines({
|
|
96
|
-
lines: lb.lines,
|
|
97
|
-
regex: /function\s+buildSingletonMatcher\s*\(/,
|
|
98
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
99
|
-
source: `${LB_FILE} (buildSingletonMatcher)`,
|
|
100
|
-
expected: 'function buildSingletonMatcher(...)',
|
|
101
|
-
recovery: `Re-locate buildSingletonMatcher in ${LB_FILE}.`
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const apiFrontRuleMatch = findInLines({
|
|
105
|
-
lines: lb.lines,
|
|
106
|
-
regex: /['"`]\/apifront['"`]/,
|
|
107
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
108
|
-
source: `${LB_FILE} (command-broker /apifront rule)`,
|
|
109
|
-
expected: `'/apifront' in buildCommandBrokerRules`,
|
|
110
|
-
recovery: `Re-locate the /apifront command-broker rule.`
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ── Source 2: serviceManifestManager.js — proxyToExternalService ──
|
|
114
|
-
const smm = await readSourceFile({
|
|
115
|
-
cliPaths,
|
|
116
|
-
relPath: SMM_FILE,
|
|
117
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const proxyMatch = findInLines({
|
|
121
|
-
lines: smm.lines,
|
|
122
|
-
regex: /export\s+async\s+function\s+proxyToExternalService\s*\(/,
|
|
123
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
124
|
-
source: `${SMM_FILE}:685-765 (proxyToExternalService)`,
|
|
125
|
-
expected: 'export async function proxyToExternalService(commandName, params, sessionToken = null, req = null)',
|
|
126
|
-
recovery: `Re-locate proxyToExternalService in ${SMM_FILE}.`,
|
|
127
|
-
expectedRange: [650, 800]
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ── Source 3: apiFront.js — invoke() dispatch ──
|
|
131
|
-
const apiFront = await readSourceFile({
|
|
132
|
-
cliPaths,
|
|
133
|
-
relPath: APIFRONT_FILE,
|
|
134
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const invokeMatch = findInLines({
|
|
138
|
-
lines: apiFront.lines,
|
|
139
|
-
regex: /static\s+async\s+invoke\s*\(\s*commandName/,
|
|
140
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
141
|
-
source: `${APIFRONT_FILE}:83-186 (CommandHandler.invoke + _invokeExternalCommand)`,
|
|
142
|
-
expected: 'static async invoke(commandName, params, req, res)',
|
|
143
|
-
recovery: `Re-locate CommandHandler.invoke in ${APIFRONT_FILE}.`,
|
|
144
|
-
expectedRange: [50, 200]
|
|
145
|
-
});
|
|
146
|
-
const externalDispatchMatch = findInLines({
|
|
147
|
-
lines: apiFront.lines,
|
|
148
|
-
regex: /static\s+async\s+_invokeExternalCommand\s*\(/,
|
|
149
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
150
|
-
source: `${APIFRONT_FILE} (_invokeExternalCommand)`,
|
|
151
|
-
expected: 'static async _invokeExternalCommand(commandName, params, req)',
|
|
152
|
-
recovery: `Re-locate _invokeExternalCommand in ${APIFRONT_FILE}.`
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// ── Source 4: app-sdk gateway.js + createViteProxyConfig.js ──
|
|
156
|
-
const gateway = await readSourceFile({
|
|
157
|
-
cliPaths,
|
|
158
|
-
relPath: GATEWAY_FILE,
|
|
159
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
160
|
-
});
|
|
161
|
-
const proxyConfig = await readSourceFile({
|
|
162
|
-
cliPaths,
|
|
163
|
-
relPath: PROXY_CONFIG_FILE,
|
|
164
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ── M3: live gcloud probe (HARD-FAIL on demo/prod, skipped on dev) ──
|
|
168
|
-
// routing.js already hard-rejects --env=dev above (no LB URL map in DEV),
|
|
169
|
-
// so this probe ALWAYS runs for demo/prod and MUST succeed. probeGcloudJson
|
|
170
|
-
// throws BRIEFER-GCLOUD-FAIL with an actionable recovery message otherwise.
|
|
171
|
-
let gcloudBlock = '';
|
|
172
|
-
let gcloudCitation = null;
|
|
173
|
-
const lbProbe = await probeGcloudJson({
|
|
174
|
-
command: ['compute', 'url-maps', 'describe', 'descix-discord-app-lb', '--format=json'],
|
|
175
|
-
env,
|
|
176
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
177
|
-
anchor: 'lb-url-map',
|
|
178
|
-
expected: 'JSON describing the descix-discord-app-lb URL map',
|
|
179
|
-
recovery: `Run 'gcloud auth login' and 'gcloud config set project descix'. If the URL map does not exist for env=${env}, that is a real platform-state issue — investigate before regenerating the briefer.`
|
|
180
|
-
});
|
|
181
|
-
// lbProbe is non-null for demo/prod (dev is rejected upstream).
|
|
182
|
-
const hostRuleCount = (lbProbe.json.hostRules || []).length;
|
|
183
|
-
const pathMatcherCount = (lbProbe.json.pathMatchers || []).length;
|
|
184
|
-
const fingerprint = lbProbe.json.fingerprint || 'unknown';
|
|
185
|
-
gcloudBlock = [
|
|
186
|
-
'',
|
|
187
|
-
'**Live LB state** (live `gcloud compute url-maps describe descix-discord-app-lb` at regen time):',
|
|
188
|
-
`- Host rules: \`${hostRuleCount}\``,
|
|
189
|
-
`- Path matchers: \`${pathMatcherCount}\``,
|
|
190
|
-
`- URL map fingerprint: \`${fingerprint}\``,
|
|
191
|
-
`- Probed: \`${lbProbe.citation.probe}\` (env=\`${env}\`)`,
|
|
192
|
-
''
|
|
193
|
-
].join('\n');
|
|
194
|
-
gcloudCitation = lbProbe.citation;
|
|
195
|
-
|
|
196
|
-
const singletonBlockEnd = findFunctionEnd(lb.lines, singletonMatcherMatch.lineNumber);
|
|
197
|
-
const singletonBlock = sliceRange(lb.lines, singletonMatcherMatch.lineNumber, singletonBlockEnd);
|
|
198
|
-
|
|
199
|
-
// ── Render the section markdown ──
|
|
200
|
-
const markdown = [
|
|
201
|
-
`Broker-first mesh (WS-MESH-BROKER-ONLY): sites call \`/apifront\`; Core validates entitlements and dispatches via Firestore \`ServiceManifests\`. **No NEG on routine deploy for non-core apps.** No runtime LB lookup for new apps.`,
|
|
202
|
-
``,
|
|
203
|
-
`Platform LB (\`descix-discord-app-lb\`) is provisioned at platform standup by \`deploy-backend-env.sh\` → \`provision-platform-lb.js\` (re-run after \`deploy-service-env.sh powch\` for powch NEG):`,
|
|
204
|
-
``,
|
|
205
|
-
`- **daita** broker NEG: \`apifront-http-{env}-neg\` → \`{env}-api-backend\` → Cloud Function \`apiFront-http-{env}\` — \`${LB_FILE}:${ensureCoreNegMatch.lineNumber}\``,
|
|
206
|
-
`- Apex singleton (\`daita\`): \`demo.descix.net\` / \`descix.net\` → GCS \`/{env}/daita/site/\` + \`/apifront\`, \`/mcp\`, \`/api\` → daita broker — \`${LB_FILE}:${singletonMatcherMatch.lineNumber}\``,
|
|
207
|
-
`- **powch** core platform NEG: \`powch-{env}-neg\` → \`powch-{env}-backend\` → Cloud Run \`powch-{env}\` — \`${LB_FILE} (ensureCloudRunAppNeg)\``,
|
|
208
|
-
`- Platform peer host (\`powch.{env}.descix.net\`): GCS site + \`/apifront\`, \`/mcp\` → daita broker; \`/api/*\` → powch NEG`,
|
|
209
|
-
``,
|
|
210
|
-
`Singleton matcher construction (verbatim from \`${LB_FILE}\` L${singletonMatcherMatch.lineNumber}-${singletonBlockEnd}):`,
|
|
211
|
-
``,
|
|
212
|
-
'```js',
|
|
213
|
-
singletonBlock,
|
|
214
|
-
'```',
|
|
215
|
-
``,
|
|
216
|
-
`Runtime dispatch: central CF reads \`externalCommandRegistry\` and calls \`proxyToExternalService()\`. — \`${SMM_FILE}:${proxyMatch.lineNumber}\`, \`${APIFRONT_FILE}:${invokeMatch.lineNumber} (invoke), ${externalDispatchMatch.lineNumber} (_invokeExternalCommand)\``,
|
|
217
|
-
gcloudBlock,
|
|
218
|
-
`**Local \`descix serve\` mirror:** \`${GATEWAY_FILE}\` + \`${PROXY_CONFIG_FILE}\` proxy \`/apifront\`, \`/api\`, \`/mcp\` to Core. Product sites at \`/p/{appId}/\` or static plugin routes.`
|
|
219
|
-
].join('\n');
|
|
220
|
-
|
|
221
|
-
const citations = [
|
|
222
|
-
makeCitation({ file: LB_FILE, lines: `${ensureCoreNegMatch.lineNumber}-${singletonBlockEnd}`, anchor: 'ensureCoreNeg + buildSingletonMatcher', fileLines: lb.lines }),
|
|
223
|
-
makeCitation({ file: LB_FILE, lines: String(apiFrontRuleMatch.lineNumber), anchor: 'command-broker /apifront', fileLines: lb.lines }),
|
|
224
|
-
makeCitation({ file: SMM_FILE, lines: String(proxyMatch.lineNumber), anchor: 'proxyToExternalService', fileLines: smm.lines }),
|
|
225
|
-
makeCitation({ file: APIFRONT_FILE, lines: String(invokeMatch.lineNumber), anchor: 'CommandHandler.invoke', fileLines: apiFront.lines }),
|
|
226
|
-
makeCitation({ file: APIFRONT_FILE, lines: String(externalDispatchMatch.lineNumber), anchor: '_invokeExternalCommand', fileLines: apiFront.lines }),
|
|
227
|
-
makeCitation({ file: GATEWAY_FILE, lines: `1-${gateway.lines.length}`, anchor: 'local serve gateway', fileLines: gateway.lines }),
|
|
228
|
-
makeCitation({ file: PROXY_CONFIG_FILE, lines: `1-${proxyConfig.lines.length}`, anchor: 'vite proxy rules', fileLines: proxyConfig.lines }),
|
|
229
|
-
gcloudCitation
|
|
230
|
-
].filter(Boolean);
|
|
231
|
-
|
|
232
|
-
return { markdown, citations };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Walk forward from `const pathRules = [` until the matching `];`.
|
|
237
|
-
* Bounded to 100 lines so a pathological refactor doesn't drag huge swaths.
|
|
238
|
-
*/
|
|
239
|
-
function findFunctionEnd(lines, startLine) {
|
|
240
|
-
const MAX = Math.min(lines.length, startLine + 80);
|
|
241
|
-
let depth = 0;
|
|
242
|
-
let started = false;
|
|
243
|
-
for (let i = startLine - 1; i < MAX; i++) {
|
|
244
|
-
const line = lines[i];
|
|
245
|
-
for (const ch of line) {
|
|
246
|
-
if (ch === '{') { depth++; started = true; }
|
|
247
|
-
else if (ch === '}') {
|
|
248
|
-
depth--;
|
|
249
|
-
if (started && depth === 0) return i + 1;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return Math.min(lines.length, startLine + 40);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function findArrayEnd(lines, startLine) {
|
|
257
|
-
const MAX = Math.min(lines.length, startLine + 100);
|
|
258
|
-
let depth = 0;
|
|
259
|
-
for (let i = startLine - 1; i < MAX; i++) {
|
|
260
|
-
const line = lines[i];
|
|
261
|
-
for (const ch of line) {
|
|
262
|
-
if (ch === '[') depth++;
|
|
263
|
-
else if (ch === ']') {
|
|
264
|
-
depth--;
|
|
265
|
-
if (depth === 0) return i + 1;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return Math.min(lines.length, startLine + 50);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function shortenGcloudErr(msg) {
|
|
273
|
-
if (!msg) return 'unknown error';
|
|
274
|
-
return msg.split('\n')[0].slice(0, 120);
|
|
275
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* §6 — What is NOT in the system extractor (M2 implementation).
|
|
3
|
-
*
|
|
4
|
-
* Per WS-DESCIX-BRIEFER-CLI scope doc §2.2 §6, derived by NEGATIVE ASSERTION:
|
|
5
|
-
* the CLI greps for forbidden constructs and HARD-FAILS with BRIEFER-NEGATIVE-CLAIM
|
|
6
|
-
* if any are present. Toggle-claims (CLI subcommands "not yet built") are
|
|
7
|
-
* rendered dynamically — present if absent, removed if found.
|
|
8
|
-
*
|
|
9
|
-
* Negative claims this extractor verifies:
|
|
10
|
-
* (1) No API Gateway middleware at runtime — grep services/ for gatewayDispatch/routeByProduct/etc.
|
|
11
|
-
* (2) No runtime Firestore lookup for routing — grep provision-platform-lb.js for CacheFirestore/admin.firestore
|
|
12
|
-
* (3) No per-app DNS provisioning — grep for `gcloud dns` / `domain-mappings` (recent CRUFT-8 fix)
|
|
13
|
-
* (4) No per-app TLS cert — verify wildcard cert config in provision-platform-lb.js
|
|
14
|
-
* (5) No `descix microservice deploy` CLI (TOGGLE) — grep bin/descix.js
|
|
15
|
-
* (6) No descix-chain runtime lookup for routing — grep provision-platform-lb.js for descix-chain
|
|
16
|
-
* (7) No path rewriting in `descix serve` for product sites — cite createViteProxyConfig.js
|
|
17
|
-
* (8) No `--env` flag for the MCP server — toggle, grep mcp-server.js
|
|
18
|
-
*/
|
|
19
|
-
import {
|
|
20
|
-
readSourceFile,
|
|
21
|
-
findInLines,
|
|
22
|
-
makeCitation,
|
|
23
|
-
grepFiles
|
|
24
|
-
} from '../util/source-reader.js';
|
|
25
|
-
import { BrieferExtractorError, BRIEFER_ERROR_CODES } from '../errors.js';
|
|
26
|
-
|
|
27
|
-
export const SECTION = {
|
|
28
|
-
number: 6,
|
|
29
|
-
heading: '6. What is NOT in the system',
|
|
30
|
-
sourceFiles: [
|
|
31
|
-
'DeSciX/DeSciX_Cloud/microservice/services',
|
|
32
|
-
'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js',
|
|
33
|
-
'DeSciX/DeSciX_Core/descix-cli/bin/descix.js',
|
|
34
|
-
'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/createViteProxyConfig.js'
|
|
35
|
-
]
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const MESH_FILE = 'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js';
|
|
39
|
-
const CLI_FILE = 'DeSciX/DeSciX_Core/descix-cli/bin/descix.js';
|
|
40
|
-
const PROXY_CONFIG = 'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/createViteProxyConfig.js';
|
|
41
|
-
const SERVICES_DIR = 'DeSciX/DeSciX_Cloud/microservice/services';
|
|
42
|
-
|
|
43
|
-
export async function extract({ env, cliPaths } = {}) {
|
|
44
|
-
if (!env) {
|
|
45
|
-
throw new BrieferExtractorError({
|
|
46
|
-
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
47
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
48
|
-
source: 'extractor argument',
|
|
49
|
-
expected: 'env (dev|demo|prod) provided by caller',
|
|
50
|
-
recovery: 'Pass {env} from the briefer entry point.'
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (!cliPaths) {
|
|
54
|
-
throw new BrieferExtractorError({
|
|
55
|
-
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
56
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
57
|
-
source: 'extractor argument',
|
|
58
|
-
expected: 'cliPaths provided by caller',
|
|
59
|
-
recovery: 'Pass {cliPaths} from the briefer entry point.'
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// (1) HARD: no gatewayDispatch/routeByProduct/etc. in services/.
|
|
64
|
-
const forbiddenGateway = await grepFiles({
|
|
65
|
-
repoRoot: cliPaths.repoRoot,
|
|
66
|
-
relPaths: [SERVICES_DIR],
|
|
67
|
-
regex: /\b(gatewayDispatch|routeByProduct|extractProductId|getProductId)\b/,
|
|
68
|
-
excludePaths: new Set(['/node_modules/', '/.git/', '/tests/'])
|
|
69
|
-
});
|
|
70
|
-
if (forbiddenGateway.length > 0) {
|
|
71
|
-
const sample = forbiddenGateway.slice(0, 3).map(m => `${m.file}:${m.lineNumber}`).join(', ');
|
|
72
|
-
throw new BrieferExtractorError({
|
|
73
|
-
code: BRIEFER_ERROR_CODES.NEGATIVE_CLAIM,
|
|
74
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
75
|
-
source: `grep on ${SERVICES_DIR}`,
|
|
76
|
-
expected: 'no matches for gatewayDispatch|routeByProduct|extractProductId|getProductId',
|
|
77
|
-
recovery: 'Either the briefer is stale (a runtime gateway middleware was introduced) or the code added a forbidden construct. Reconcile by rewriting the briefer claim or removing the code.',
|
|
78
|
-
detail: `Found ${forbiddenGateway.length} match(es). Sample: ${sample}`
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// (2) HARD: no CacheFirestore / admin.firestore in provision-platform-lb.js.
|
|
83
|
-
const mesh = await readSourceFile({
|
|
84
|
-
cliPaths,
|
|
85
|
-
relPath: MESH_FILE,
|
|
86
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
87
|
-
});
|
|
88
|
-
for (let i = 0; i < mesh.lines.length; i++) {
|
|
89
|
-
if (/CacheFirestore|admin\.firestore\(/.test(mesh.lines[i])) {
|
|
90
|
-
throw new BrieferExtractorError({
|
|
91
|
-
code: BRIEFER_ERROR_CODES.NEGATIVE_CLAIM,
|
|
92
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
93
|
-
source: `${MESH_FILE}:${i + 1}`,
|
|
94
|
-
expected: 'no runtime Firestore lookup in provision-platform-lb.js (LB wiring is deploy-time only)',
|
|
95
|
-
recovery: 'A Firestore lookup was added to the LB-wiring path. Reconcile: routing must not depend on runtime Firestore reads.',
|
|
96
|
-
detail: `Match at ${MESH_FILE}:${i + 1} — ${mesh.lines[i].trim()}`
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// (3) HARD: no per-app DNS provisioning in deploy scripts.
|
|
102
|
-
// CRUFT-8 fix yesterday removed the legacy `gcloud dns` references; ensure
|
|
103
|
-
// they don't come back.
|
|
104
|
-
const dnsMatches = await grepFiles({
|
|
105
|
-
repoRoot: cliPaths.repoRoot,
|
|
106
|
-
relPaths: [
|
|
107
|
-
'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy',
|
|
108
|
-
'DeSciX/DeSciX_Powch/microservice/scripts'
|
|
109
|
-
],
|
|
110
|
-
regex: /gcloud\s+dns\s+|domain-mappings\s+/,
|
|
111
|
-
excludePaths: new Set(['/node_modules/', '/.git/'])
|
|
112
|
-
});
|
|
113
|
-
// Soft: only WARN. DNS may legitimately surface in cert-management docs.
|
|
114
|
-
// The briefer claim is rendered as-is; matches surface as a footnote.
|
|
115
|
-
|
|
116
|
-
// (4) HARD: wildcard cert config (single managed cert) — grep
|
|
117
|
-
// provision-platform-lb.js to ensure no per-app cert provisioning was wired.
|
|
118
|
-
for (let i = 0; i < mesh.lines.length; i++) {
|
|
119
|
-
if (/ssl-certificates\s+create|managedCert/.test(mesh.lines[i])) {
|
|
120
|
-
throw new BrieferExtractorError({
|
|
121
|
-
code: BRIEFER_ERROR_CODES.NEGATIVE_CLAIM,
|
|
122
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
123
|
-
source: `${MESH_FILE}:${i + 1}`,
|
|
124
|
-
expected: 'no per-app SSL cert provisioning in mesh-routing wiring',
|
|
125
|
-
recovery: 'Per-app cert provisioning was added — reconcile with the wildcard-cert architecture.',
|
|
126
|
-
detail: `Match at ${MESH_FILE}:${i + 1} — ${mesh.lines[i].trim()}`
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// (5) TOGGLE: no `descix microservice deploy` (yet) — grep CLI.
|
|
132
|
-
const cli = await readSourceFile({
|
|
133
|
-
cliPaths,
|
|
134
|
-
relPath: CLI_FILE,
|
|
135
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
136
|
-
});
|
|
137
|
-
let microserviceDeployFound = false;
|
|
138
|
-
for (let i = 1; i < cli.lines.length; i++) {
|
|
139
|
-
if (/\.command\(['"]deploy['"]\)/.test(cli.lines[i])) {
|
|
140
|
-
for (let j = i - 1; j >= Math.max(0, i - 3); j--) {
|
|
141
|
-
if (/microserviceCommand/.test(cli.lines[j])) {
|
|
142
|
-
microserviceDeployFound = true;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (microserviceDeployFound) break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// (6) HARD: no descix-chain runtime lookup for routing — grep mesh.
|
|
151
|
-
for (let i = 0; i < mesh.lines.length; i++) {
|
|
152
|
-
if (/descix-chain/.test(mesh.lines[i])) {
|
|
153
|
-
throw new BrieferExtractorError({
|
|
154
|
-
code: BRIEFER_ERROR_CODES.NEGATIVE_CLAIM,
|
|
155
|
-
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
156
|
-
source: `${MESH_FILE}:${i + 1}`,
|
|
157
|
-
expected: 'no descix-chain reference in LB-wiring code (routing is host-rule based, not chain-lookup based)',
|
|
158
|
-
recovery: 'A descix-chain reference appeared in the LB wiring path. Reconcile.',
|
|
159
|
-
detail: `Match at ${MESH_FILE}:${i + 1} — ${mesh.lines[i].trim()}`
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// (7) Cite: no path rewriting in `descix serve` for product sites.
|
|
165
|
-
const proxyConfig = await readSourceFile({
|
|
166
|
-
cliPaths,
|
|
167
|
-
relPath: PROXY_CONFIG,
|
|
168
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// (8) TOGGLE: no `--env` for MCP server. Per scope §2.2 §6, surface this
|
|
172
|
-
// as the open WS-CLI-HEALTH-ENV-PIVOT gap. We grep for an `--env` option
|
|
173
|
-
// declaration in mcp-server.js / mcp* CLI surface.
|
|
174
|
-
let mcpEnvFound = false;
|
|
175
|
-
try {
|
|
176
|
-
const mcp = await readSourceFile({
|
|
177
|
-
cliPaths,
|
|
178
|
-
relPath: 'DeSciX/DeSciX_Core/descix-cli/bin/mcp-server.js',
|
|
179
|
-
section: `§${SECTION.number} ${SECTION.heading}`
|
|
180
|
-
});
|
|
181
|
-
for (let i = 0; i < mcp.lines.length; i++) {
|
|
182
|
-
if (/--env|\.option\(['"]--env/.test(mcp.lines[i])) {
|
|
183
|
-
mcpEnvFound = true;
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
} catch (err) {
|
|
188
|
-
// If mcp-server.js doesn't exist, the gap is trivially open.
|
|
189
|
-
if (err.code !== BRIEFER_ERROR_CODES.SRC_NOT_FOUND) throw err;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ── Render markdown ──
|
|
193
|
-
const lines = [
|
|
194
|
-
`- **No "API Gateway" middleware at runtime.** No Lambda, no Cloud Function, no Cloud Run named "gateway." The LB URL map is the only request-time router. _Verified: grep on \`${SERVICES_DIR}\` for \`gatewayDispatch|routeByProduct|extractProductId|getProductId\` → 0 matches._`,
|
|
195
|
-
`- **No runtime Firestore lookup for routing.** The LB does not read Firestore. Period. _Verified: grep on \`${MESH_FILE}\` for \`CacheFirestore|admin.firestore\` → 0 matches._`,
|
|
196
|
-
`- **No per-app DNS provisioning.** Wildcard \`*.{env}.descix.net\` cert + wildcard A record cover all apps. Adding an app does NOT touch DNS.${dnsMatches.length > 0 ? ` _(${dnsMatches.length} \`gcloud dns\`/\`domain-mappings\` reference(s) detected in deploy scripts — manually verify these are wildcard-cert touches, not per-app DNS.)_` : ''}`,
|
|
197
|
-
`- **No per-app TLS cert.** Same wildcard cert covers everything. _Verified: grep on \`${MESH_FILE}\` for \`ssl-certificates create|managedCert\` → 0 matches._`
|
|
198
|
-
];
|
|
199
|
-
if (microserviceDeployFound) {
|
|
200
|
-
lines.push(`- ~~No \`descix microservice deploy\` CLI command~~ — **GAP CLOSED** at this regen. \`descix microservice deploy\` is present in \`${CLI_FILE}\`.`);
|
|
201
|
-
} else {
|
|
202
|
-
lines.push(`- **No \`descix microservice deploy\` CLI command (yet).** \`descix microservice register\` does Firestore-manifest-registration only. _Detected: grep on \`${CLI_FILE}\` for \`microserviceCommand.command('deploy')\` → 0 matches._`);
|
|
203
|
-
}
|
|
204
|
-
lines.push(
|
|
205
|
-
`- **No central \`descix-chain\` runtime lookup for routing.** \`descix-chain\` Firestore is the canonical token/contract registry, read for contract addresses (e.g., \`getContractAddressBySymbol()\`); never for request routing. _Verified: grep on \`${MESH_FILE}\` for \`descix-chain\` → 0 matches._`,
|
|
206
|
-
`- **No path rewriting for product sites in \`descix serve\` (local dev).** Each app sets its own framework base path. — \`${PROXY_CONFIG}\``
|
|
207
|
-
);
|
|
208
|
-
if (mcpEnvFound) {
|
|
209
|
-
lines.push(`- ~~No \`--env\` flag for the MCP server~~ — **GAP CLOSED**. WS-CLI-HEALTH-ENV-PIVOT shipped: MCP accepts \`--env\`.`);
|
|
210
|
-
} else {
|
|
211
|
-
lines.push(`- **No \`--env\` flag for the MCP server.** MCP reads workspace.json once at startup and is bound for its lifetime. Tracked as WS-CLI-HEALTH-ENV-PIVOT (open as of this regen).`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const markdown = lines.join('\n');
|
|
215
|
-
|
|
216
|
-
const citations = [
|
|
217
|
-
makeCitation({ file: MESH_FILE, lines: '1-' + mesh.lines.length, anchor: 'no runtime Firestore / TLS / chain in LB wiring', fileLines: mesh.lines }),
|
|
218
|
-
makeCitation({ file: CLI_FILE, lines: '1-' + cli.lines.length, anchor: 'descix microservice deploy toggle', fileLines: cli.lines }),
|
|
219
|
-
makeCitation({ file: PROXY_CONFIG, lines: '1-' + proxyConfig.lines.length, anchor: 'no path rewriting in descix serve', fileLines: proxyConfig.lines })
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
return { markdown, citations };
|
|
223
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown stitcher — composes per-section extract outputs into the final
|
|
3
|
-
* briefer markdown, with the canonical header (provenance, last-regen timestamp,
|
|
4
|
-
* confidence, audience) per the existing briefer at
|
|
5
|
-
* DeSciX/V2_docs/architecture/platform-must-know-briefer.md.
|
|
6
|
-
*
|
|
7
|
-
* Per WS-DESCIX-BRIEFER-CLI scope doc §2.5 (Provenance), every successful regen
|
|
8
|
-
* appends a row to the briefer's "Provenance / regeneration history" table with
|
|
9
|
-
* source blob SHAs, gcloud etags, and Products counts.
|
|
10
|
-
*
|
|
11
|
-
* Per scope doc §2.2 §3 critical safeguard, each citation also emits a hidden
|
|
12
|
-
* HTML comment `<!-- briefer-cite: file=... sha=... lines=... anchor=... -->`
|
|
13
|
-
* so `--check` can diff citations (not just rendered prose).
|
|
14
|
-
*
|
|
15
|
-
* M1: produces a real (non-empty) markdown file using the section stubs so the
|
|
16
|
-
* CLI surface can be exercised end-to-end.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const HEADER_TEMPLATE = (env, regenTs, mechanism, sectionsCount) => [
|
|
20
|
-
`# DeSciX Platform Must-Know Briefer`,
|
|
21
|
-
``,
|
|
22
|
-
`> **THIS DOC WINS.** If anything else in V2_docs, the cloud KB, an agent persona,`,
|
|
23
|
-
`> or a code comment conflicts with what is written here, this briefer is canonical.`,
|
|
24
|
-
`> Conflicting prose is cruft and should be patched, not relied on.`,
|
|
25
|
-
``,
|
|
26
|
-
`**Last regenerated:** ${regenTs}`,
|
|
27
|
-
`**Regeneration mechanism:** ${mechanism}`,
|
|
28
|
-
`**Target env (for this regen):** \`${env}\``,
|
|
29
|
-
`**Sections rendered:** ${sectionsCount}`,
|
|
30
|
-
`**Audience:** every COS / EVP / sub-agent at bootstrap. Load this BEFORE any other architecture chunk.`,
|
|
31
|
-
``,
|
|
32
|
-
`---`,
|
|
33
|
-
``
|
|
34
|
-
].join('\n');
|
|
35
|
-
|
|
36
|
-
const FOOTER_TEMPLATE = (provenanceRow) => [
|
|
37
|
-
`---`,
|
|
38
|
-
``,
|
|
39
|
-
`## Provenance / regeneration history`,
|
|
40
|
-
``,
|
|
41
|
-
`| Date | Mechanism | By | Notes |`,
|
|
42
|
-
`|---|---|---|---|`,
|
|
43
|
-
`${provenanceRow}`,
|
|
44
|
-
``,
|
|
45
|
-
`---`,
|
|
46
|
-
``,
|
|
47
|
-
`## Engineering principle declarations (this briefer)`,
|
|
48
|
-
``,
|
|
49
|
-
`- **No fallback prose.** If a claim cannot be cited to code or live infra state, it is not in this briefer.`,
|
|
50
|
-
`- **Single source of truth.** This file is the canonical mental model. Other docs paraphrase; this one binds.`,
|
|
51
|
-
`- **Removal over deprecation.** When this briefer changes, contradictory passages in other files are REWRITTEN or REMOVED — never marked "deprecated" while left in place.`,
|
|
52
|
-
``
|
|
53
|
-
].join('\n');
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Render a citation set as hidden HTML comments. These are the "machine trail"
|
|
57
|
-
* that --check uses to detect citation drift (see scope doc §2.2 §3 safeguard).
|
|
58
|
-
*/
|
|
59
|
-
function renderCitationTrail(citations) {
|
|
60
|
-
if (!Array.isArray(citations) || citations.length === 0) return '';
|
|
61
|
-
return citations.map(c => {
|
|
62
|
-
const sha = c.sha || 'PENDING-M2';
|
|
63
|
-
if (c.source === 'gcloud' || c.source === 'firestore-rest') {
|
|
64
|
-
// M3 — live-state probe citation. Escapes single quotes in the probe
|
|
65
|
-
// string so the comment remains a single line + diff-friendly.
|
|
66
|
-
const probeEsc = (c.probe || '').replace(/'/g, "\\'");
|
|
67
|
-
return `<!-- briefer-cite: source=${c.source} env=${c.env} probe='${probeEsc}' sha=${sha} anchor=${c.anchor} lines=${c.lines || ''} -->`;
|
|
68
|
-
}
|
|
69
|
-
return `<!-- briefer-cite: file=${c.file} lines=${c.lines} sha=${sha} anchor=${c.anchor} -->`;
|
|
70
|
-
}).join('\n');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Stitch the briefer document.
|
|
75
|
-
*
|
|
76
|
-
* @param {object} args
|
|
77
|
-
* @param {string} args.env Target env (dev|demo|prod).
|
|
78
|
-
* @param {string} args.mechanism Mechanism description (e.g., "descix briefer v1.0 (M1 scaffold)").
|
|
79
|
-
* @param {string} args.regenBy Who triggered the regen (e.g., wallet email).
|
|
80
|
-
* @param {Array<{section, extract}>} args.sections
|
|
81
|
-
* Per-section results: section metadata + extract output.
|
|
82
|
-
* @returns {string} Final markdown document.
|
|
83
|
-
*/
|
|
84
|
-
export function stitchBriefer({ env, mechanism, regenBy, sections }) {
|
|
85
|
-
if (!env) throw new Error('stitchBriefer: env is required');
|
|
86
|
-
if (!mechanism) throw new Error('stitchBriefer: mechanism is required');
|
|
87
|
-
if (!regenBy) throw new Error('stitchBriefer: regenBy is required');
|
|
88
|
-
if (!Array.isArray(sections)) throw new Error('stitchBriefer: sections must be an array');
|
|
89
|
-
|
|
90
|
-
const regenTs = new Date().toISOString().replace('T', ' ').replace(/\..*$/, ' UTC');
|
|
91
|
-
const header = HEADER_TEMPLATE(env, regenTs, mechanism, sections.length);
|
|
92
|
-
|
|
93
|
-
const body = sections.map(({ section, extract }) => {
|
|
94
|
-
const heading = `## ${section.heading}`;
|
|
95
|
-
const trail = renderCitationTrail(extract.citations || []);
|
|
96
|
-
const blocks = [heading, '', extract.markdown || '_(extractor returned no markdown)_'];
|
|
97
|
-
if (trail) blocks.push('', trail);
|
|
98
|
-
return blocks.join('\n');
|
|
99
|
-
}).join('\n\n');
|
|
100
|
-
|
|
101
|
-
const sourcesNote = sections
|
|
102
|
-
.flatMap(s => (s.extract.citations || []).map(c => {
|
|
103
|
-
const key = c.source === 'gcloud' || c.source === 'firestore-rest'
|
|
104
|
-
? `${c.source}:${c.anchor}@${c.sha || 'PENDING-M3'}`
|
|
105
|
-
: `${c.file}@${c.sha || 'PENDING-M2'}`;
|
|
106
|
-
return key;
|
|
107
|
-
}))
|
|
108
|
-
.filter((v, i, a) => a.indexOf(v) === i)
|
|
109
|
-
.join('; ');
|
|
110
|
-
const provenanceRow = `| ${regenTs} | ${mechanism} | ${regenBy} | sources: ${sourcesNote || '(none)'} |`;
|
|
111
|
-
|
|
112
|
-
const footer = FOOTER_TEMPLATE(provenanceRow);
|
|
113
|
-
|
|
114
|
-
return [header, body, '', footer].join('\n');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Extract just the citation trail comments from a rendered briefer document.
|
|
119
|
-
* Used by --check to compare citations (not prose) between canonical + regen.
|
|
120
|
-
*
|
|
121
|
-
* Per scope doc §2.2 §3: --check MUST diff §3 source-by-source against the
|
|
122
|
-
* canonical briefer with stricter tolerance — any line-range citation that no
|
|
123
|
-
* longer points at the documented code construct triggers non-zero exit even
|
|
124
|
-
* if the prose has not changed.
|
|
125
|
-
*/
|
|
126
|
-
export function extractCitationTrail(markdown) {
|
|
127
|
-
const trail = [];
|
|
128
|
-
const lines = (markdown || '').split(/\r?\n/);
|
|
129
|
-
for (const line of lines) {
|
|
130
|
-
const m = line.match(/^<!-- briefer-cite:\s*(.*?)\s*-->$/);
|
|
131
|
-
if (m) trail.push(m[1]);
|
|
132
|
-
}
|
|
133
|
-
return trail;
|
|
134
|
-
}
|