@writedocs/generator 0.3.0 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedocs/generator",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,8 @@
20
20
  "build": "node bin/writedocs.js build",
21
21
  "init": "node bin/writedocs.js init",
22
22
  "build:schema": "esbuild src/lib/config-schema.ts --format=esm --target=node22 --outfile=src/lib/config-schema.js",
23
- "prepare": "npm run build:schema"
23
+ "prepare": "npm run build:schema",
24
+ "test": "node --test \"scripts/*.test.js\""
24
25
  },
25
26
  "keywords": [
26
27
  "docs",
@@ -464,15 +464,33 @@ function issuesDoRamo(ramo) {
464
464
  const comIssues = ramo;
465
465
  return comIssues?.issues ?? [];
466
466
  }
467
+ function maiorProfundidade(lista) {
468
+ let maior = 0;
469
+ for (const issue of lista) {
470
+ const p = issue.caminho.length + (issue.code === "unrecognized_keys" ? 1 : 0);
471
+ if (p > maior) maior = p;
472
+ }
473
+ return maior;
474
+ }
475
+ const planoPorRamo = /* @__PURE__ */ new WeakMap();
476
+ function planoDoRamo(ramo) {
477
+ const lista = issuesDoRamo(ramo);
478
+ if (typeof ramo !== "object" || ramo === null) return desembrulharUnioes(lista);
479
+ const memoizado = planoPorRamo.get(ramo);
480
+ if (memoizado) return memoizado;
481
+ const plano = desembrulharUnioes(lista);
482
+ planoPorRamo.set(ramo, plano);
483
+ return plano;
484
+ }
467
485
  function melhorRamo(ramos) {
468
486
  return ramos.map((ramo) => {
469
- const lista = issuesDoRamo(ramo);
487
+ const plano = planoDoRamo(ramo);
470
488
  return {
471
- lista,
472
- n: lista.length,
473
- profundidade: Math.max(0, ...lista.map((i) => (i.path ?? []).length))
489
+ lista: issuesDoRamo(ramo),
490
+ n: plano.length,
491
+ profundidade: maiorProfundidade(plano)
474
492
  };
475
- }).sort((a, b) => a.n - b.n || b.profundidade - a.profundidade)[0].lista;
493
+ }).sort((a, b) => b.profundidade - a.profundidade || a.n - b.n)[0].lista;
476
494
  }
477
495
  function desembrulharUnioes(issues, prefixo = []) {
478
496
  const saida = [];
@@ -1062,15 +1062,84 @@ function issuesDoRamo(ramo: unknown): IssueCru[] {
1062
1062
  return comIssues?.issues ?? [];
1063
1063
  }
1064
1064
 
1065
+ /** O maior valor de `f` sobre a lista, ou 0 se ela for vazia. Um `for` e nao
1066
+ * `Math.max(0, ...lista.map(f))`: o spread vira argumentos de chamada, e uma
1067
+ * lista grande o bastante (config com dezenas de milhares de erros) estoura o
1068
+ * limite de argumentos com RangeError em vez de devolver um numero. */
1069
+ function maiorProfundidade(lista: IssuePlano[]): number {
1070
+ let maior = 0;
1071
+ for (const issue of lista) {
1072
+ const p = issue.caminho.length + (issue.code === 'unrecognized_keys' ? 1 : 0);
1073
+ if (p > maior) maior = p;
1074
+ }
1075
+ return maior;
1076
+ }
1077
+
1078
+ /** Plano ACHATADO de um ramo, memoizado por identidade do ramo.
1079
+ *
1080
+ * A memoizacao nao e otimizacao, e o que torna o achatamento viavel. Pontuar um
1081
+ * ramo exige desembrulhar as unioes DENTRO dele, e cada uma dessas tem os seus
1082
+ * proprios ramos - sem cache, a pontuacao visita o mesmo sub-ramo uma vez por
1083
+ * caminho que leva ate ele, o que e exponencial na profundidade do aninhamento.
1084
+ * Medido com grupos aninhados e um erro no fundo: 3,7 ms com 4 niveis, 281 ms
1085
+ * com 8, 69 SEGUNDOS com 12. Com o cache, os mesmos casos ficam em 0,2 ms,
1086
+ * porque cada no da arvore de erro do Zod e achatado uma vez so.
1087
+ *
1088
+ * Se alguem remover este WeakMap por parecer supérfluo, o `writedocs validate`
1089
+ * passa a travar em configs profundas. Nao remova. */
1090
+ const planoPorRamo = new WeakMap<object, IssuePlano[]>();
1091
+
1092
+ function planoDoRamo(ramo: unknown): IssuePlano[] {
1093
+ const lista = issuesDoRamo(ramo);
1094
+ if (typeof ramo !== 'object' || ramo === null) return desembrulharUnioes(lista);
1095
+ const memoizado = planoPorRamo.get(ramo);
1096
+ if (memoizado) return memoizado;
1097
+ // Prefixo vazio de proposito: a pontuacao compara ramos entre si, entao a
1098
+ // profundidade tem que ser relativa ao proprio ramo, nao ao documento.
1099
+ const plano = desembrulharUnioes(lista);
1100
+ planoPorRamo.set(ramo, plano);
1101
+ return plano;
1102
+ }
1103
+
1065
1104
  /** Escolhe qual ramo de uma uniao o cliente PROVAVELMENTE quis.
1066
1105
  *
1067
- * Criterio: menos issues primeiro; empate resolvido pelo caminho mais fundo. A
1106
+ * Criterio: caminho mais fundo primeiro; empate resolvido por menos issues. A
1068
1107
  * intuicao e a do Gabriel na spec - o ramo certo e o que nao reclama da chave
1069
1108
  * que o cliente usou -, e a profundidade importa porque um ramo que chegou
1070
1109
  * fundo antes de falhar (`products.0.product`) reconheceu a forma, enquanto um
1071
1110
  * que falha na raiz (`expected array, received object`) so recusou o shape.
1072
1111
  *
1073
- * EMPATE TOTAL (mesmo numero de issues E mesma profundidade): vence o ramo
1112
+ * DUAS CORRECOES sobre a primeira versao (WD-063), as duas medidas contra o
1113
+ * `00-kitchen-sink` real - ver o relato de revisao 43:
1114
+ *
1115
+ * 1. PONTUA O RAMO ACHATADO, nao a lista crua. Um ramo que e ele proprio uma
1116
+ * uniao aparece como UM issue (`invalid_union`) no caminho vazio - ou seja,
1117
+ * com a MENOR contagem e a MENOR profundidade possiveis, sem que isso diga
1118
+ * nada sobre o quanto ele reconheceu. Era o caso mais comum que existe: um
1119
+ * item de `pages` e `string | grupo | link`, e o grupo e outra uniao, entao
1120
+ * todo erro estrutural dentro de um grupo empatava com o ramo `string` e
1121
+ * perdia pela ordem de declaracao. O cliente que escrevia
1122
+ * `{ "group": "G", "pages": 42 }` recebia "expected string, received
1123
+ * object" - conselho errado, nao so inutil. Achatando primeiro, o mesmo caso
1124
+ * vira `pages.0.pages: expected array, received number`.
1125
+ *
1126
+ * 2. PROFUNDIDADE EFETIVA: `unrecognized_keys` reporta no caminho do PAI, com a
1127
+ * chave ofensora em `keys`, entao a profundidade dele sai subestimada em 1.
1128
+ * Sem a correcao, um ramo que identificou a chave errada perde para um que
1129
+ * so exigiu um campo ausente. (`validateDocsConfig` ja fazia a mesma conta
1130
+ * para achar a LINHA; aqui ela entra tambem na pontuacao.)
1131
+ *
1132
+ * Medido sobre as 60 folhas da `navigation` do `00-kitchen-sink`, trocando o
1133
+ * tipo de cada uma: a versao anterior apontava o campo exato em 42/60 e PARAVA
1134
+ * RASO em 18/60 (sempre os mesmos 18: os que ficam sob um `pages`); esta aponta
1135
+ * 60/60 para valor escalar errado e nunca para raso. Para um objeto vazio ou
1136
+ * parcial no lugar de uma pagina, as duas ficam tecnicamente empatadas (41
1137
+ * contra 42): a anterior diz "expected string", esta lista os campos que
1138
+ * faltam para ser um grupo. E o custo aceito desta versao, e substitui o antigo
1139
+ * (`tabs` e `products` escritos juntos, que agora sai como
1140
+ * `Unrecognized key: "products"`).
1141
+ *
1142
+ * EMPATE TOTAL (mesma profundidade E mesmo numero de issues): vence o ramo
1074
1143
  * declarado primeiro no schema. `Array.prototype.sort` e estavel desde a
1075
1144
  * ES2019, entao ordenar e pegar o [0] ja entrega isso - se alguem trocar por
1076
1145
  * uma ordenacao instavel, o desempate vira sorteio. Duas razoes para "o
@@ -1084,14 +1153,14 @@ function issuesDoRamo(ramo: unknown): IssueCru[] {
1084
1153
  function melhorRamo(ramos: unknown[]): IssueCru[] {
1085
1154
  return ramos
1086
1155
  .map((ramo) => {
1087
- const lista = issuesDoRamo(ramo);
1156
+ const plano = planoDoRamo(ramo);
1088
1157
  return {
1089
- lista,
1090
- n: lista.length,
1091
- profundidade: Math.max(0, ...lista.map((i) => (i.path ?? []).length)),
1158
+ lista: issuesDoRamo(ramo),
1159
+ n: plano.length,
1160
+ profundidade: maiorProfundidade(plano),
1092
1161
  };
1093
1162
  })
1094
- .sort((a, b) => a.n - b.n || b.profundidade - a.profundidade)[0].lista;
1163
+ .sort((a, b) => b.profundidade - a.profundidade || a.n - b.n)[0].lista;
1095
1164
  }
1096
1165
 
1097
1166
  /** Desembrulha `invalid_union` recursivamente - o ramo escolhido normalmente e