@descix/cli 1.0.0 → 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.
Files changed (103) hide show
  1. package/LICENSE.md +671 -0
  2. package/README.md +43 -4
  3. package/agent-assets/guides/README.md +2 -2
  4. package/agent-assets/guides/dynamic-site-pattern.md +29 -14
  5. package/agent-assets/guides/knowledge-only-pattern.md +5 -5
  6. package/agent-assets/guides/microservice-pattern.md +48 -4
  7. package/agent-assets/guides/static-site-pattern.md +8 -0
  8. package/agent-assets/instructions/app-asset-validation.md +5 -5
  9. package/agent-assets/instructions/onboarding-flow.md +4 -4
  10. package/agent-assets/instructions/standard-folder-structure.md +20 -11
  11. package/agent-assets/reference/app-creation.md +42 -20
  12. package/agent-assets/reference/architecture.md +24 -22
  13. package/agent-assets/reference/local-dev.md +230 -67
  14. package/agent-assets/reference/mcp-integration.md +9 -9
  15. package/agent-assets/reference/platform-api-consumer.md +244 -0
  16. package/agent-assets/reference/workspace-config.md +7 -7
  17. package/agent-assets/rules/descix_mcp.template.mdc +8 -8
  18. package/bin/descix.js +2559 -1435
  19. package/bin/kb-deny-lint.mjs +91 -0
  20. package/bin/mcp-server.js +175 -91
  21. package/lib/agent-files.js +37 -25
  22. package/lib/api-client.js +223 -56
  23. package/lib/auth-guard.js +13 -3
  24. package/lib/chat-session.js +187 -0
  25. package/lib/cli-version.js +59 -0
  26. package/lib/commands/airdrop.js +251 -0
  27. package/lib/commands/auth.js +254 -72
  28. package/lib/commands/briefer/errors.js +88 -0
  29. package/lib/commands/briefer/index.js +319 -0
  30. package/lib/commands/briefer/sources/canonical-sources.js +129 -0
  31. package/lib/commands/briefer/sources/entitlements.js +209 -0
  32. package/lib/commands/briefer/sources/environments.js +205 -0
  33. package/lib/commands/briefer/sources/identifiers.js +214 -0
  34. package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
  35. package/lib/commands/briefer/sources/routing.js +275 -0
  36. package/lib/commands/briefer/sources/what-is-not.js +223 -0
  37. package/lib/commands/briefer/stitcher.js +134 -0
  38. package/lib/commands/briefer/util/source-reader.js +502 -0
  39. package/lib/commands/config.js +210 -90
  40. package/lib/commands/corpus.js +978 -0
  41. package/lib/commands/credits.js +122 -0
  42. package/lib/commands/health.js +627 -0
  43. package/lib/commands/kb-list-render.js +32 -0
  44. package/lib/commands/kb.js +190 -413
  45. package/lib/commands/mcp.js +5 -5
  46. package/lib/commands/microservice-restart.js +359 -0
  47. package/lib/commands/model-config.js +313 -0
  48. package/lib/commands/retired-kb-sync.js +134 -0
  49. package/lib/commands/serve.js +26 -1
  50. package/lib/commands/status.js +23 -51
  51. package/lib/commands/update.js +36 -107
  52. package/lib/core/Chunker.js +491 -17
  53. package/lib/core/CorpusDenyLint.js +326 -0
  54. package/lib/core/CorpusWalker.js +357 -0
  55. package/lib/core/Hydrator.js +75 -29
  56. package/lib/core/ManifestLoader.js +533 -0
  57. package/lib/core/SiteWalker.js +218 -0
  58. package/lib/core/Syncer.js +163 -11
  59. package/lib/diagnostic-log.js +41 -0
  60. package/lib/environment-report.js +100 -0
  61. package/lib/global-config.js +4 -1
  62. package/lib/oauth-client.js +164 -0
  63. package/lib/origin.js +143 -0
  64. package/lib/service-api-client.js +95 -0
  65. package/lib/wallet-environment.js +49 -0
  66. package/lib/wallet-file.js +16 -2
  67. package/lib/wizard/setup.js +10 -9
  68. package/lib/workspace-config.js +380 -235
  69. package/lib/workspace-identity.js +92 -0
  70. package/lib/workspace-utils.js +41 -61
  71. package/package.json +29 -9
  72. package/templates/agent-claude.md +157 -9
  73. package/templates/agent-cline.md +68 -8
  74. package/templates/agent-copilot.md +68 -8
  75. package/templates/agent-cursor.md +68 -8
  76. package/templates/alignment-schema.json +20 -0
  77. package/templates/cloud-brain-hook.sh +20 -0
  78. package/templates/drive/README.md +1 -1
  79. package/templates/drive_templates/README.md +2 -2
  80. package/templates/scaffolds/README.md +6 -10
  81. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +30 -0
  82. package/templates/scaffolds/microservice/app.js +51 -10
  83. package/templates/scaffolds/microservice/app.yaml +2 -2
  84. package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
  85. package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
  86. package/templates/scaffolds/microservice/manifest.json +0 -1
  87. package/templates/scaffolds/microservice/package.json +3 -3
  88. package/templates/scaffolds/microservice/scripts/register.js +56 -41
  89. package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
  90. package/templates/scaffolds/microservice/services/utils.js +20 -238
  91. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
  92. package/SERVICE_README_sdk.md +0 -399
  93. package/descix-cli-1.0.0.tgz +0 -0
  94. package/lib/commands/app-wizard.js +0 -627
  95. package/lib/commands/folder.js +0 -291
  96. package/lib/core/PathContext.js +0 -335
  97. package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
  98. package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
  99. package/templates/scaffolds/site/README.md +0 -51
  100. package/templates/scaffolds/site/app.js +0 -22
  101. package/templates/scaffolds/site/index.html +0 -42
  102. package/templates/scaffolds/site/styles.css +0 -123
  103. package/test-hydration.js +0 -150
@@ -0,0 +1,205 @@
1
+ /**
2
+ * §2 — Environments extractor (M2 implementation).
3
+ *
4
+ * Per WS-DESCIX-BRIEFER-CLI scope doc §2.2 §2, this extractor reads:
5
+ * - DEV/DEMO/PROD parallelism — architectural fact distilled from
6
+ * V2_docs/architecture/platform-runtime-mental-model.md (cite the doc).
7
+ * - ENV_CONFIG block from provision-platform-lb.js:32-47.
8
+ * - DEPLOY_ENV-bound-once hard-fail from descix-cloud-core/src/config.js
9
+ * (locate dynamically; canonical briefer line range is allowed to drift).
10
+ * - Secret Manager config name pattern from DeSciX_Cloud/CLAUDE.md (cite the
11
+ * doc since the values are env-keyed and live in env-vars/secrets, not
12
+ * code) — locate the "Secret Manager — Per-Environment Isolation" table.
13
+ *
14
+ * Optional gcloud verification: list Cloud Function names matching `apiFront-http`
15
+ * and Firestore database names matching `descix-{env}` / `descix`.
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: 2,
28
+ heading: '2. Environments',
29
+ sourceFiles: [
30
+ 'DeSciX/V2_docs/architecture/platform-runtime-mental-model.md',
31
+ 'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js',
32
+ 'DeSciX/DeSciX_Core/descix-cloud-core/src/config.js',
33
+ 'DeSciX/DeSciX_Cloud/CLAUDE.md'
34
+ ]
35
+ };
36
+
37
+ const RUNTIME_MM = 'DeSciX/V2_docs/architecture/platform-runtime-mental-model.md';
38
+ const MESH_FILE = 'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js';
39
+ const CONFIG_FILE = 'DeSciX/DeSciX_Core/descix-cloud-core/src/config.js';
40
+ const CLOUD_CLAUDE = 'DeSciX/DeSciX_Cloud/CLAUDE.md';
41
+
42
+ export async function extract({ env, cliPaths } = {}) {
43
+ if (!env) {
44
+ throw new BrieferExtractorError({
45
+ code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
46
+ section: `§${SECTION.number} ${SECTION.heading}`,
47
+ source: 'extractor argument',
48
+ expected: 'env (dev|demo|prod) provided by caller',
49
+ recovery: 'Pass {env} from the briefer entry point.'
50
+ });
51
+ }
52
+ if (!cliPaths) {
53
+ throw new BrieferExtractorError({
54
+ code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
55
+ section: `§${SECTION.number} ${SECTION.heading}`,
56
+ source: 'extractor argument',
57
+ expected: 'cliPaths provided by caller',
58
+ recovery: 'Pass {cliPaths} from the briefer entry point.'
59
+ });
60
+ }
61
+
62
+ // ── Source 1: ENV_CONFIG in provision-platform-lb.js ──
63
+ const mesh = await readSourceFile({
64
+ cliPaths,
65
+ relPath: MESH_FILE,
66
+ section: `§${SECTION.number} ${SECTION.heading}`
67
+ });
68
+ const envConfigMatch = findInLines({
69
+ lines: mesh.lines,
70
+ regex: /const\s+ENV_CONFIG\s*=\s*\{/,
71
+ section: `§${SECTION.number} ${SECTION.heading}`,
72
+ source: `${MESH_FILE} (ENV_CONFIG)`,
73
+ expected: 'const ENV_CONFIG = { prod: {...}, demo: {...}, dev: {...} }',
74
+ recovery: `Re-locate the ENV_CONFIG construct in ${MESH_FILE}.`,
75
+ expectedRange: [20, 60]
76
+ });
77
+ const envConfigEnd = findObjectEnd(mesh.lines, envConfigMatch.lineNumber);
78
+ const envConfigBlock = sliceRange(mesh.lines, envConfigMatch.lineNumber, envConfigEnd);
79
+
80
+ // ── Source 2: config.js — DEPLOY_ENV-bound-once hard-fail ──
81
+ const config = await readSourceFile({
82
+ cliPaths,
83
+ relPath: CONFIG_FILE,
84
+ section: `§${SECTION.number} ${SECTION.heading}`
85
+ });
86
+ const deployEnvBoundMatch = findInLines({
87
+ lines: config.lines,
88
+ regex: /FATAL:\s*DEPLOY_ENV\s+not\s+set/,
89
+ section: `§${SECTION.number} ${SECTION.heading}`,
90
+ source: `${CONFIG_FILE} (DEPLOY_ENV bound-once hard-fail)`,
91
+ expected: 'throw with message starting "[CloudConfig] FATAL: DEPLOY_ENV not set"',
92
+ recovery: `Re-locate the DEPLOY_ENV hard-fail in ${CONFIG_FILE}.`
93
+ });
94
+
95
+ // ── Source 3: runtime-mental-model.md doc cite (distilled architectural fact) ──
96
+ // We just verify the doc exists + record SHA of the canonical range. The doc
97
+ // is the source for "DEV/DEMO/PROD are fully parallel" because that's an
98
+ // assertion spread across many code paths — too noisy to grep.
99
+ const rmm = await readSourceFile({
100
+ cliPaths,
101
+ relPath: RUNTIME_MM,
102
+ section: `§${SECTION.number} ${SECTION.heading}`
103
+ });
104
+
105
+ // ── Source 4: Cloud CLAUDE.md Secret Manager table ──
106
+ const claude = await readSourceFile({
107
+ cliPaths,
108
+ relPath: CLOUD_CLAUDE,
109
+ section: `§${SECTION.number} ${SECTION.heading}`
110
+ });
111
+ const secretTableMatch = findInLines({
112
+ lines: claude.lines,
113
+ regex: /descix_config_demo/,
114
+ section: `§${SECTION.number} ${SECTION.heading}`,
115
+ source: `${CLOUD_CLAUDE} (Secret Manager Per-Environment Isolation table)`,
116
+ expected: 'descix_config_demo row in the per-env secret table',
117
+ recovery: `Re-locate the Secret Manager isolation table in ${CLOUD_CLAUDE}.`
118
+ });
119
+
120
+ // ── M3: live gcloud probes (HARD-FAIL on demo/prod, skipped on dev) ──
121
+ const probeCitations = [];
122
+ let gcloudCfNote = '';
123
+ let gcloudFsNote = '';
124
+
125
+ if (env === 'dev') {
126
+ // DEV is local. Probe is intentionally skipped; emit a stanza so the
127
+ // operator knows live state was not verified for this regen.
128
+ gcloudCfNote = '\n_DEV: gcloud probes skipped (local dev environment — no Cloud Functions or Firestore to probe)._\n';
129
+ } else {
130
+ const cfProbe = await probeGcloudJson({
131
+ command: ['functions', 'list', '--v2', '--format=json'],
132
+ env,
133
+ section: `§${SECTION.number} ${SECTION.heading}`,
134
+ anchor: 'cloud-functions-list',
135
+ expected: 'JSON array of gen2 Cloud Functions for project descix',
136
+ recovery: `Run 'gcloud auth login' and 'gcloud config set project descix'. The Cloud Functions list cross-check is required for env=${env}.`
137
+ });
138
+ const apiFrontCfs = (cfProbe.json || [])
139
+ .filter(f => /apiFront-http/.test(f.name || ''))
140
+ .map(f => (f.name || '').split('/').pop());
141
+ gcloudCfNote = `\n_Live Cloud Functions matching \`apiFront-http\` (probed \`${cfProbe.citation.probe}\`, env=\`${env}\`):_ ${apiFrontCfs.length > 0 ? apiFrontCfs.map(n => '\`' + n + '\`').join(', ') : '_(none found)_'}\n`;
142
+ probeCitations.push(cfProbe.citation);
143
+
144
+ const fsProbe = await probeGcloudJson({
145
+ command: ['firestore', 'databases', 'list', '--format=json'],
146
+ env,
147
+ section: `§${SECTION.number} ${SECTION.heading}`,
148
+ anchor: 'firestore-databases-list',
149
+ expected: 'JSON array of Firestore databases for project descix',
150
+ recovery: `Run 'gcloud auth login' and 'gcloud config set project descix'. The Firestore databases cross-check is required for env=${env}.`
151
+ });
152
+ const dbs = (fsProbe.json || [])
153
+ .map(d => (d.name || '').split('/').pop())
154
+ .filter(n => /^descix/.test(n));
155
+ gcloudFsNote = `\n_Live Firestore databases matching \`descix*\` (probed \`${fsProbe.citation.probe}\`, env=\`${env}\`):_ ${dbs.length > 0 ? dbs.map(n => '\`' + n + '\`').join(', ') : '_(none found)_'}\n`;
156
+ probeCitations.push(fsProbe.citation);
157
+ }
158
+
159
+ // ── Render markdown ──
160
+ const markdown = [
161
+ `DEV / DEMO / PROD are **fully parallel cloud deployments.** They co-exist; PROD does not replace DEMO. — \`${RUNTIME_MM}\``,
162
+ ``,
163
+ `Per env, isolated:`,
164
+ `- Cloud Function: \`apiFront-http-{env}\` (and \`apiFront-http\` for PROD)`,
165
+ `- Firestore DB: \`descix-{env}\` (and \`descix\` for PROD)`,
166
+ `- Secret Manager config: \`descix_config_{env}\` (DEV/DEMO) or \`descix_config\` with version aliases (PROD/PREVIEW) — \`${CLOUD_CLAUDE}:${secretTableMatch.lineNumber}\``,
167
+ `- GCS bucket prefix: \`{env}/\``,
168
+ `- LB host suffix: \`.{env}.descix.net\` for DEV/DEMO, \`.descix.net\` for PROD`,
169
+ ``,
170
+ `ENV_CONFIG (the deploy-time env table — \`${MESH_FILE}:${envConfigMatch.lineNumber}-${envConfigEnd}\`):`,
171
+ ``,
172
+ '```js',
173
+ envConfigBlock,
174
+ '```',
175
+ ``,
176
+ `One process serves exactly one env, determined by \`DEPLOY_ENV\`. The cloud-core bootstrap **hard-fails** at construct time if \`DEPLOY_ENV\` is unset — there is no default. — \`${CONFIG_FILE}:${deployEnvBoundMatch.lineNumber}\``,
177
+ gcloudCfNote,
178
+ gcloudFsNote
179
+ ].join('\n');
180
+
181
+ const citations = [
182
+ makeCitation({ file: MESH_FILE, lines: `${envConfigMatch.lineNumber}-${envConfigEnd}`, anchor: 'ENV_CONFIG', fileLines: mesh.lines }),
183
+ makeCitation({ file: CONFIG_FILE, lines: String(deployEnvBoundMatch.lineNumber), anchor: 'DEPLOY_ENV bound-once', fileLines: config.lines }),
184
+ makeCitation({ file: RUNTIME_MM, lines: `1-${rmm.lines.length}`, anchor: 'DEV/DEMO/PROD parallelism', fileLines: rmm.lines }),
185
+ makeCitation({ file: CLOUD_CLAUDE, lines: String(secretTableMatch.lineNumber), anchor: 'Secret Manager per-env isolation', fileLines: claude.lines }),
186
+ ...probeCitations
187
+ ];
188
+
189
+ return { markdown, citations };
190
+ }
191
+
192
+ function findObjectEnd(lines, startLine) {
193
+ const MAX = Math.min(lines.length, startLine + 50);
194
+ let depth = 0;
195
+ for (let i = startLine - 1; i < MAX; i++) {
196
+ for (const ch of lines[i]) {
197
+ if (ch === '{') depth++;
198
+ else if (ch === '}') {
199
+ depth--;
200
+ if (depth === 0) return i + 1;
201
+ }
202
+ }
203
+ }
204
+ return Math.min(lines.length, startLine + 30);
205
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * §1 — Identifiers & invariants extractor (M2 implementation).
3
+ *
4
+ * Per WS-DESCIX-BRIEFER-CLI scope doc §2.2 §1, this extractor reads:
5
+ * - Domain pattern from provision-platform-lb.js:120 (host construction)
6
+ * - GCS path pattern from provision-platform-lb.js:123 (pathPrefix)
7
+ * - product_id === app_id invariant from hydrateUtils.js
8
+ * - Agentic-app / communities-as-apps invariant from the BEAST CEO record
9
+ * `apps/unk-beast/kb/Org/agentic-app-invariant-2026-05-26.jsonl`
10
+ * - the AI (model, thinking) precedence chain from geminiInteractions.js::resolveModelThinkingPair()
11
+ *
12
+ * Extraction method: regex+checksum-of-surrounding-lines (NOT AST).
13
+ * Per scope §9.1 third-order question, decision documented 2026-05-26: regex
14
+ * keeps the CLI dep-light; AST may be added per-extractor later if needed.
15
+ *
16
+ * HARD-FAIL behavior:
17
+ * - Missing source file → BRIEFER-SRC-NOT-FOUND
18
+ * - Regex pattern doesn't match → BRIEFER-SRC-NOT-FOUND
19
+ * - Pattern matches outside expected line range → BRIEFER-PARSE-FAIL
20
+ * - BEAST agentic-app record missing → BRIEFER-SRC-NOT-FOUND (a hard
21
+ * invariant — this is the CEO-broadcast canonical record)
22
+ */
23
+ import {
24
+ readSourceFile,
25
+ findInLines,
26
+ makeCitation,
27
+ sliceRange
28
+ } from '../util/source-reader.js';
29
+ import { BrieferExtractorError, BRIEFER_ERROR_CODES } from '../errors.js';
30
+ // The (model, thinking) precedence chain in words, from its ONE owner. The briefer
31
+ // QUOTES the served contract rather than paraphrasing it — a briefer paraphrase of a
32
+ // chain is exactly how the old five-step list outlived the code it described.
33
+ import { MODEL_THINKING_CHAIN } from '@descix/platform-api/mcp-tools';
34
+
35
+ export const SECTION = {
36
+ number: 1,
37
+ heading: '1. Identifiers & invariants',
38
+ sourceFiles: [
39
+ 'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js',
40
+ 'DeSciX/DeSciX_Core/descix-platform-api/src/entitlements/index.js',
41
+ 'DeSciX/DeSciX_Cloud/microservice/services/geminiInteractions.js',
42
+ 'apps/unk-beast/kb/Org/agentic-app-invariant-2026-05-26.jsonl'
43
+ ]
44
+ };
45
+
46
+ const MESH_FILE = 'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/provision-platform-lb.js';
47
+ const HYDRATE_FILE = 'DeSciX/DeSciX_Core/descix-platform-api/src/entitlements/index.js';
48
+ const GEMINI_FILE = 'DeSciX/DeSciX_Cloud/microservice/services/geminiInteractions.js';
49
+ const AGENTIC_RECORD = 'apps/unk-beast/kb/Org/agentic-app-invariant-2026-05-26.jsonl';
50
+
51
+ export async function extract({ env, cliPaths } = {}) {
52
+ if (!env) {
53
+ throw new BrieferExtractorError({
54
+ code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
55
+ section: `§${SECTION.number} ${SECTION.heading}`,
56
+ source: 'extractor argument',
57
+ expected: 'env (dev|demo|prod) provided by caller',
58
+ recovery: 'Pass {env} from the briefer entry point.'
59
+ });
60
+ }
61
+ if (!cliPaths) {
62
+ throw new BrieferExtractorError({
63
+ code: BRIEFER_ERROR_CODES.NOT_IMPLEMENTED,
64
+ section: `§${SECTION.number} ${SECTION.heading}`,
65
+ source: 'extractor argument',
66
+ expected: 'cliPaths provided by caller',
67
+ recovery: 'Pass {cliPaths} from the briefer entry point.'
68
+ });
69
+ }
70
+
71
+ // ── Source 1: provision-platform-lb.js — domain + gcsPath patterns ──
72
+ const mesh = await readSourceFile({
73
+ cliPaths,
74
+ relPath: MESH_FILE,
75
+ section: `§${SECTION.number} ${SECTION.heading}`
76
+ });
77
+
78
+ // Host pattern: canonical line shifted to L214 after WS-DESCIX-SINGLETON-PROVISIONER
79
+ // (CEO-D-2026-05-26-SINGLETON-PROVISIONER-UNIFIED, 2026-05-26) added the
80
+ // SINGLETON_APPS branch + buildSingletonMatcher helper near top of file. The
81
+ // line is the canonical UNIFORM host formula (which a singleton override then
82
+ // shadows via `effectiveHost` for daita) — briefer extractor still keys on
83
+ // this exact construct as the platform routing invariant. Range expanded to
84
+ // 100-350 to tolerate NEG helpers + matcher builders in provision-platform-lb.js.
85
+ const hostMatch = findInLines({
86
+ lines: mesh.lines,
87
+ regex: /const\s+host\s*=\s*env\s*===\s*['"]prod['"]\s*\?/,
88
+ section: `§${SECTION.number} ${SECTION.heading}`,
89
+ source: `${MESH_FILE}:214 (uniform host pattern; CEO-D-2026-05-26-LB-ROUTING-UNIFORM)`,
90
+ expected: `const host = env === 'prod' ? ... : ... construct`,
91
+ recovery: `Re-locate the host construct in ${MESH_FILE} and update sources/identifiers.js.`,
92
+ expectedRange: [100, 350]
93
+ });
94
+
95
+ // gcsPath pattern: canonical line shifted to L223 after WS-DESCIX-SINGLETON-PROVISIONER.
96
+ // This is the non-singleton (uniform) pathPrefix; the singleton override
97
+ // (pathPrefixSingletonOverride) is a few lines below. Range matches hostMatch.
98
+ const gcsMatch = findInLines({
99
+ lines: mesh.lines,
100
+ regex: /const\s+pathPrefix\s*=\s*`\/\$\{env\}\/\$\{app\}\/site`/,
101
+ section: `§${SECTION.number} ${SECTION.heading}`,
102
+ source: `${MESH_FILE}:223 (uniform gcsPath pattern; singleton override at ~L228)`,
103
+ expected: 'const pathPrefix = `/${env}/${app}/site` (template literal)',
104
+ recovery: `Re-locate the gcsPath construct in ${MESH_FILE}.`,
105
+ expectedRange: [100, 350]
106
+ });
107
+
108
+ // ── Source 2: hydrateUtils.js — product_id===app_id invariant ──
109
+ const hydrate = await readSourceFile({
110
+ cliPaths,
111
+ relPath: HYDRATE_FILE,
112
+ section: `§${SECTION.number} ${SECTION.heading}`
113
+ });
114
+
115
+ const hydrateMatch = findInLines({
116
+ lines: hydrate.lines,
117
+ regex: /hydrateCommunityIdFromProducts/,
118
+ section: `§${SECTION.number} ${SECTION.heading}`,
119
+ source: `${HYDRATE_FILE} (hydrateCommunityIdFromProducts definition)`,
120
+ expected: 'hydrateCommunityIdFromProducts function declaration',
121
+ recovery: `Verify hydrateCommunityIdFromProducts is still exported from ${HYDRATE_FILE}.`
122
+ });
123
+
124
+ // ── Source 3: geminiInteractions.js — the (model, thinking) pair owner ──
125
+ const gemini = await readSourceFile({
126
+ cliPaths,
127
+ relPath: GEMINI_FILE,
128
+ section: `§${SECTION.number} ${SECTION.heading}`
129
+ });
130
+
131
+ const resolverMatch = findInLines({
132
+ lines: gemini.lines,
133
+ regex: /export\s+function\s+resolveModelThinkingPair\b/,
134
+ section: `§${SECTION.number} ${SECTION.heading}`,
135
+ source: `${GEMINI_FILE} (resolveModelThinkingPair)`,
136
+ expected: 'export function resolveModelThinkingPair(...)',
137
+ recovery: `resolveModelThinkingPair has been moved or renamed. Re-locate and update the extractor.`
138
+ });
139
+ // Capture the resolver body — from the export to the next closing brace at
140
+ // column 0. We'll scan forward a bounded window and slice for the briefer.
141
+ const resolverEnd = findResolverEnd(gemini.lines, resolverMatch.lineNumber);
142
+ const resolverBody = sliceRange(gemini.lines, resolverMatch.lineNumber, resolverEnd);
143
+
144
+ // ── Source 4: BEAST CEO record — agentic-app invariant ──
145
+ const beast = await readSourceFile({
146
+ cliPaths,
147
+ relPath: AGENTIC_RECORD,
148
+ section: `§${SECTION.number} ${SECTION.heading}`
149
+ });
150
+ // The record is JSONL; the canonical claim is one of the question/answer
151
+ // chunks. We don't parse the JSONL — we just verify the record EXISTS and
152
+ // SHA the whole file for citation purposes. The CEO broadcast title is the
153
+ // contract; the briefer prose embeds it verbatim.
154
+ const beastSha = makeCitation({
155
+ file: AGENTIC_RECORD,
156
+ lines: `1-${beast.lines.length}`,
157
+ anchor: 'AGENTIC-APP-INHERITANCE-INVARIANT-2026-05-26',
158
+ fileLines: beast.lines
159
+ }).sha;
160
+
161
+ // ── Render the briefer markdown ──
162
+ const hostLine = mesh.lines[hostMatch.lineNumber - 1].trim();
163
+ const gcsLine = mesh.lines[gcsMatch.lineNumber - 1].trim();
164
+
165
+ const markdown = [
166
+ `- **Apps are the central unit. Everything is an agentic app.** Communities are themselves agentic apps that share IP with other apps in the same community. The platform is fundamentally an agentic-app store, not a generic SaaS platform with apps bolted on. — \`${AGENTIC_RECORD}\` (CEO broadcast \`AGENTIC-APP-INHERITANCE-INVARIANT-2026-05-26\`, sha=\`${beastSha}\`)`,
167
+ `- \`app_id\` is a globally unique slug (e.g., \`daita\`, \`powch\`, \`egpt\`). Same value across DEV, DEMO, PROD. Never contains the environment.`,
168
+ `- \`community_id\` is operational metadata stored INSIDE \`Products/{app_id}\`. Never parsed from URL. Never trusted from client.`,
169
+ `- \`product_id === app_id\` — the same opaque slug. Server re-derives \`community_id\` from \`Products/{app_id}\` via \`hydrateCommunityIdFromProducts()\`. — \`${HYDRATE_FILE}:${hydrateMatch.lineNumber}\``,
170
+ `- Domain pattern: \`{app_id}.{env}.descix.net\` for DEV/DEMO; \`{app_id}.descix.net\` for PROD. — \`${MESH_FILE}:${hostMatch.lineNumber}\``,
171
+ ' ```js',
172
+ ` ${hostLine}`,
173
+ ' ```',
174
+ `- GCS path: \`{env}/{app_id}/site/\`. Flat — no community in the path. — \`${MESH_FILE}:${gcsMatch.lineNumber}\``,
175
+ ' ```js',
176
+ ` ${gcsLine}`,
177
+ ' ```',
178
+ `- Wildcard TLS cert: \`*.descix.net\` plus per-env wildcards. ONE cert. No per-app cert provisioning.`,
179
+ `- **AI (model, thinking) precedence.** ${MODEL_THINKING_CHAIN} Owner: \`resolveModelThinkingPair()\` in \`${GEMINI_FILE}:${resolverMatch.lineNumber}\` — the ONE resolver; the model chain and the thinking chain used to be separate and could assemble a pair no config declared.`,
180
+ ' ```js',
181
+ indentBlock(resolverBody, ' '),
182
+ ' ```'
183
+ ].join('\n');
184
+
185
+ const citations = [
186
+ makeCitation({ file: MESH_FILE, lines: String(hostMatch.lineNumber), anchor: 'host pattern', fileLines: mesh.lines }),
187
+ makeCitation({ file: MESH_FILE, lines: String(gcsMatch.lineNumber), anchor: 'gcsPath pattern', fileLines: mesh.lines }),
188
+ makeCitation({ file: HYDRATE_FILE, lines: String(hydrateMatch.lineNumber), anchor: 'hydrateCommunityIdFromProducts', fileLines: hydrate.lines }),
189
+ makeCitation({ file: GEMINI_FILE, lines: `${resolverMatch.lineNumber}-${resolverEnd}`, anchor: 'resolveModelThinkingPair', fileLines: gemini.lines }),
190
+ { file: AGENTIC_RECORD, lines: `1-${beast.lines.length}`, anchor: 'AGENTIC-APP-INHERITANCE-INVARIANT-2026-05-26', sha: beastSha }
191
+ ];
192
+
193
+ return { markdown, citations };
194
+ }
195
+
196
+ /**
197
+ * Walk forward from the `export function resolveModelThinkingPair` line until we hit
198
+ * a closing brace at column 0. Bounded scan window of 80 lines so a runaway
199
+ * regex on a refactored file doesn't drag the whole file into the briefer.
200
+ */
201
+ function findResolverEnd(lines, startLine) {
202
+ const MAX = Math.min(lines.length, startLine + 80);
203
+ for (let i = startLine; i < MAX; i++) {
204
+ if (lines[i] === '}') return i + 1;
205
+ }
206
+ // If we don't find a clean close, fall back to startLine+30 — we want some
207
+ // body in the briefer even if the resolver was reformatted; --check will
208
+ // catch the SHA change.
209
+ return Math.min(lines.length, startLine + 30);
210
+ }
211
+
212
+ function indentBlock(block, indent) {
213
+ return block.split('\n').map(l => indent + l).join('\n');
214
+ }