@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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `descix briefer` — main entry point.
|
|
3
|
+
*
|
|
4
|
+
* Per WS-DESCIX-BRIEFER-CLI scope doc §2 (Architecture / Option A):
|
|
5
|
+
*
|
|
6
|
+
* descix briefer [--env=dev|demo|prod] [--out=PATH] [--check]
|
|
7
|
+
*
|
|
8
|
+
* Reads the per-section data sources defined in scope doc §2.2, dispatches to
|
|
9
|
+
* the per-section extractors in ./sources/*.js, and emits a structured
|
|
10
|
+
* BrieferDoc object. The stitcher composes the markdown; --check mode diffs
|
|
11
|
+
* the regen against the canonical briefer file.
|
|
12
|
+
*
|
|
13
|
+
* HARD-FAIL philosophy (scope doc §2.3 + feedback_no-hardcoded-fallbacks):
|
|
14
|
+
* every extractor that can't find its source data throws BrieferExtractorError
|
|
15
|
+
* with a stable error code. No fallback strings. No try-catch-and-default.
|
|
16
|
+
*
|
|
17
|
+
* M1 SCOPE (this implementation):
|
|
18
|
+
* - CLI subcommand registration in bin/descix.js (separate edit).
|
|
19
|
+
* - This entry orchestrates 7 stub extractors.
|
|
20
|
+
* - Stitcher composes a real (non-empty) markdown with TODO placeholder bodies.
|
|
21
|
+
* - --check runs the full extraction, diffs against canonical, prints a
|
|
22
|
+
* unified-style diff, exits non-zero on drift (which is EXPECTED for M1 —
|
|
23
|
+
* stubs WILL differ from the manually-authored canonical briefer).
|
|
24
|
+
*
|
|
25
|
+
* M2+ SCOPE (NOT IMPLEMENTED HERE):
|
|
26
|
+
* - Real AST/regex parsing of source files.
|
|
27
|
+
* - Live gcloud probes.
|
|
28
|
+
* - Firestore Products counts via apifront.
|
|
29
|
+
* - MustKnow KB partition + auto-sync.
|
|
30
|
+
* - Bootstrap protocol edits (/cos skill, @bootstrap agent).
|
|
31
|
+
* - CI --check gate.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import fs from 'fs/promises';
|
|
35
|
+
import path from 'path';
|
|
36
|
+
import chalk from 'chalk';
|
|
37
|
+
import { fileURLToPath } from 'url';
|
|
38
|
+
|
|
39
|
+
import { BrieferExtractorError, BRIEFER_ERROR_CODES } from './errors.js';
|
|
40
|
+
import { stitchBriefer, extractCitationTrail } from './stitcher.js';
|
|
41
|
+
|
|
42
|
+
import * as identifiers from './sources/identifiers.js';
|
|
43
|
+
import * as environments from './sources/environments.js';
|
|
44
|
+
import * as routing from './sources/routing.js';
|
|
45
|
+
import * as microserviceDeploy from './sources/microservice-deploy.js';
|
|
46
|
+
import * as entitlements from './sources/entitlements.js';
|
|
47
|
+
import * as whatIsNot from './sources/what-is-not.js';
|
|
48
|
+
import * as canonicalSources from './sources/canonical-sources.js';
|
|
49
|
+
|
|
50
|
+
import { WorkspaceConfig } from '../../workspace-config.js';
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// Extractor dispatch table (scope doc §2.2 ordering)
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
export const EXTRACTORS = Object.freeze([
|
|
56
|
+
identifiers,
|
|
57
|
+
environments,
|
|
58
|
+
routing,
|
|
59
|
+
microserviceDeploy,
|
|
60
|
+
entitlements,
|
|
61
|
+
whatIsNot,
|
|
62
|
+
canonicalSources
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
export const VALID_ENVS = Object.freeze(['dev', 'demo', 'prod']);
|
|
66
|
+
|
|
67
|
+
// Default output path (scope doc §2.1). Resolved relative to workspace root
|
|
68
|
+
// via WorkspaceConfig.getWorkspaceRoot() — never a hardcoded absolute path.
|
|
69
|
+
export const DEFAULT_OUT_RELATIVE = 'DeSciX/V2_docs/architecture/platform-must-know-briefer.md';
|
|
70
|
+
|
|
71
|
+
const MECHANISM_TAG = 'descix briefer v1.0 (M2 — code-grounded extractors)';
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// Helpers
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export function resolveCliPaths() {
|
|
78
|
+
// Resolve the descix-cli root from this file's location, then walk up to
|
|
79
|
+
// DeSciX_Core and the parent repo. M2/M3 extractors that read source files
|
|
80
|
+
// will need these to anchor file reads.
|
|
81
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
const descixCliRoot = path.resolve(here, '../../..'); // .../descix-cli
|
|
83
|
+
const descixCoreRoot = path.resolve(descixCliRoot, '..'); // .../DeSciX_Core
|
|
84
|
+
const desciXRoot = path.resolve(descixCoreRoot, '..'); // .../DeSciX
|
|
85
|
+
const repoRoot = path.resolve(desciXRoot, '..'); // .../Unkamon
|
|
86
|
+
return { descixCliRoot, descixCoreRoot, desciXRoot, repoRoot };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveOutPath(cliOut, workspaceConfig, cliPaths, cwd) {
|
|
90
|
+
// Explicit override always wins.
|
|
91
|
+
if (cliOut) return path.resolve(cliOut);
|
|
92
|
+
|
|
93
|
+
// Default: workspace-root-relative path.
|
|
94
|
+
if (workspaceConfig) {
|
|
95
|
+
const wsRoot = workspaceConfig.getWorkspaceRoot();
|
|
96
|
+
return path.join(wsRoot, DEFAULT_OUT_RELATIVE);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// HARD-FAIL per feedback_no-hardcoded-fallbacks. The previous implementation
|
|
100
|
+
// derived a default from cliPaths.repoRoot (computed from the CLI file
|
|
101
|
+
// location), which silently produced a wrong path like
|
|
102
|
+
// "Unkamon/DeSciX/DeSciX/V2_docs/..." (double DeSciX/) when invoked from
|
|
103
|
+
// inside descix-cli/ with no workspace.json in cwd. No silent path-guessing.
|
|
104
|
+
throw new BrieferExtractorError({
|
|
105
|
+
code: BRIEFER_ERROR_CODES.WORKSPACE_NOT_FOUND,
|
|
106
|
+
section: 'briefer entry',
|
|
107
|
+
source: cwd || '(unknown cwd)',
|
|
108
|
+
expected: 'either .descix/workspace.json reachable by upward search, OR --out=<path>',
|
|
109
|
+
recovery: 'Run from your workspace root (where .descix/workspace.json lives), or pass --out=<path> explicitly.',
|
|
110
|
+
detail: `cliPaths.repoRoot was previously used as a silent fallback (resolved to ${cliPaths.repoRoot}); that fallback is removed.`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function resolveRegenBy() {
|
|
115
|
+
// Best signal we have without forcing an auth round-trip in M1. M2 will
|
|
116
|
+
// require auth (for apifront/Firestore calls) and can use the wallet email
|
|
117
|
+
// from apiClient.loadCredentials() — at that point this changes to a hard
|
|
118
|
+
// requirement. For M1 scaffolding we just tag the system user.
|
|
119
|
+
return process.env.USER || process.env.USERNAME || 'unknown';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Minimal unified-style diff (line-based). Avoids adding a new npm dependency
|
|
123
|
+
// for the M1 scaffold; M2 may swap in the `diff` package if richer output is
|
|
124
|
+
// needed (currently an unanswered question §9.1 in scope doc — Babel dep).
|
|
125
|
+
function simpleLineDiff(a, b, { aLabel = 'canonical', bLabel = 'regen' } = {}) {
|
|
126
|
+
const aLines = (a || '').split(/\r?\n/);
|
|
127
|
+
const bLines = (b || '').split(/\r?\n/);
|
|
128
|
+
const out = [`--- ${aLabel}`, `+++ ${bLabel}`];
|
|
129
|
+
const max = Math.max(aLines.length, bLines.length);
|
|
130
|
+
let inHunk = false;
|
|
131
|
+
for (let i = 0; i < max; i++) {
|
|
132
|
+
const av = aLines[i];
|
|
133
|
+
const bv = bLines[i];
|
|
134
|
+
if (av === bv) {
|
|
135
|
+
if (inHunk) { out.push(' ' + (av ?? '')); inHunk = false; }
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!inHunk) {
|
|
139
|
+
out.push(`@@ line ${i + 1} @@`);
|
|
140
|
+
inHunk = true;
|
|
141
|
+
}
|
|
142
|
+
if (av !== undefined) out.push(`-${av}`);
|
|
143
|
+
if (bv !== undefined) out.push(`+${bv}`);
|
|
144
|
+
}
|
|
145
|
+
return out.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
// BrieferDoc — the structured object returned by buildBrieferDoc()
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
/**
|
|
152
|
+
* @typedef {object} BrieferSectionResult
|
|
153
|
+
* @property {{number: number, heading: string, sourceFiles: string[]}} section
|
|
154
|
+
* @property {{markdown: string, citations: object[]}} extract
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @typedef {object} BrieferDoc
|
|
159
|
+
* @property {string} env
|
|
160
|
+
* @property {string} mechanism
|
|
161
|
+
* @property {string} regenBy
|
|
162
|
+
* @property {BrieferSectionResult[]} sections
|
|
163
|
+
* @property {string} markdown Final stitched markdown.
|
|
164
|
+
* @property {string[]} citationTrail Hidden HTML-comment trail (one per line).
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run all extractors and return the BrieferDoc.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} opts
|
|
171
|
+
* @param {string} opts.env
|
|
172
|
+
* @param {object} opts.cliPaths
|
|
173
|
+
* @param {object|null} [opts.apiClient] Optional in M1 (stubs don't need it).
|
|
174
|
+
* @param {object|null} [opts.gcloud] Optional in M1 (stubs don't need it).
|
|
175
|
+
* @param {string} opts.regenBy
|
|
176
|
+
* @param {string} [opts.mechanism]
|
|
177
|
+
* @returns {Promise<BrieferDoc>}
|
|
178
|
+
*/
|
|
179
|
+
export async function buildBrieferDoc({ env, cliPaths, apiClient = null, gcloud = null, regenBy, mechanism = MECHANISM_TAG }) {
|
|
180
|
+
const sections = [];
|
|
181
|
+
for (const extractor of EXTRACTORS) {
|
|
182
|
+
const result = await extractor.extract({ env, cliPaths, gcloud, apiClient });
|
|
183
|
+
sections.push({ section: extractor.SECTION, extract: result });
|
|
184
|
+
}
|
|
185
|
+
const markdown = stitchBriefer({ env, mechanism, regenBy, sections });
|
|
186
|
+
const citationTrail = extractCitationTrail(markdown);
|
|
187
|
+
return { env, mechanism, regenBy, sections, markdown, citationTrail };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
// Public CLI entry — invoked by bin/descix.js `briefer` subcommand
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {object} options Commander options object.
|
|
196
|
+
* @param {string} [options.env] dev|demo|prod (default: 'dev' for M1; M2 will hard-fail dev per scope doc §2.1).
|
|
197
|
+
* @param {string} [options.out] Output path override.
|
|
198
|
+
* @param {boolean} [options.check] Drift-detection mode: regen to memory, diff against canonical, exit non-zero on drift.
|
|
199
|
+
* @param {boolean} [options.verbose] Print per-source timing + extractor names.
|
|
200
|
+
*/
|
|
201
|
+
export async function runBriefer(options = {}) {
|
|
202
|
+
const env = (options.env || 'dev').toLowerCase();
|
|
203
|
+
if (!VALID_ENVS.includes(env)) {
|
|
204
|
+
console.error(chalk.red(`\n❌ Invalid --env=${options.env}. Must be one of: ${VALID_ENVS.join(', ')}\n`));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Per scope §2.1: --env=dev is rejected by the routing extractor specifically
|
|
209
|
+
// (DEV has no LB URL map). Other extractors accept dev because they read
|
|
210
|
+
// only source files (no gcloud probe required). The rejection lives in
|
|
211
|
+
// sources/routing.js; this entry point passes env through unchanged.
|
|
212
|
+
if (options.verbose) {
|
|
213
|
+
console.log(chalk.gray(`[briefer] env=${env}`));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const cliPaths = resolveCliPaths();
|
|
217
|
+
const cwd = process.cwd();
|
|
218
|
+
let workspaceConfig = null;
|
|
219
|
+
try {
|
|
220
|
+
workspaceConfig = await WorkspaceConfig.tryLoad(cwd);
|
|
221
|
+
} catch {
|
|
222
|
+
// tryLoad returns null on missing workspace; an exception here means the
|
|
223
|
+
// workspace.json file exists but is invalid. Hard-fail per philosophy —
|
|
224
|
+
// do NOT proceed with a guessed default workspace.
|
|
225
|
+
throw new BrieferExtractorError({
|
|
226
|
+
code: BRIEFER_ERROR_CODES.PARSE_FAIL,
|
|
227
|
+
section: 'briefer entry',
|
|
228
|
+
source: '.descix/workspace.json',
|
|
229
|
+
expected: 'valid v2.1 workspace JSON',
|
|
230
|
+
recovery: 'Fix the workspace.json or run `descix init` to regenerate.'
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const outPath = resolveOutPath(options.out, workspaceConfig, cliPaths, cwd);
|
|
235
|
+
const regenBy = await resolveRegenBy();
|
|
236
|
+
|
|
237
|
+
if (options.verbose) {
|
|
238
|
+
console.log(chalk.gray(`[briefer] outPath=${outPath}`));
|
|
239
|
+
console.log(chalk.gray(`[briefer] regenBy=${regenBy}`));
|
|
240
|
+
console.log(chalk.gray(`[briefer] extractors=${EXTRACTORS.length}`));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Build the briefer doc. Each extractor is run sequentially; M2 can
|
|
244
|
+
// parallelize if hot-path latency matters (it likely won't for 7 sources).
|
|
245
|
+
let doc;
|
|
246
|
+
try {
|
|
247
|
+
doc = await buildBrieferDoc({ env, cliPaths, regenBy });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (err instanceof BrieferExtractorError) {
|
|
250
|
+
console.error(chalk.red(`\n❌ Briefer extraction failed:\n`));
|
|
251
|
+
console.error(chalk.red(err.message));
|
|
252
|
+
console.error('');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.check) {
|
|
259
|
+
return runCheckMode({ doc, outPath, verbose: options.verbose });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Write mode — overwrite the canonical briefer file.
|
|
263
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
264
|
+
await fs.writeFile(outPath, doc.markdown, 'utf-8');
|
|
265
|
+
|
|
266
|
+
console.log(chalk.green(`\n✅ Briefer regenerated\n`));
|
|
267
|
+
console.log(chalk.cyan(` Env: ${env}`));
|
|
268
|
+
console.log(chalk.gray (` Mechanism: ${doc.mechanism}`));
|
|
269
|
+
console.log(chalk.gray (` By: ${regenBy}`));
|
|
270
|
+
console.log(chalk.gray (` Out: ${outPath}`));
|
|
271
|
+
console.log(chalk.gray (` Sections: ${doc.sections.length}`));
|
|
272
|
+
console.log(chalk.gray (` Citations: ${doc.citationTrail.length}\n`));
|
|
273
|
+
return doc;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function runCheckMode({ doc, outPath, verbose }) {
|
|
277
|
+
let canonical = null;
|
|
278
|
+
try {
|
|
279
|
+
canonical = await fs.readFile(outPath, 'utf-8');
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (err.code === 'ENOENT') {
|
|
282
|
+
console.error(chalk.red(`\n❌ --check failed: canonical briefer not found at ${outPath}\n`));
|
|
283
|
+
console.error(chalk.gray(' Run `descix briefer` (without --check) to create it.\n'));
|
|
284
|
+
process.exit(2);
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (canonical === doc.markdown) {
|
|
290
|
+
console.log(chalk.green(`\n✅ Briefer is in sync — no drift detected.\n`));
|
|
291
|
+
return { drift: false, doc };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Drift detected. Print a unified-style diff and exit non-zero.
|
|
295
|
+
// Per scope doc §2.2 §3, --check also compares the citation trail. For M1
|
|
296
|
+
// we report both prose-drift and citation-drift counts.
|
|
297
|
+
const canonicalTrail = extractCitationTrail(canonical);
|
|
298
|
+
const regenTrail = doc.citationTrail;
|
|
299
|
+
const trailChanged = (canonicalTrail.length !== regenTrail.length) ||
|
|
300
|
+
canonicalTrail.some((c, i) => c !== regenTrail[i]);
|
|
301
|
+
|
|
302
|
+
console.error(chalk.yellow(`\n⚠ [${BRIEFER_ERROR_CODES.DRIFT_DETECTED}] Briefer drift detected.\n`));
|
|
303
|
+
console.error(chalk.gray(` Canonical: ${outPath}`));
|
|
304
|
+
console.error(chalk.gray(` Regen length: ${doc.markdown.length} bytes`));
|
|
305
|
+
console.error(chalk.gray(` Canon length: ${canonical.length} bytes`));
|
|
306
|
+
console.error(chalk.gray(` Citation trail drift: ${trailChanged ? 'YES' : 'no'}\n`));
|
|
307
|
+
|
|
308
|
+
if (verbose) {
|
|
309
|
+
console.error(simpleLineDiff(canonical, doc.markdown, { aLabel: outPath, bLabel: 'regen (in-memory)' }));
|
|
310
|
+
console.error('');
|
|
311
|
+
} else {
|
|
312
|
+
console.error(chalk.gray(' (Run with --verbose for the full diff.)\n'));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// M2: drift means a source-of-truth file changed shape since last regen.
|
|
316
|
+
// Caller should regenerate (run `descix briefer` without --check) and commit
|
|
317
|
+
// the updated canonical briefer.
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §7 — Where to look for canonical answers extractor (M2 implementation).
|
|
3
|
+
*
|
|
4
|
+
* Per WS-DESCIX-BRIEFER-CLI scope doc §2.2 §7, this section is PARTIALLY
|
|
5
|
+
* DERIVED, PARTIALLY EDITORIAL:
|
|
6
|
+
* - "Question → Canonical source" rows verify each cited path resolves.
|
|
7
|
+
* - The "Do NOT trust" list is editorial cruft inventory, optionally read
|
|
8
|
+
* from a JSON sidecar (.briefer-cruft-inventory.json) — if absent, a
|
|
9
|
+
* minimal default list (the docs called out in the canonical briefer
|
|
10
|
+
* yesterday) is rendered.
|
|
11
|
+
*
|
|
12
|
+
* Files cited as canonical sources (all must exist at extract time):
|
|
13
|
+
* - provision-platform-lb.js (routing source)
|
|
14
|
+
* - serviceManifestManager.js (dispatch source)
|
|
15
|
+
* - entitlements/index.js, communityManagement.js, bootstrap.js (Products)
|
|
16
|
+
* - dev/gateway.js, createViteProxyConfig.js (local dev)
|
|
17
|
+
* - descix-cloud-core/src/config.js (bootstrap precedence)
|
|
18
|
+
* - platform-runtime-mental-model.md (env parallelism + cutover)
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'fs/promises';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import {
|
|
23
|
+
readSourceFile,
|
|
24
|
+
makeCitation,
|
|
25
|
+
resolveRepoPath
|
|
26
|
+
} from '../util/source-reader.js';
|
|
27
|
+
import { BrieferExtractorError, BRIEFER_ERROR_CODES } from '../errors.js';
|
|
28
|
+
|
|
29
|
+
export const SECTION = {
|
|
30
|
+
number: 7,
|
|
31
|
+
heading: '7. Where to look for canonical answers',
|
|
32
|
+
sourceFiles: [
|
|
33
|
+
'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js',
|
|
34
|
+
'DeSciX/DeSciX_Cloud/microservice/services/serviceManifestManager.js',
|
|
35
|
+
'DeSciX/DeSciX_Core/descix-platform-api/src/entitlements/index.js',
|
|
36
|
+
'DeSciX/DeSciX_Cloud/microservice/services/communityManagement.js',
|
|
37
|
+
'DeSciX/DeSciX_Cloud/microservice/admin/bootstrap.js',
|
|
38
|
+
'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/gateway.js',
|
|
39
|
+
'DeSciX/DeSciX_Core/descix-app-sdk/src/dev/createViteProxyConfig.js',
|
|
40
|
+
'DeSciX/DeSciX_Core/descix-cloud-core/src/config.js',
|
|
41
|
+
'DeSciX/V2_docs/architecture/platform-runtime-mental-model.md'
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const CRUFT_SIDECAR = 'DeSciX/V2_docs/architecture/.briefer-cruft-inventory.json';
|
|
46
|
+
|
|
47
|
+
export async function extract({ env, cliPaths } = {}) {
|
|
48
|
+
if (!env) {
|
|
49
|
+
throw new BrieferExtractorError({
|
|
50
|
+
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
51
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
52
|
+
source: 'extractor argument',
|
|
53
|
+
expected: 'env (dev|demo|prod) provided by caller',
|
|
54
|
+
recovery: 'Pass {env} from the briefer entry point.'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!cliPaths) {
|
|
58
|
+
throw new BrieferExtractorError({
|
|
59
|
+
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
60
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
61
|
+
source: 'extractor argument',
|
|
62
|
+
expected: 'cliPaths provided by caller',
|
|
63
|
+
recovery: 'Pass {cliPaths} from the briefer entry point.'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Verify each cited canonical source actually exists. HARD-FAIL with
|
|
68
|
+
// BRIEFER-SRC-NOT-FOUND on the FIRST missing file — surfaces drift early.
|
|
69
|
+
const citations = [];
|
|
70
|
+
for (const rel of SECTION.sourceFiles) {
|
|
71
|
+
const file = await readSourceFile({
|
|
72
|
+
cliPaths,
|
|
73
|
+
relPath: rel,
|
|
74
|
+
section: `§${SECTION.number} ${SECTION.heading}`
|
|
75
|
+
});
|
|
76
|
+
citations.push(makeCitation({
|
|
77
|
+
file: rel,
|
|
78
|
+
lines: `1-${file.lines.length}`,
|
|
79
|
+
anchor: 'canonical source',
|
|
80
|
+
fileLines: file.lines
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cruft sidecar — optional. If present, render the entries; if absent, render
|
|
85
|
+
// the default minimal cruft list from yesterday's briefer.
|
|
86
|
+
let cruftRows;
|
|
87
|
+
let cruftSource = null;
|
|
88
|
+
try {
|
|
89
|
+
const sidecarAbs = resolveRepoPath(cliPaths, CRUFT_SIDECAR);
|
|
90
|
+
const raw = await fs.readFile(sidecarAbs, 'utf-8');
|
|
91
|
+
const data = JSON.parse(raw);
|
|
92
|
+
cruftRows = (data.entries || []).map(e => `- ${e.path || e.file}${e.note ? ` — ${e.note}` : ''}`);
|
|
93
|
+
cruftSource = CRUFT_SIDECAR;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// Sidecar absent or malformed — fall back to the canonical-briefer's list
|
|
96
|
+
// for prose continuity. This is NOT a value-fallback (per
|
|
97
|
+
// feedback_no-hardcoded-fallbacks); it's the documented default rendering
|
|
98
|
+
// until the sidecar lands.
|
|
99
|
+
cruftRows = [
|
|
100
|
+
'- `unified-product-registry.md` routing section',
|
|
101
|
+
'- `cloud-lb-decentralized-routing.md` table without host-dim context',
|
|
102
|
+
'- Any cloud-KB chunk that claims runtime-Gateway / runtime-Firestore-lookup-for-routing'
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const markdown = [
|
|
107
|
+
`Trust order for any new architectural question:`,
|
|
108
|
+
``,
|
|
109
|
+
`1. **Code with \`file:line\` cite.** Always wins.`,
|
|
110
|
+
`2. \`DeSciX/V2_docs/architecture/platform-runtime-mental-model.md\` — closest to a code-grounded reference doc.`,
|
|
111
|
+
`3. \`DeSciX/CLAUDE.md\` — mostly right (note: any "load balancers look up by app_id" phrasing is misleading; the LB's host rules + path matchers do the routing, not a runtime lookup).`,
|
|
112
|
+
`4. \`DeSciX/V2_docs/architecture/split-stack-production-architecture.md\` — signal.`,
|
|
113
|
+
``,
|
|
114
|
+
`**Do NOT trust** (cruft inventory${cruftSource ? `, from \`${cruftSource}\`` : ', default list — install `.briefer-cruft-inventory.json` to override'}):`,
|
|
115
|
+
``,
|
|
116
|
+
cruftRows.join('\n'),
|
|
117
|
+
``,
|
|
118
|
+
`| Question | Canonical source (in this order) | Notes |`,
|
|
119
|
+
`|---|---|---|`,
|
|
120
|
+
`| How does prod LB route? | \`DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js\`; verify with \`gcloud compute url-maps describe descix-discord-app-lb\` | Code = signal |`,
|
|
121
|
+
`| How does the env command-broker dispatch external commands? | \`services/serviceManifestManager.js\` (\`proxyToExternalService\`) and \`services/apiFront.js\` | Code = signal |`,
|
|
122
|
+
`| What is Products for? | \`descix-platform-api/src/entitlements/index.js\`, \`services/communityManagement.js\`, \`admin/bootstrap.js\` | Code = signal |`,
|
|
123
|
+
`| How does local \`descix serve\` work? | \`descix-app-sdk/src/dev/gateway.js\` + \`createViteProxyConfig.js\` | Code = signal |`,
|
|
124
|
+
`| Bootstrap & config precedence? | \`descix-cloud-core/src/config.js\` | Code = signal |`,
|
|
125
|
+
`| Environment parallelism & PROD cutover? | \`platform-runtime-mental-model.md\` | Distilled architectural doc |`
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
return { markdown, citations };
|
|
129
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §5 — Entitlements & Products Firestore extractor (M2 implementation).
|
|
3
|
+
*
|
|
4
|
+
* Per WS-DESCIX-BRIEFER-CLI scope doc §2.2 §5 + M2 dispatch §6.7 extension:
|
|
5
|
+
* - Products schema fields + readers — cite hydrateCommunityIdFromProducts
|
|
6
|
+
* in @descix/platform-api/src/entitlements/index.js + the apiFront
|
|
7
|
+
* middleware that invokes it (communityManagement.js re-export site).
|
|
8
|
+
* - Writers — grep DeSciX_Cloud/microservice/admin/ for Products write
|
|
9
|
+
* patterns (bootstrap.js hydrate-env + confirm_site_deploy).
|
|
10
|
+
* - App-level config layer (§6.7 extension) — cite the resolver chain
|
|
11
|
+
* in geminiInteractions.js::resolveModelThinkingPair: request level → kb_model_override
|
|
12
|
+
* → default_app_model → levelConfig → DEFAULT_AI_MODEL.
|
|
13
|
+
*
|
|
14
|
+
* NO direct @google-cloud/firestore or @pinecone-database/pinecone imports
|
|
15
|
+
* (AC-7 anti-regression). All Firestore touches in the briefer code go
|
|
16
|
+
* through apifront or @descix/cloud-core wrappers.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
readSourceFile,
|
|
20
|
+
findInLines,
|
|
21
|
+
makeCitation,
|
|
22
|
+
grepFiles,
|
|
23
|
+
probeFirestoreRest
|
|
24
|
+
} from '../util/source-reader.js';
|
|
25
|
+
import { BrieferExtractorError, BRIEFER_ERROR_CODES } from '../errors.js';
|
|
26
|
+
// The (model, thinking) precedence chain in words, from its ONE owner. The briefer
|
|
27
|
+
// QUOTES the served contract rather than paraphrasing it — a briefer paraphrase of a
|
|
28
|
+
// chain is exactly how the old five-step list outlived the code it described.
|
|
29
|
+
import { MODEL_THINKING_CHAIN } from '@descix/platform-api/mcp-tools';
|
|
30
|
+
|
|
31
|
+
export const SECTION = {
|
|
32
|
+
number: 5,
|
|
33
|
+
heading: '5. Entitlements & Products Firestore',
|
|
34
|
+
sourceFiles: [
|
|
35
|
+
'DeSciX/DeSciX_Core/descix-platform-api/src/entitlements/index.js',
|
|
36
|
+
'DeSciX/DeSciX_Cloud/microservice/services/communityManagement.js',
|
|
37
|
+
'DeSciX/DeSciX_Cloud/microservice/admin/bootstrap.js',
|
|
38
|
+
'DeSciX/DeSciX_Cloud/microservice/services/geminiInteractions.js'
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ENTITLEMENTS_FILE = 'DeSciX/DeSciX_Core/descix-platform-api/src/entitlements/index.js';
|
|
43
|
+
const CM_FILE = 'DeSciX/DeSciX_Cloud/microservice/services/communityManagement.js';
|
|
44
|
+
const BOOTSTRAP_FILE = 'DeSciX/DeSciX_Cloud/microservice/admin/bootstrap.js';
|
|
45
|
+
const GEMINI_FILE = 'DeSciX/DeSciX_Cloud/microservice/services/geminiInteractions.js';
|
|
46
|
+
|
|
47
|
+
export async function extract({ env, cliPaths } = {}) {
|
|
48
|
+
if (!env) {
|
|
49
|
+
throw new BrieferExtractorError({
|
|
50
|
+
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
51
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
52
|
+
source: 'extractor argument',
|
|
53
|
+
expected: 'env (dev|demo|prod) provided by caller',
|
|
54
|
+
recovery: 'Pass {env} from the briefer entry point.'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!cliPaths) {
|
|
58
|
+
throw new BrieferExtractorError({
|
|
59
|
+
code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
|
|
60
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
61
|
+
source: 'extractor argument',
|
|
62
|
+
expected: 'cliPaths provided by caller',
|
|
63
|
+
recovery: 'Pass {cliPaths} from the briefer entry point.'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Source 1: entitlements/index.js — hydrateCommunityIdFromProducts ──
|
|
68
|
+
const ent = await readSourceFile({
|
|
69
|
+
cliPaths,
|
|
70
|
+
relPath: ENTITLEMENTS_FILE,
|
|
71
|
+
section: `§${SECTION.number} ${SECTION.heading}`
|
|
72
|
+
});
|
|
73
|
+
const hydrateMatch = findInLines({
|
|
74
|
+
lines: ent.lines,
|
|
75
|
+
regex: /export\s+async\s+function\s+hydrateCommunityIdFromProducts\b/,
|
|
76
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
77
|
+
source: `${ENTITLEMENTS_FILE} (hydrateCommunityIdFromProducts)`,
|
|
78
|
+
expected: 'export async function hydrateCommunityIdFromProducts(params)',
|
|
79
|
+
recovery: `Re-locate hydrateCommunityIdFromProducts in ${ENTITLEMENTS_FILE}.`
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── Source 2: communityManagement.js — re-export site ──
|
|
83
|
+
const cm = await readSourceFile({
|
|
84
|
+
cliPaths,
|
|
85
|
+
relPath: CM_FILE,
|
|
86
|
+
section: `§${SECTION.number} ${SECTION.heading}`
|
|
87
|
+
});
|
|
88
|
+
const reExportMatch = findInLines({
|
|
89
|
+
lines: cm.lines,
|
|
90
|
+
regex: /export\s*\{\s*hydrateCommunityIdFromProducts\s*\}/,
|
|
91
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
92
|
+
source: `${CM_FILE} (hydrate re-export)`,
|
|
93
|
+
expected: 'export { hydrateCommunityIdFromProducts }',
|
|
94
|
+
recovery: `Verify ${CM_FILE} still re-exports hydrateCommunityIdFromProducts.`
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Source 3: bootstrap.js — hydrate-env writer ──
|
|
98
|
+
const bootstrap = await readSourceFile({
|
|
99
|
+
cliPaths,
|
|
100
|
+
relPath: BOOTSTRAP_FILE,
|
|
101
|
+
section: `§${SECTION.number} ${SECTION.heading}`
|
|
102
|
+
});
|
|
103
|
+
const hydrateEnvMatch = findInLines({
|
|
104
|
+
lines: bootstrap.lines,
|
|
105
|
+
regex: /hydrate-env|hydrateEnv|hydrate_env/,
|
|
106
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
107
|
+
source: `${BOOTSTRAP_FILE} (hydrate-env writer)`,
|
|
108
|
+
expected: 'hydrate-env subcommand or hydrateEnv function declaration',
|
|
109
|
+
recovery: `Re-locate the hydrate-env writer in ${BOOTSTRAP_FILE}.`
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Source 4: geminiInteractions.js — the (model, thinking) pair owner (§6.7 link) ──
|
|
113
|
+
const gemini = await readSourceFile({
|
|
114
|
+
cliPaths,
|
|
115
|
+
relPath: GEMINI_FILE,
|
|
116
|
+
section: `§${SECTION.number} ${SECTION.heading}`
|
|
117
|
+
});
|
|
118
|
+
const resolverMatch = findInLines({
|
|
119
|
+
lines: gemini.lines,
|
|
120
|
+
regex: /export\s+function\s+resolveModelThinkingPair\b/,
|
|
121
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
122
|
+
source: `${GEMINI_FILE} (resolveModelThinkingPair)`,
|
|
123
|
+
expected: 'export function resolveModelThinkingPair(...)',
|
|
124
|
+
recovery: `Re-locate resolveModelThinkingPair in ${GEMINI_FILE}.`
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── Optional grep: count Products readers across services/ ──
|
|
128
|
+
let runtimeReadersList = '';
|
|
129
|
+
const readers = await grepFiles({
|
|
130
|
+
repoRoot: cliPaths.repoRoot,
|
|
131
|
+
relPaths: ['DeSciX/DeSciX_Cloud/microservice/services'],
|
|
132
|
+
regex: /Products\.doc\(|collection\(['"]Products['"]\)|getProductContext/,
|
|
133
|
+
excludePaths: new Set(['/node_modules/', '/.git/'])
|
|
134
|
+
});
|
|
135
|
+
if (readers.length > 0) {
|
|
136
|
+
const unique = [...new Set(readers.map(r => r.file))].slice(0, 8);
|
|
137
|
+
runtimeReadersList = unique.map(f => ` - \`${f}\``).join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── M3: live Firestore Products probe (HARD-FAIL on demo/prod, skipped on dev) ──
|
|
141
|
+
// Uses Firestore REST API via gcloud-derived access token. Per AC-7
|
|
142
|
+
// anti-regression: no @google-cloud/firestore import — this is a different
|
|
143
|
+
// API surface (REST + gcloud-CLI token), explicitly permitted.
|
|
144
|
+
const probeCitations = [];
|
|
145
|
+
let productsLiveNote = '';
|
|
146
|
+
if (env === 'dev') {
|
|
147
|
+
productsLiveNote = '\n_DEV: Firestore probe skipped (local dev — no cloud Firestore for the descix-dev database in this regen scope)._\n';
|
|
148
|
+
} else {
|
|
149
|
+
const productsProbe = await probeFirestoreRest({
|
|
150
|
+
dbPath: `descix-${env}/documents/Products`,
|
|
151
|
+
query: { pageSize: 200, 'mask.fieldPaths': 'app_id' },
|
|
152
|
+
env,
|
|
153
|
+
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
154
|
+
anchor: 'products-collection-list',
|
|
155
|
+
expected: `Firestore Products list for env=${env}`,
|
|
156
|
+
recovery: `Run 'gcloud auth login' and ensure the account has Firestore read scope. The Products collection cross-check is required for env=${env}.`
|
|
157
|
+
});
|
|
158
|
+
const docs = (productsProbe.json && productsProbe.json.documents) || [];
|
|
159
|
+
const names = docs
|
|
160
|
+
.map(d => (d.name || '').split('/').pop())
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.sort();
|
|
163
|
+
const sample = names.slice(0, 8);
|
|
164
|
+
productsLiveNote = [
|
|
165
|
+
'',
|
|
166
|
+
`**Live Products collection** (probed \`Firestore REST listDocuments\` against \`descix-${env}/documents/Products\`):`,
|
|
167
|
+
`- Docs returned: \`${names.length}\` (pageSize=200; further pages not enumerated this milestone)`,
|
|
168
|
+
`- Sample app_ids: ${sample.length > 0 ? sample.map(n => '\`' + n + '\`').join(', ') : '_(none)_'}${names.length > sample.length ? ` (+ ${names.length - sample.length} more)` : ''}`,
|
|
169
|
+
''
|
|
170
|
+
].join('\n');
|
|
171
|
+
probeCitations.push(productsProbe.citation);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Render markdown ──
|
|
175
|
+
const markdown = [
|
|
176
|
+
`\`Products/{app_id}\` in \`descix-{env}\` Firestore holds:`,
|
|
177
|
+
`- \`productPath\`: \`Community/{community_id}/Apps/{app_id}\` (operational path)`,
|
|
178
|
+
`- \`gcsPath\`: \`gs://descix-assets-public/{env}/{app_id}/\``,
|
|
179
|
+
`- \`ip_site_gcs_path_url\`: subdomain URL (set by \`confirm_site_deploy\` AFTER site deploy)`,
|
|
180
|
+
`- \`community_id\` (operational metadata)`,
|
|
181
|
+
`- \`type\`, \`price\`, \`metadata\``,
|
|
182
|
+
``,
|
|
183
|
+
`**Readers at runtime:** \`hydrateCommunityIdFromProducts()\` middleware (canonical at \`${ENTITLEMENTS_FILE}:${hydrateMatch.lineNumber}\`, re-exported at \`${CM_FILE}:${reExportMatch.lineNumber}\`), purchase / entitlement handlers, app-metadata handlers. **ALL AT THE APIFRONT-OR-DOWNSTREAM LAYER.** **NEVER AT THE LB.**`,
|
|
184
|
+
...(runtimeReadersList ? ['', `_Grep of \`services/\` for Products readers:_`, runtimeReadersList] : []),
|
|
185
|
+
``,
|
|
186
|
+
`**Writers:** \`${BOOTSTRAP_FILE}\` \`hydrate-env\` (\`:${hydrateEnvMatch.lineNumber}\`), \`confirm_site_deploy\`, migration scripts.`,
|
|
187
|
+
``,
|
|
188
|
+
`**Consumer at deploy time:** Products drives bootstrap; platform peer hosts are configured in \`provision-platform-lb.js\` (\`PLATFORM_PEER_APPS\`), not read from Products at deploy time.`,
|
|
189
|
+
``,
|
|
190
|
+
`### App-level config layer (§6.7 — added 2026-05-26)`,
|
|
191
|
+
``,
|
|
192
|
+
`App-level and KB-level config flows from Firestore (\`Community/{c}/Apps/{a}\` for \`default_app_model\`, \`Community/{c}/Apps/{a}/KnowledgeBases/{k}\` for \`kb_model_override\`) into \`resolveModelThinkingPair()\` at \`${GEMINI_FILE}:${resolverMatch.lineNumber}\` — the ONE owner of the (model, thinking) pair. Both fields are **opt-in** — null means the tier below applies.`,
|
|
193
|
+
``,
|
|
194
|
+
MODEL_THINKING_CHAIN,
|
|
195
|
+
``,
|
|
196
|
+
`\`app.default_app_model\` no longer shadows an intelligence_level named on the request: a level names a (model, thinking) PAIR and outranks it. It still outranks the KB's own configured level and the platform default. Apps that want per-level routing leave \`default_app_model = null/absent\`.`,
|
|
197
|
+
productsLiveNote
|
|
198
|
+
].join('\n');
|
|
199
|
+
|
|
200
|
+
const citations = [
|
|
201
|
+
makeCitation({ file: ENTITLEMENTS_FILE, lines: String(hydrateMatch.lineNumber), anchor: 'hydrateCommunityIdFromProducts (canonical)', fileLines: ent.lines }),
|
|
202
|
+
makeCitation({ file: CM_FILE, lines: String(reExportMatch.lineNumber), anchor: 'hydrate re-export', fileLines: cm.lines }),
|
|
203
|
+
makeCitation({ file: BOOTSTRAP_FILE, lines: String(hydrateEnvMatch.lineNumber), anchor: 'hydrate-env writer', fileLines: bootstrap.lines }),
|
|
204
|
+
makeCitation({ file: GEMINI_FILE, lines: String(resolverMatch.lineNumber), anchor: 'resolveModelThinkingPair (app-level config bridge)', fileLines: gemini.lines }),
|
|
205
|
+
...probeCitations
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
return { markdown, citations };
|
|
209
|
+
}
|