house-lights 1.4.0 → 1.5.0

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.
@@ -9,6 +9,11 @@
9
9
  // node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]
10
10
  // what was filled in the workbook (Declared, the Decision column): listed with a key each,
11
11
  // and with --take written to the cast file
12
+ // node build.mjs <agents-folder> --declared <table.csv|.xlsx> [--declared <another>] [--agent-column <title>] [--take <key,…|all>]
13
+ // what a spreadsheet says about the agents (the team's, the admin center's inventory): each
14
+ // column becomes a declared fact, listed with a key, and with --take written to the cast file
15
+ // … --telemetry <export.csv|.xlsx> [--telemetry-days <n>] what fired, from Application Insights (references/telemetry.md):
16
+ // the events in the YAML that did not fire, and who has no line at all
12
17
  //
13
18
  // With no --html/--xlsx/--lesson it makes the map. Files go in --out (or beside the folder), named by the folder:
14
19
  // <id>.html, <id>.xlsx, <id>.lesson.html (a chalk-talk lesson), and <id>.cast.yaml, kept up to date (read again on every
@@ -20,23 +25,26 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
20
25
  import { basename, dirname, join, resolve } from 'node:path'
21
26
  import { fileURLToPath } from 'node:url'
22
27
  import { runPage } from './browser.mjs'
23
- import { agentsIn, applyChanges, compare, DEFAULT_PROFILE, extractAgent, fromSheets, knownKinds, labelOf, lessonOf, problemText, readCast, readProfile, reviewCast, validateScene, workbookFile, workbookOf, writeCast } from './engine.mjs'
28
+ import { agentsIn, applyChanges, compare, declaredFrom, DEFAULT_PROFILE, extractAgent, firedFrom, fromSheets, mergeDeclared, knownKinds, labelOf, lessonOf, problemText, readCast, readCsv, readProfile, reviewCast, validateScene, workbookFile, workbookOf, writeCast } from './engine.mjs'
24
29
  import { readXlsx } from './read-xlsx.mjs'
25
30
  import { fill, inline } from './page.mjs'
26
31
 
27
32
  const here = dirname(fileURLToPath(import.meta.url))
28
33
  const argv = process.argv.slice(2)
29
- const valued = new Set(['--lang', '--out', '--profile', '--from', '--take'])
34
+ const valued = new Set(['--lang', '--out', '--profile', '--from', '--take', '--declared', '--agent-column', '--telemetry', '--telemetry-days'])
30
35
  const flags = new Set(['--check', '--html', '--xlsx', '--lesson', '--all', '--facts', ...valued])
31
36
  const option = (name) => {
32
37
  const at = argv.indexOf(name)
33
38
  return at >= 0 ? argv[at + 1] : undefined
34
39
  }
40
+ const options = (name) => argv.flatMap((a, i) => (a === name && argv[i + 1] ? [argv[i + 1]] : []))
35
41
  const args = argv.filter((a, i) => !a.startsWith('--') && !valued.has(argv[i - 1]))
36
42
  const usage = `usage: node build.mjs <agents-folder> --check [--profile <p.cast.yaml>] [--lang en|pt]
37
43
  node build.mjs <agents-folder> [--profile <p.cast.yaml>] [--html] [--xlsx] [--lesson] [--all] [--lang en|pt] [--out <folder>]
38
44
  node build.mjs <agents-folder> --facts [--out <folder>]
39
- node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]`
45
+ node build.mjs <agents-folder> --from <edited.xlsx> [--take <key,key…|all>] [--out <folder>]
46
+ node build.mjs <agents-folder> --declared <table.csv|.xlsx> [--agent-column <title>] [--take <key,key…|all>] [--out <folder>]
47
+ … [--telemetry <export.csv|.xlsx>] [--telemetry-days <n>]`
40
48
  const lang = option('--lang') ?? 'en'
41
49
  const fail = (msg) => {
42
50
  console.error(`${msg}\n${usage}`)
@@ -47,7 +55,11 @@ const unknown = argv.find((a) => a.startsWith('--') && !flags.has(a))
47
55
  if (unknown) fail(`✗ unknown option ${unknown}`)
48
56
  if (!['en', 'pt'].includes(lang)) fail('✗ --lang is en or pt')
49
57
  for (const v of valued) if (argv.includes(v) && !option(v)) fail(`✗ ${v} needs a value`)
50
- if (argv.includes('--take') && !argv.includes('--from')) fail('✗ --take goes with --from')
58
+ if (argv.includes('--take') && !argv.includes('--from') && !argv.includes('--declared')) fail('✗ --take goes with --from or --declared')
59
+ if (argv.includes('--from') && argv.includes('--declared')) fail('✗ --from or --declared, one at a time')
60
+ for (const f of [...options('--declared'), ...options('--telemetry')]) if (!existsSync(f) || !/\.(csv|xlsx)$/i.test(f)) fail(`✗ ${f}: a .csv or an .xlsx that is there`)
61
+ const days = Number(option('--telemetry-days') ?? 30)
62
+ if (!Number.isInteger(days) || days < 1) fail('✗ --telemetry-days is a whole number of days')
51
63
 
52
64
  const T = {
53
65
  en: {
@@ -57,6 +69,12 @@ const T = {
57
69
  changes: (n) => (n ? `${n} change${n === 1 ? '' : 's'} in the workbook (take them with --take <key,…> or --take all):` : '✓ the workbook changes nothing'),
58
70
  took: (n, f) => `✓ took ${n} change${n === 1 ? '' : 's'} into ${f}`, unknownKey: (k) => `✗ no change with the key «${k}»`,
59
71
  declared: 'declared', decision: 'decision', add: '+', set: '~', remove: '−', accepted: 'accepted', rejected: 'rejected', platform: 'platform', specialist: 'specialist', orchestrator: 'orchestrator', none: (f) => `✗ ${f} has no folder with agent.mcs.yml`, privacy: '• the files hold the client’s data: keep them in the client’s environment',
72
+ noColumn: (f, cols) => `✗ ${f}: no column names the agents of the folder (by schema name, folder or display name): say which with --agent-column "<title>" (columns: ${cols.join(', ')})`,
73
+ tie: (f, cols) => `✗ ${f}: the columns ${cols.map((c) => `«${c}»`).join(' and ')} name the agents in different ways: say which with --agent-column "<title>"`,
74
+ noHeader: (f, cols) => `✗ ${f}: no column with that title (columns: ${cols.join(', ')})`,
75
+ noEvent: (f, cols) => `✗ ${f}: no column of the event («evento», «event» or «name»; columns: ${cols.join(', ')})`,
76
+ unmatched: (n, f, list) => `• ${f}: ${n} ${n === 1 ? 'row names' : 'rows name'} nobody in the folder: ${list}`, line: 'line',
77
+ telemetry: (f, n, d) => `✓ telemetry ${f}: ${n} ${n === 1 ? 'row' : 'rows'} of the folder's agents, ${d} days`,
60
78
  },
61
79
  pt: {
62
80
  found: (n, o, s) => `✓ ${n} no elenco: ${o} ${o === 1 ? 'orquestrador' : 'orquestradores'}, ${s} ${s === 1 ? 'especialista' : 'especialistas'}`,
@@ -65,6 +83,12 @@ const T = {
65
83
  changes: (n) => (n ? `${n} ${n === 1 ? 'mudança' : 'mudanças'} na planilha (aceite com --take <chave,…> ou --take all):` : '✓ a planilha não muda nada'),
66
84
  took: (n, f) => `✓ ${n} ${n === 1 ? 'mudança gravada' : 'mudanças gravadas'} em ${f}`, unknownKey: (k) => `✗ nenhuma mudança com a chave «${k}»`,
67
85
  declared: 'declarado', decision: 'decisão', add: '+', set: '~', remove: '−', accepted: 'aceito', rejected: 'rejeitado', platform: 'plataforma', specialist: 'especialista', orchestrator: 'orquestrador', none: (f) => `✗ ${f} não tem nenhuma pasta com agent.mcs.yml`, privacy: '• os arquivos têm dados do cliente: deixe-os no ambiente dele',
86
+ noColumn: (f, cols) => `✗ ${f}: nenhuma coluna cita os agentes da pasta (pelo nome de esquema, pela pasta ou pelo nome de exibição): diga qual com --agent-column "<título>" (colunas: ${cols.join(', ')})`,
87
+ tie: (f, cols) => `✗ ${f}: as colunas ${cols.map((c) => `«${c}»`).join(' e ')} citam os agentes de jeitos diferentes: diga qual com --agent-column "<título>"`,
88
+ noHeader: (f, cols) => `✗ ${f}: nenhuma coluna com esse título (colunas: ${cols.join(', ')})`,
89
+ noEvent: (f, cols) => `✗ ${f}: nenhuma coluna do evento («evento», «event» ou «name»; colunas: ${cols.join(', ')})`,
90
+ unmatched: (n, f, list) => `• ${f}: ${n} ${n === 1 ? 'linha não cita' : 'linhas não citam'} ninguém da pasta: ${list}`, line: 'linha',
91
+ telemetry: (f, n, d) => `✓ telemetria ${f}: ${n} ${n === 1 ? 'linha' : 'linhas'} dos agentes da pasta, ${d} dias`,
68
92
  },
69
93
  }[lang]
70
94
 
@@ -84,36 +108,45 @@ const castFile = join(out, `${id}.cast.yaml`)
84
108
  const castText = existsSync(castFile) ? readFileSync(castFile, 'utf8') : undefined
85
109
  const cast = castText !== undefined ? read(castFile, readCast).cast : { ...(profile.name ? { name: profile.name } : {}), agents: [], blocks: [], declared: [], decisions: [] }
86
110
 
87
- // ---- the way back: an edited workbook as the changes it proposes ----
88
- if (argv.includes('--from')) {
89
- const edited = fromSheets(cast, readXlsx(readFileSync(resolve(option('--from')))), Object.fromEntries(profile.types.map((t) => [t.id, t.name])))
90
- if (edited.problems.length) {
91
- for (const p of edited.problems) console.error(`✗ ${basename(option('--from'))}: ${p}`)
92
- process.exit(1)
93
- }
94
- const line = (c) => {
95
- const x = c.after ?? c.before
96
- // a key with no kind («kpi») is something only people know: declared:<key>
97
- if (c.kind === 'declared') return `${T[c.op]} ${T.declared} ${x.agent} · ${labelOf(x.key.includes(':') ? x.key : `declared:${x.key}`, lang)}${x.value ? ` = «${x.value}»` : ''}${x.source ? ` (${x.source})` : ''}`
98
- return `${T[c.op]} ${T.decision} ${labelOf(x.key, lang)}: ${T[x.status]} (${T[x.scope]})${x.why ? ` — ${x.why}` : ''}`
99
- }
111
+ // a table from outside: the sheets of an .xlsx, or a .csv
112
+ const tablesOf = (file) => (/\.xlsx$/i.test(file) ? readXlsx(readFileSync(file)) : [readCsv(readFileSync(file, 'utf8'), '')])
113
+ const tableProblem = (file, p) => {
114
+ console.error(p.problem === 'tie' ? T.tie(basename(file), p.titles) : p.problem === 'header' ? T.noHeader(basename(file), p.titles) : p.problem === 'event' ? T.noEvent(basename(file), p.titles) : T.noColumn(basename(file), p.titles))
115
+ process.exit(1)
116
+ }
117
+ const line = (c) => {
118
+ const x = c.after ?? c.before
119
+ if (c.kind === 'declared') return `${T[c.op]} ${T.declared} ${x.agent} · ${labelOf(x.key.includes(':') ? x.key : `declared:${x.key}`, lang)}${x.value ? ` = «${x.value}»` : ''}${x.source ? ` (${x.source})` : ''}`
120
+ return `${T[c.op]} ${T.decision} ${labelOf(x.key, lang)}: ${T[x.status]} (${T[x.scope]})${x.why ? ` — ${x.why}` : ''}`
121
+ }
122
+ // what changes the cast file: listed, each with its key, and with --take written
123
+ const proposeOrTake = (changes) => {
100
124
  const take = option('--take')
101
125
  if (!take) {
102
- console.log(T.changes(edited.changes.length))
103
- for (const c of edited.changes) console.log(` ${c.key} ${line(c)}`)
126
+ console.log(T.changes(changes.length))
127
+ for (const c of changes) console.log(` ${c.key} ${line(c)}`)
104
128
  process.exit(0)
105
129
  }
106
- const keys = take === 'all' ? edited.changes.map((c) => c.key) : take.split(',').map((k) => k.trim()).filter(Boolean)
107
- const missing = keys.find((k) => !edited.changes.some((c) => c.key === k))
130
+ const keys = take === 'all' ? changes.map((c) => c.key) : take.split(',').map((k) => k.trim()).filter(Boolean)
131
+ const missing = keys.find((k) => !changes.some((c) => c.key === k))
108
132
  if (missing) {
109
133
  console.error(T.unknownKey(missing))
110
134
  process.exit(2)
111
135
  }
112
136
  mkdirSync(out, { recursive: true })
113
- writeFileSync(castFile, writeCast(applyChanges(cast, edited.changes, keys), castText))
137
+ writeFileSync(castFile, writeCast(applyChanges(cast, changes, keys), castText))
114
138
  console.log(T.took(keys.length, basename(castFile)))
115
139
  process.exit(0)
116
140
  }
141
+ // ---- the way back: an edited workbook as the changes it proposes ----
142
+ if (argv.includes('--from')) {
143
+ const edited = fromSheets(cast, readXlsx(readFileSync(resolve(option('--from')))), Object.fromEntries(profile.types.map((t) => [t.id, t.name])))
144
+ if (edited.problems.length) {
145
+ for (const p of edited.problems) console.error(`✗ ${basename(option('--from'))}: ${p}`)
146
+ process.exit(1)
147
+ }
148
+ proposeOrTake(edited.changes)
149
+ }
117
150
 
118
151
  // the schema's kinds, to tell a made-up one; without the schema every kind is taken as real
119
152
  const schema = join(here, '../vendor/microsoft/reference/bot.schema.yaml-authoring.json')
@@ -132,7 +165,41 @@ if (argv.includes('--facts')) {
132
165
  console.log(T.privacy)
133
166
  process.exit(0)
134
167
  }
135
- const result = compare(found, profile, cast)
168
+ if (argv.includes('--declared')) {
169
+ const changes = []
170
+ for (const file of options('--declared')) {
171
+ const sheets = tablesOf(file)
172
+ let read = 0
173
+ let problem
174
+ for (const t of sheets) {
175
+ const r = declaredFrom(t, found, cast, { file: basename(file), lang, agentColumn: option('--agent-column') })
176
+ if (r.problem) {
177
+ problem ??= r.problem
178
+ continue
179
+ }
180
+ read++
181
+ changes.push(...r.changes)
182
+ if (r.unmatched.length) console.log(T.unmatched(r.unmatched.length, basename(file), `${r.unmatched.slice(0, 5).map((u) => `${T.line} ${u.line} («${u.value}»)`).join(', ')}${r.unmatched.length > 5 ? ', …' : ''}`))
183
+ }
184
+ if (!read) tableProblem(file, problem)
185
+ }
186
+ proposeOrTake(mergeDeclared(changes))
187
+ }
188
+ let fired
189
+ if (option('--telemetry')) {
190
+ const file = option('--telemetry')
191
+ let problem
192
+ for (const t of tablesOf(file)) {
193
+ const r = firedFrom(t, found, { file: basename(file), days, agentColumn: option('--agent-column') })
194
+ if ('problem' in r) problem ??= r.problem
195
+ else {
196
+ fired = r
197
+ break
198
+ }
199
+ }
200
+ if (!fired) tableProblem(file, problem)
201
+ }
202
+ const result = compare(found, profile, cast, fired)
136
203
  const looks = reviewCast(result)
137
204
  const roles = Object.values(result.roles)
138
205
  console.log(T.found(found.length, roles.filter((r) => r === 'orchestrator').length, roles.filter((r) => r === 'specialist').length))