@microteia/console 2.10.0 → 2.19.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.
@@ -22,6 +22,63 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.19.0] — 2026-08-28
26
+
27
+ - fix(cli/deploy): the installation gate is charged against the functions that are GOING UP, not against every source found on disk. It lived in `functionProblems` — the door that answers "can this set be assembled?" — and that door reads every `extensions/<x>/functions/` directory that exists, including one left behind by an extension that was uninstalled and removed. The result was `microteia deploy` refusing in full — hosting, rules and indexes with it — over a callable no command would have deployed. `gateProblems` now carries the function's name, and `gateProblemsFor` filters by what is being deployed.
28
+ - fix(cli/provision): a gated function that fails the check leaves the deploy queue instead of taking the command down with it. Everything else provisions, `--prune` still removes the orphan, and the command ends in an error naming the function that was held back.
29
+ - feat(cli/provision): orphan functions are found in the PROJECT, not only in the record. `backend.functions` in `microteia.yaml` was the single source of truth about what had been deployed, and it is a truth that gets lost — a redone `init`, a restored backup, a Console older than the field. Lost it, and the Cloud Function of an extension nobody has stayed in the air with no command mentioning it. `straysInProject` crosses `firebase functions:list` with what the installation can justify; only a name this installation knows how to produce can become an orphan, and what it does not know is reported and never touched.
30
+ - feat(cli/provision): an orphan found that way enters the record when it is not removed, so it stays known from the next run on even if its source leaves the disk.
31
+ - fix(cli/provision): `functions:list` is asked on every run. It was skipped when there was nothing applied to check — the exact case that needed it, since an installation with no extensions left is where the departed extension's function sits. When the listing cannot be read, the command says so instead of letting silence read as "nothing left over".
32
+
33
+ #### [2.18.0] — 2026-08-28
34
+
35
+ - feat(console/functionGate): an extension's callable without `requireInstalledExtension` is refused, and a guard naming another extension's id is rejected the way a misgated trigger is. The refusal exists because the deploy makes every callable publicly invocable — this Console's own `callableAccess.ts` is there to confirm that grant went through — so the callable answers any signed-in user of the project, from any organization, whether or not they installed the extension, with the organization arriving in the payload.
36
+ - feat(console/functionGate): the scope of this check is the FILE, like the per-office loop and unlike the trigger guard. The guard runs inside the handler because it needs the `request`, and the authoring guide asks authors to keep the handler thin with the decision in a file beside it — demanding the call inside the parentheses would refuse exactly the code the guide asks for.
37
+ - feat(console/functionGate): the SHARED callable is left out, and it is recorded rather than silently skipped: `requireInstalledExtension` takes one id and a shared function serves several. The "any of the ones I serve" guard exists for a trigger (`forAnyInstalledExtension`), not for a callable.
38
+
39
+ #### [2.17.0] — 2026-08-28
40
+
41
+ - feat(console/customFunctions): `microteia.serves` in `functions/package.json` — function name to the extension ids it serves. It closes the widest and quietest gap in the whole withdrawal mechanism. A function in `functions/` has no owner by construction (no extension may claim it), so it sat outside the mechanism ENTIRELY: never recorded as applied, never classified as an orphan, never reported, and out of `--prune`'s reach. It stayed deployed forever, put up by every deploy, with every extension it served long uninstalled — and the authoring guide was the thing telling authors to put shared functions there.
42
+ - feat(console/provision): a shared function that declares who it serves is deployed while ANY of them is installed anywhere in the project, and becomes an orphan when none is. The question is asked against every active installation and not against the claims: an extension with no `manifest.backend` never becomes a claim, and could well be one of the served ones — deciding from the claim list would delete the function of somebody who has it installed. The applied record keeps the declared list rather than electing one owner, because that is what explains, months later, why the function was there and why it stopped being.
43
+ - feat(console/deploy): a shared function that declares who it serves leaves the unconditional deploy list. Putting it up here anyway would have `--prune` delete it and the next deploy restore it, indefinitely — the same loop the claim refusal avoids. One with NO declaration goes up exactly as before: keeping that is what stops this version from taking a function out of an installation that already works.
44
+ - feat(console/functionGate): a declared shared trigger without a guard is refused, and so is a guard naming an extension the function does not declare serving. The two lists answer the same question — `serves` decides the withdrawal, the guard decides the execution — and one of them being wrong makes the function act where it can no longer be withdrawn. Guarding by one of the declared extensions is accepted: the list is the ceiling, not the floor. A shared function with no `serves` is not asked for a guard, because without ids there would be no guard to ask for.
45
+ - feat(console/provision): shared functions with no `serves` are listed on every run. They keep working as before and nothing withdraws them — what changes is that they stop being invisible, which was the defect: whoever inherited an installation had no way to know those functions were outside the mechanism, and the only way to find out was a trigger firing years after the last extension that used it had gone.
46
+ - fix(console/provision): a parameter declared in `functions/` now marks the served shared functions as stale. They used to go up on every deploy, which masked the question; moving them to provisioning would have left one running with the old value, which is the defect that mechanism exists to close. The granularity is the directory, not the function, because `functions/` declares its `defineString` calls in one set without saying which function reads which.
47
+
48
+ #### [2.16.0] — 2026-08-28
49
+
50
+ - feat(console/functionGate): an extension's function whose event carries no `{tenantId}` — `onSchedule`, `onMessagePublished`, `onRequest`, the Storage object triggers, the login-blocking ones — is refused unless it loops with `forEachInstalledOffice`. The refusal only became honest once there was an answer to point at: before it, both of the author's options were wrong, and the worse one passed every check. A loop naming another extension's id is rejected the same way a misgated trigger is, and the report names WHICH guard got the wrong id, because the fix is in one string and the author should not have to guess which.
51
+ - feat(console/functionGate): the scope of this check is the FILE, not the call's parentheses. A document trigger's guard wraps that handler, so it has to be there; the loop runs inside the handler and may sit a couple of calls deeper, in a helper the author wrote. Demanding it in the parentheses would refuse correct code, and the price is stated: two scheduled functions in one file, one looping and one not, both pass.
52
+ - feat(console/functionGate): the list of constructors is explicit rather than "everything that is not a document trigger". RTDB is deliberately left out — its path HAS parameters, so a `{tenantId}` there is possible, and the platform uses no RTDB anywhere. What is not known is not refused.
53
+
54
+ #### [2.15.0] — 2026-08-28
55
+
56
+ - feat(console/backendResidue): provisioning writes what it found back to the database, at `backend_state/current`. It is the one place in the system that can say "this Cloud Function has no owner any more" — the answer needs a scan of every office in the project compared against the applied record — and it said so in the terminal and kept the conclusion in `microteia.yaml`, a file on one operator's machine. The application never learned anything, so the whole mechanism depended on somebody remembering. This is not a new path: the Console already writes to the database over REST, which is how `backendState` is stamped.
57
+ - feat(console/backendResidue): the document is written on EVERY run, empty included. Writing only when there are orphans would leave the banner up after `--prune` had already dealt with them, and a banner that never goes away is one people learn to ignore — the empty list is what clears the previous one. The list is the one left AFTER pruning: what it deleted is gone, what it could not delete stays, which is the state of the project now rather than the state at the start of the run. `tenants` and `extensions` travel with it, because "nobody needs it any more" is a claim about every office in the project and whoever reads the banner deserves to check it instead of believing it.
58
+ - fix(console/provision): failing to record the leftovers does not fail the command. The functions and indexes were already applied; what was left to do was a REPORT, and refusing the whole run would send whoever installs to repeat a command that finished its work. The failure is said out loud, though — staying quiet would leave a silent screen looking like "there is no residue", which is the opposite claim.
59
+ - feat(console/firestoreRest): `asInteger`, `asArray`, `asMap` and `patchFields`. The mask lists every field written, deliberately: without it a PATCH behaves differently depending on whether the document exists, and what is wanted here is convergence — since all fields are written every time, the complete mask is also what erases what stopped being true.
60
+
61
+ #### [2.14.0] — 2026-08-28
62
+
63
+ - feat(console/provision): claiming a function that lives in `functions/` — the directory for a function SEVERAL extensions share — is refused. `baselineClaimProblems` already guarded the same defect against the platform's own functions, and the sentence is identical: the deploy puts it up regardless, so the claim changes nothing while the extension is installed, and the day it is uninstalled `--prune` deletes a function other extensions depended on. This case adds one turn of the screw — the next deploy puts it back, because the source is still sitting in `functions/` — so the symptom is an outage that closes by itself, repeating on every `provision --prune`, with nothing in the tree to suggest what happened. Rejecting the extension puts the name in `unresolved`, which is what keeps `--prune` off the shared function while the wrong declaration stands.
64
+
65
+ #### [2.13.0] — 2026-08-28
66
+
67
+ - feat(console/functionGate): an extension's Firestore trigger without `forInstalledExtension` is refused. The guard is the half of the withdrawal that acts at the moment somebody clicks Uninstall — `--prune` cannot reach the office that uninstalled while another still has the extension — and nothing cobbled it: `create function` wrote it into the scaffold, and no check in packing, deploying or provisioning ever read the source again. An ungated trigger went up and stayed up, and its symptom is not a failure: it is the code of an extension nobody has any more running over the data of offices that never asked for it, with no error anywhere.
68
+ - feat(console/provision): a guard that names ANOTHER extension's id rejects the extension that claims the function. It is what a copied function leaves behind, and its symptom is the opposite of the ungated one — the trigger deploys, fires on every write, and the handler never runs, because the gate asks about an extension nobody installed. Only provisioning can ask this: the claim comes from the office's database, and the deploy does not have it. The owner is rejected by identity and not by substring, because the message names two ids and matching on text would also reject the extension whose id was copied.
69
+ - feat(console/provision): an ungated trigger already deployed is REPORTED rather than refused. Refusing here would lock `--prune` — the one command that removes the orphan — behind a source it does not need to read. The refusal belongs where the source enters the package, and there it blocks nobody's way out.
70
+ - fix(console/functionGate): comments are stripped before the source is read, and strings are not. The scaffold's own JSDoc cites `forInstalledExtension` to explain it, so deleting the call and keeping the explanation would have passed the check — which is exactly what an author produces while editing generated code. A guard declared in an identifier beside the trigger is accepted, because that is correct code and a check that refuses correct code is the one that teaches the next person to switch it off.
71
+
72
+ #### [2.12.0] — 2026-08-27
73
+
74
+ - feat(pack): the packaged `functions/package.json` gains an `exports` map and `custom/tsconfig.json` a `paths` entry, which is what lets a Cloud Function written by the organization import `microteia-functions/office` — the platform surface that keeps an extension's trigger from running in organizations that do not have it. No new dependency, so the packaged lockfile stays valid.
75
+
76
+ #### [2.11.0] — 2026-08-27
77
+
78
+ - fix(provision): orphan Cloud Functions are reported even when no installed extension asks for a backend at all — the case that mattered most, and the only one the previous shortcut skipped.
79
+ - fix(provision --prune): a function claimed by an extension that is still installed, and one the platform deploys itself, are never deleted. Both were reachable before.
80
+ - feat(provision): the record of applied functions carries the plugin that owns each one, and the orphan report names it along with how many offices were read to reach the conclusion.
81
+
25
82
  #### [2.10.0] — 2026-08-27
26
83
 
27
84
  - feat(export-catalog): a new command writes what an office HAS — its screens with their fields, its
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.10.0
1
+ # Microteia Console 2.19.0
2
2
 
3
3
  Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
4
4
  what it needs are command-line tools that are already on the machine, listed at the bottom.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * O registro do que o provisionamento já deployou — e de QUEM.
3
+ *
4
+ * Ele morava como uma lista de nomes soltos em `backend.functions` do `microteia.yaml`, e a lista de
5
+ * nomes nunca respondeu à única pergunta que a remoção faz: de quem era esta function? Sem o dono, o
6
+ * relatório de uma órfã é "sobrou `acmeSync`", que não é uma decisão — é um enigma para quem herda a
7
+ * instalação meses depois. Com o dono ele é "`acmeSync` era de `com.acme.crm`, que não está instalada
8
+ * em escritório nenhum", que é.
9
+ *
10
+ * A forma gravada é `nome@pluginId`, separada por vírgula, no mesmo escalar de antes. Uma entrada sem
11
+ * `@` continua sendo lida: é o que uma instalação anterior a este campo gravou, e ela vale — o que
12
+ * falta é o dono, não a function.
13
+ */
14
+ export interface AppliedFunction {
15
+ name: string;
16
+ /** O plugin que a reivindicava quando ela subiu. `null` numa instalação anterior ao campo. */
17
+ owner: string | null;
18
+ }
19
+ export declare const parseAppliedFunctions: (raw: string | null) => AppliedFunction[];
20
+ export declare const serializeAppliedFunctions: (applied: AppliedFunction[]) => string | null;
21
+ /** Por que uma function que ninguém exige mais NÃO é órfã. */
22
+ export type PreservedReason = 'baseline' | 'unresolved';
23
+ export interface PreservedFunction {
24
+ entry: AppliedFunction;
25
+ reason: PreservedReason;
26
+ /** O plugin cuja declaração não pôde ser resolvida — só em `unresolved`. */
27
+ claimant?: string;
28
+ }
29
+ export interface AppliedClassification {
30
+ /** Alguma extensão instalada continua exigindo. */
31
+ wanted: AppliedFunction[];
32
+ /** Ninguém exige, e nada impede de remover. */
33
+ orphans: AppliedFunction[];
34
+ /** Ninguém exige, e remover seria errado. */
35
+ preserved: PreservedFunction[];
36
+ }
37
+ /**
38
+ * O que fazer com cada function já aplicada, e a razão de nunca ser só "ninguém pediu".
39
+ *
40
+ * As duas categorias preservadas existem porque "ninguém exige" tem três causas diferentes, e uma
41
+ * delas — a única — autoriza apagar:
42
+ *
43
+ * - `baseline`: a function é da PLATAFORMA. O `deploy` a sobe sempre, independentemente de extensão
44
+ * nenhuma. Ela só chegou a este registro porque uma extensão reivindicou um nome do kernel, e apagá-la
45
+ * derrubaria um gatilho do produto num escritório que não fez nada de errado.
46
+ * - `unresolved`: a extensão que a reivindica CONTINUA instalada, e o que falhou foi resolver a
47
+ * declaração dela — um export renomeado em `extensions/<x>/functions/index.ts`, um upgrade que tirou o
48
+ * nome do pacote. Apagar aqui é apagar a function de uma extensão viva por causa de um defeito de
49
+ * configuração que a próxima execução conserta.
50
+ * - órfã de verdade: nome nenhum a reivindica, e nenhuma das duas condições acima vale. Foi
51
+ * desinstalada de todos os escritórios do projeto.
52
+ */
53
+ export declare const classifyApplied: (input: {
54
+ applied: AppliedFunction[];
55
+ wanted: string[];
56
+ baseline: string[];
57
+ /** Nome de function -> plugin cuja reivindicação foi recusada nesta execução. */
58
+ unresolved: Map<string, string>;
59
+ }) => AppliedClassification;
60
+ /**
61
+ * A lista aplicada CONVERGE para o que ainda é exigido: o que já estava e continua sendo pedido, mais
62
+ * o que subiu agora, mais o órfão que não foi removido, mais o que foi preservado. Reescrevê-la só com
63
+ * o que subiu NESTA execução a esvaziava na segunda passada — e a terceira redeployava tudo, achando
64
+ * que nada havia sido feito.
65
+ *
66
+ * O dono é reconciliado com quem reivindica AGORA: uma function que trocou de extensão entre duas
67
+ * versões fica registrada com a atual, e não com a que a pôs no ar da primeira vez.
68
+ */
69
+ export declare const appliedAfterRun: (input: {
70
+ applied: AppliedFunction[];
71
+ wanted: string[];
72
+ deployed: string[];
73
+ keptOrphans: AppliedFunction[];
74
+ preserved: AppliedFunction[];
75
+ /** Nome de function -> plugin que a reivindica nesta execução. */
76
+ owners: Map<string, string>;
77
+ }) => AppliedFunction[];
78
+ /** Como uma function aplicada aparece num relatório: sem dono conhecido, só o nome. */
79
+ export declare const describeApplied: (entry: AppliedFunction) => string;
80
+ export interface StrayFunctions {
81
+ /** Roda no projeto, esta instalação sabe produzi-la, e nada a justifica agora: órfã de verdade. */
82
+ orphans: AppliedFunction[];
83
+ /** Roda no projeto e esta instalação não a conhece. Relatada, nunca apagada. */
84
+ strangers: string[];
85
+ }
86
+ /**
87
+ * A órfã que o REGISTRO não conhece — e o limite que impede a varredura de virar um estrago.
88
+ *
89
+ * O registro (`backend.functions` do `microteia.yaml`) era a ÚNICA verdade sobre o que havia sido
90
+ * deployado, e é uma verdade que se perde: um Console anterior ao campo, um `init` refeito, um deploy
91
+ * dado à mão, um arquivo restaurado de um backup mais velho. Perdido o registro, `classifyApplied` não
92
+ * tinha o que classificar, `--prune` não tinha o que apagar, e a Cloud Function de uma extensão que
93
+ * ninguém tem mais ficava no ar para sempre, sem nenhum comando que a mencionasse. É o pior sintoma da
94
+ * família: uma callable publicamente invocável, de código que ninguém instalou, e silêncio.
95
+ *
96
+ * A resposta é perguntar ao PROJETO o que ele roda, em vez de só ao arquivo. E o limite é `known`: só é
97
+ * candidata a órfã a function que ESTA instalação sabe produzir — o que o pacote traz mais o que o
98
+ * escritório escreveu. O que o projeto roda e a instalação não conhece pode ser de qualquer um: um
99
+ * projeto Firebase é compartilhável, e apagar ali seria a Microteia decidir sobre o que não é dela.
100
+ * Esse caso é relatado e nunca tocado.
101
+ *
102
+ * `recorded` sai do resultado porque quem responde por ele é `classifyApplied`, que sabe mais: ele tem o
103
+ * dono e as duas razões de preservação. Contar duas vezes a mesma function proporia apagá-la duas vezes.
104
+ */
105
+ export declare const straysInProject: (input: {
106
+ /** O que o projeto roda AGORA, na região do kernel. */
107
+ running: string[];
108
+ /** O que esta instalação sabe produzir: o pacote mais o que o escritório escreveu. */
109
+ known: string[];
110
+ /** O que tem razão de estar no ar agora: exigida, baseline, comum sem declaração, ou não resolvida. */
111
+ justified: string[];
112
+ /** O que o registro já conhece — respondido por `classifyApplied`. */
113
+ recorded: string[];
114
+ }) => StrayFunctions;
@@ -0,0 +1,123 @@
1
+ export const parseAppliedFunctions = (raw) => {
2
+ const applied = [];
3
+ const seen = new Set();
4
+ for (const entry of (raw ?? '').split(',')) {
5
+ const trimmed = entry.trim();
6
+ if (trimmed === '')
7
+ continue;
8
+ const separator = trimmed.indexOf('@');
9
+ const name = separator === -1 ? trimmed : trimmed.slice(0, separator).trim();
10
+ const owner = separator === -1 ? '' : trimmed.slice(separator + 1).trim();
11
+ if (name === '' || seen.has(name))
12
+ continue;
13
+ seen.add(name);
14
+ applied.push({ name, owner: owner === '' ? null : owner });
15
+ }
16
+ return applied;
17
+ };
18
+ export const serializeAppliedFunctions = (applied) => {
19
+ const entries = [...applied]
20
+ .sort((left, right) => left.name.localeCompare(right.name))
21
+ .map((entry) => (entry.owner === null ? entry.name : `${entry.name}@${entry.owner}`));
22
+ return entries.length === 0 ? null : entries.join(',');
23
+ };
24
+ /**
25
+ * O que fazer com cada function já aplicada, e a razão de nunca ser só "ninguém pediu".
26
+ *
27
+ * As duas categorias preservadas existem porque "ninguém exige" tem três causas diferentes, e uma
28
+ * delas — a única — autoriza apagar:
29
+ *
30
+ * - `baseline`: a function é da PLATAFORMA. O `deploy` a sobe sempre, independentemente de extensão
31
+ * nenhuma. Ela só chegou a este registro porque uma extensão reivindicou um nome do kernel, e apagá-la
32
+ * derrubaria um gatilho do produto num escritório que não fez nada de errado.
33
+ * - `unresolved`: a extensão que a reivindica CONTINUA instalada, e o que falhou foi resolver a
34
+ * declaração dela — um export renomeado em `extensions/<x>/functions/index.ts`, um upgrade que tirou o
35
+ * nome do pacote. Apagar aqui é apagar a function de uma extensão viva por causa de um defeito de
36
+ * configuração que a próxima execução conserta.
37
+ * - órfã de verdade: nome nenhum a reivindica, e nenhuma das duas condições acima vale. Foi
38
+ * desinstalada de todos os escritórios do projeto.
39
+ */
40
+ export const classifyApplied = (input) => {
41
+ const wantedNames = new Set(input.wanted);
42
+ const baseline = new Set(input.baseline);
43
+ const classification = { wanted: [], orphans: [], preserved: [] };
44
+ for (const entry of input.applied) {
45
+ if (wantedNames.has(entry.name)) {
46
+ classification.wanted.push(entry);
47
+ continue;
48
+ }
49
+ if (baseline.has(entry.name)) {
50
+ classification.preserved.push({ entry, reason: 'baseline' });
51
+ continue;
52
+ }
53
+ const claimant = input.unresolved.get(entry.name);
54
+ if (claimant !== undefined) {
55
+ classification.preserved.push({ entry, reason: 'unresolved', claimant });
56
+ continue;
57
+ }
58
+ classification.orphans.push(entry);
59
+ }
60
+ return classification;
61
+ };
62
+ /**
63
+ * A lista aplicada CONVERGE para o que ainda é exigido: o que já estava e continua sendo pedido, mais
64
+ * o que subiu agora, mais o órfão que não foi removido, mais o que foi preservado. Reescrevê-la só com
65
+ * o que subiu NESTA execução a esvaziava na segunda passada — e a terceira redeployava tudo, achando
66
+ * que nada havia sido feito.
67
+ *
68
+ * O dono é reconciliado com quem reivindica AGORA: uma function que trocou de extensão entre duas
69
+ * versões fica registrada com a atual, e não com a que a pôs no ar da primeira vez.
70
+ */
71
+ export const appliedAfterRun = (input) => {
72
+ const wanted = new Set(input.wanted);
73
+ const byName = new Map();
74
+ const remember = (name, recorded) => {
75
+ byName.set(name, { name, owner: input.owners.get(name) ?? recorded });
76
+ };
77
+ for (const entry of input.applied) {
78
+ if (wanted.has(entry.name))
79
+ remember(entry.name, entry.owner);
80
+ }
81
+ for (const name of input.deployed)
82
+ remember(name, byName.get(name)?.owner ?? null);
83
+ for (const entry of [...input.keptOrphans, ...input.preserved])
84
+ remember(entry.name, entry.owner);
85
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
86
+ };
87
+ /** Como uma function aplicada aparece num relatório: sem dono conhecido, só o nome. */
88
+ export const describeApplied = (entry) => entry.owner === null ? entry.name : `${entry.name} (${entry.owner})`;
89
+ /**
90
+ * A órfã que o REGISTRO não conhece — e o limite que impede a varredura de virar um estrago.
91
+ *
92
+ * O registro (`backend.functions` do `microteia.yaml`) era a ÚNICA verdade sobre o que havia sido
93
+ * deployado, e é uma verdade que se perde: um Console anterior ao campo, um `init` refeito, um deploy
94
+ * dado à mão, um arquivo restaurado de um backup mais velho. Perdido o registro, `classifyApplied` não
95
+ * tinha o que classificar, `--prune` não tinha o que apagar, e a Cloud Function de uma extensão que
96
+ * ninguém tem mais ficava no ar para sempre, sem nenhum comando que a mencionasse. É o pior sintoma da
97
+ * família: uma callable publicamente invocável, de código que ninguém instalou, e silêncio.
98
+ *
99
+ * A resposta é perguntar ao PROJETO o que ele roda, em vez de só ao arquivo. E o limite é `known`: só é
100
+ * candidata a órfã a function que ESTA instalação sabe produzir — o que o pacote traz mais o que o
101
+ * escritório escreveu. O que o projeto roda e a instalação não conhece pode ser de qualquer um: um
102
+ * projeto Firebase é compartilhável, e apagar ali seria a Microteia decidir sobre o que não é dela.
103
+ * Esse caso é relatado e nunca tocado.
104
+ *
105
+ * `recorded` sai do resultado porque quem responde por ele é `classifyApplied`, que sabe mais: ele tem o
106
+ * dono e as duas razões de preservação. Contar duas vezes a mesma function proporia apagá-la duas vezes.
107
+ */
108
+ export const straysInProject = (input) => {
109
+ const known = new Set(input.known);
110
+ const justified = new Set(input.justified);
111
+ const recorded = new Set(input.recorded);
112
+ const stray = { orphans: [], strangers: [] };
113
+ for (const name of [...new Set(input.running)].sort()) {
114
+ if (!known.has(name)) {
115
+ stray.strangers.push(name);
116
+ continue;
117
+ }
118
+ if (justified.has(name) || recorded.has(name))
119
+ continue;
120
+ stray.orphans.push({ name, owner: null });
121
+ }
122
+ return stray;
123
+ };
@@ -0,0 +1,55 @@
1
+ import type { FirestoreTypedValue } from './firestoreRest.js';
2
+ /**
3
+ * O que o provisionamento descobriu, devolvido para o banco — onde a APLICAÇÃO pode ler.
4
+ *
5
+ * O `provision` é o único ponto do sistema capaz de dizer "esta Cloud Function não tem mais dono": a
6
+ * resposta exige varrer todos os escritórios do projeto e comparar com o registro de aplicadas. Ele
7
+ * dizia isso no terminal e guardava a conclusão no `microteia.yaml` — um arquivo na máquina de quem
8
+ * rodou o comando. Quem clicou em Desinstalar, na aplicação, nunca ficava sabendo de nada; e quem lê a
9
+ * tela de Extensões não é necessariamente quem tem acesso ao projeto Firebase.
10
+ *
11
+ * O efeito era o mesmo em toda instalação: o `--prune` continua sendo um ato explícito — e isso está
12
+ * certo, apagar é destrutivo e irreversível —, mas ninguém tinha como saber que ele era necessário. O
13
+ * mecanismo inteiro dependia de alguém lembrar.
14
+ *
15
+ * A tela de Extensões já mostra o selo "Backend pendente" para o caso da INSTALAÇÃO. Este documento é o
16
+ * par que faltava, o da RETIRADA, e não inventa caminho novo: o Console já escreve no banco pela REST
17
+ * (é assim que `backendState` é carimbado), e o pacote já tem uma coleção global de espelho somente
18
+ * leitura escrita só com credencial de administrador (`workflow_step_catalog`).
19
+ */
20
+ /**
21
+ * Um documento só, de nome fixo. A órfã é um fato do PROJETO — não de um escritório —, então guardá-la
22
+ * por tenant seria guardar a mesma resposta N vezes e deixar N maneiras de ela divergir.
23
+ */
24
+ export declare const BACKEND_RESIDUE_PATH = "/backend_state/current";
25
+ export interface OrphanFunctionRecord {
26
+ name: string;
27
+ /** O plugin que a reivindicava quando ela subiu. Vazio numa instalação anterior ao registro com dono. */
28
+ owner: string;
29
+ }
30
+ export interface BackendResidueInput {
31
+ /** Quando a varredura aconteceu. É o que permite a quem lê saber se a informação é de hoje. */
32
+ scannedAt: Date;
33
+ /** Escritórios lidos, e cópias de extensão lidas: a contagem que SUSTENTA a afirmação. */
34
+ tenants: number;
35
+ extensions: number;
36
+ functions: OrphanFunctionRecord[];
37
+ /** O índice órfão, na forma legível que o relatório do terminal também usa. */
38
+ indexes: string[];
39
+ }
40
+ /**
41
+ * Os campos do documento, montados sem tocar na rede.
42
+ *
43
+ * Separado da escrita porque é a FORMA que é contrato: ela é lida pelo kernel, que é versionado à parte
44
+ * do Console. Um campo renomeado aqui é uma tela que deixa de avisar, em silêncio — e é essa a
45
+ * verificação que o teste faz.
46
+ */
47
+ export declare const backendResidueFields: (input: BackendResidueInput) => Record<string, FirestoreTypedValue>;
48
+ /**
49
+ * Escreve o resíduo, SEMPRE — inclusive quando não há nenhum.
50
+ *
51
+ * Escrever só quando há órfã deixaria o aviso na tela depois de o `--prune` já ter resolvido, e um aviso
52
+ * que não sai é um aviso que se aprende a ignorar. Como todos os campos são escritos a cada execução, a
53
+ * lista vazia é o que apaga a anterior.
54
+ */
55
+ export declare const writeBackendResidue: (projectId: string, input: BackendResidueInput) => Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { asArray, asInteger, asMap, asString, asTimestamp, patchFields } from './firestoreRest.js';
2
+ import { CLI_VERSION } from '../shared/paths.js';
3
+ /**
4
+ * O que o provisionamento descobriu, devolvido para o banco — onde a APLICAÇÃO pode ler.
5
+ *
6
+ * O `provision` é o único ponto do sistema capaz de dizer "esta Cloud Function não tem mais dono": a
7
+ * resposta exige varrer todos os escritórios do projeto e comparar com o registro de aplicadas. Ele
8
+ * dizia isso no terminal e guardava a conclusão no `microteia.yaml` — um arquivo na máquina de quem
9
+ * rodou o comando. Quem clicou em Desinstalar, na aplicação, nunca ficava sabendo de nada; e quem lê a
10
+ * tela de Extensões não é necessariamente quem tem acesso ao projeto Firebase.
11
+ *
12
+ * O efeito era o mesmo em toda instalação: o `--prune` continua sendo um ato explícito — e isso está
13
+ * certo, apagar é destrutivo e irreversível —, mas ninguém tinha como saber que ele era necessário. O
14
+ * mecanismo inteiro dependia de alguém lembrar.
15
+ *
16
+ * A tela de Extensões já mostra o selo "Backend pendente" para o caso da INSTALAÇÃO. Este documento é o
17
+ * par que faltava, o da RETIRADA, e não inventa caminho novo: o Console já escreve no banco pela REST
18
+ * (é assim que `backendState` é carimbado), e o pacote já tem uma coleção global de espelho somente
19
+ * leitura escrita só com credencial de administrador (`workflow_step_catalog`).
20
+ */
21
+ /**
22
+ * Um documento só, de nome fixo. A órfã é um fato do PROJETO — não de um escritório —, então guardá-la
23
+ * por tenant seria guardar a mesma resposta N vezes e deixar N maneiras de ela divergir.
24
+ */
25
+ export const BACKEND_RESIDUE_PATH = '/backend_state/current';
26
+ /**
27
+ * Os campos do documento, montados sem tocar na rede.
28
+ *
29
+ * Separado da escrita porque é a FORMA que é contrato: ela é lida pelo kernel, que é versionado à parte
30
+ * do Console. Um campo renomeado aqui é uma tela que deixa de avisar, em silêncio — e é essa a
31
+ * verificação que o teste faz.
32
+ */
33
+ export const backendResidueFields = (input) => ({
34
+ scannedAt: asTimestamp(input.scannedAt),
35
+ consoleVersion: asString(CLI_VERSION),
36
+ tenants: asInteger(input.tenants),
37
+ extensions: asInteger(input.extensions),
38
+ orphanFunctions: asArray(input.functions.map((entry) => asMap({
39
+ name: asString(entry.name),
40
+ owner: asString(entry.owner),
41
+ }))),
42
+ orphanIndexes: asArray(input.indexes.map((entry) => asString(entry))),
43
+ });
44
+ /**
45
+ * Escreve o resíduo, SEMPRE — inclusive quando não há nenhum.
46
+ *
47
+ * Escrever só quando há órfã deixaria o aviso na tela depois de o `--prune` já ter resolvido, e um aviso
48
+ * que não sai é um aviso que se aprende a ignorar. Como todos os campos são escritos a cada execução, a
49
+ * lista vazia é o que apaga a anterior.
50
+ */
51
+ export const writeBackendResidue = async (projectId, input) => {
52
+ await patchFields(projectId, BACKEND_RESIDUE_PATH, backendResidueFields(input));
53
+ };
@@ -1,4 +1,5 @@
1
1
  import type { BackendParameter } from '../kernel/manifest.js';
2
+ import type { TriggerDeclaration } from './functionGate.js';
2
3
  /**
3
4
  * As Cloud Functions do ESCRITÓRIO — as que ele escreve, não as do kernel.
4
5
  *
@@ -37,10 +38,35 @@ export interface FunctionSource {
37
38
  shared: boolean;
38
39
  /** As callables — as únicas que o navegador chama, e por isso as únicas com alcance a conferir. */
39
40
  callables: string[];
41
+ /**
42
+ * As callables com o estado da guarda de instalação de cada uma. Separado de `callables` porque
43
+ * responde a outra pergunta: aquela é "o navegador alcança?", esta é "quem mais alcança?".
44
+ */
45
+ callableGuards: TriggerDeclaration[];
46
+ /**
47
+ * Os gatilhos de Firestore declarados aqui, com o estado do portão de instalação de cada um. É o que
48
+ * permite recusar um gatilho de extensão que dispararia para todos os escritórios do projeto.
49
+ */
50
+ triggers: TriggerDeclaration[];
51
+ /**
52
+ * As functions cujo evento NÃO carrega escritório — agendada, fila, HTTP, Storage. Para elas o portão
53
+ * não serve (falha fechado), e quem responde é o laço por escritório.
54
+ */
55
+ unscoped: TriggerDeclaration[];
40
56
  /** Os `defineString` que ela declara. O `firebase-functions` os valida GLOBALMENTE (ver abaixo). */
41
57
  parameters: BackendParameter[];
42
58
  /** As dependências npm que ela declara no `package.json` do próprio diretório. */
43
59
  dependencies: Record<string, string>;
60
+ /**
61
+ * Quem cada function COMUM serve, de `microteia.serves` no `package.json` do diretório.
62
+ *
63
+ * É a declaração que tira `functions/` da invisibilidade. A function de extensão tem dono pelo LUGAR
64
+ * do arquivo; a comum não tem nenhum — nenhuma extensão pode reivindicá-la —, e por isso ela ficava
65
+ * fora do mecanismo de retirada por inteiro: nunca entrava no registro de aplicadas, nunca era
66
+ * classificada como órfã, nunca era relatada, e o `--prune` não a alcançava. Ficava no ar para sempre,
67
+ * com todas as extensões que ela servia desinstaladas.
68
+ */
69
+ serves: Record<string, string[]>;
44
70
  }
45
71
  /**
46
72
  * Os nomes que um módulo de entrada exporta. Cobre as duas formas que se escreve de fato: o
@@ -55,12 +81,28 @@ export declare const findFunctionSources: (root: string) => Promise<FunctionSour
55
81
  * `shared` e `owned` respondem a perguntas diferentes, e é por isso que são duas listas: a comum sobe
56
82
  * no `deploy`, sempre; a de extensão sobe pelo provisionamento, e só onde a extensão está instalada.
57
83
  */
84
+ /** Uma function comum e as extensões que ela declara servir. */
85
+ export interface SharedRequirement {
86
+ name: string;
87
+ serves: string[];
88
+ }
58
89
  export interface OfficeFunctions {
59
90
  sources: FunctionSource[];
60
91
  /** Todos os nomes, na ordem em que as origens são lidas. */
61
92
  all: string[];
62
- /** As de `functions/` — sem dono, e por isso deployadas sempre. */
93
+ /** As de `functions/`, declarem ou não a quem servem. */
63
94
  shared: string[];
95
+ /**
96
+ * As comuns que DECLARAM quem servem. Elas entram no mecanismo de retirada: sobem pelo
97
+ * provisionamento enquanto alguma das extensões servidas estiver instalada, e viram órfãs quando
98
+ * nenhuma estiver.
99
+ */
100
+ served: SharedRequirement[];
101
+ /**
102
+ * As comuns SEM declaração. Sobem no `deploy`, sempre, e nada as retira — é o comportamento anterior,
103
+ * mantido para não quebrar instalação nenhuma, e relatado para não seguir invisível.
104
+ */
105
+ unserved: string[];
64
106
  callables: string[];
65
107
  parameters: BackendParameter[];
66
108
  }
@@ -76,6 +118,14 @@ export declare const readOfficeFunctions: (root: string) => Promise<OfficeFuncti
76
118
  * silêncio — e substituir um gatilho do kernel é o tipo de defeito que só aparece quando algo deixa de
77
119
  * acontecer, sem erro em lugar nenhum.
78
120
  */
121
+ /**
122
+ * O que impede `microteia.serves` de ser levado a sério.
123
+ *
124
+ * As três recusas têm o mesmo motivo: a declaração é o que autoriza a plataforma a RETIRAR a function
125
+ * comum, e uma declaração que não fecha faria a retirada acontecer pelo motivo errado — ou não acontecer,
126
+ * e nada diria por quê.
127
+ */
128
+ export declare const servesProblems: (sources: FunctionSource[]) => string[];
79
129
  export declare const functionProblems: (sources: FunctionSource[], kernelFunctions: string[]) => string[];
80
130
  /**
81
131
  * Duas declarações da MESMA dependência com faixas diferentes, entre diretórios do escritório.