raptor-aios 0.15.0 → 0.16.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/CHANGELOG.md +117 -1
- package/README.md +1 -1
- package/dist/_core/dist/agents/prompt-builder.js +9 -5
- package/dist/_core/dist/audit/logger.js +16 -5
- package/dist/_core/dist/audit/schema.js +10 -0
- package/dist/_core/dist/audit/validate.js +15 -0
- package/dist/_core/dist/constitution/core.js +4 -0
- package/dist/_core/dist/constitution/index.js +1 -1
- package/dist/_core/dist/design/fetcher.js +12 -6
- package/dist/_core/dist/design/scaffold.js +65 -17
- package/dist/_core/dist/design/screens.js +5 -1
- package/dist/_core/dist/frontmatter/index.js +26 -0
- package/dist/_core/dist/gates/design-gates.js +135 -5
- package/dist/_core/dist/gates/m7-gates.js +5 -2
- package/dist/_core/dist/models/impl-log.js +22 -1
- package/dist/_core/dist/verify/fidelity.js +7 -3
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/plan.md.hbs +4 -6
- package/dist/_core/templates/spec.md.hbs +16 -8
- package/dist/commands/approve.js +5 -3
- package/dist/commands/clarify.js +2 -1
- package/dist/commands/design/fidelity.js +41 -10
- package/dist/commands/design/status.js +42 -6
- package/dist/commands/design/sync.js +1 -1
- package/dist/commands/doctor.js +0 -8
- package/dist/commands/implement.js +47 -0
- package/dist/commands/new.js +3 -3
- package/dist/commands/preset/add.js +8 -12
- package/dist/commands/trace.js +2 -0
- package/dist/commands/upgrade.js +10 -13
- package/dist/commands/verify/fidelity.js +4 -0
- package/dist/shared/init-core.js +2 -1
- package/dist/shared/scaffold.js +15 -16
- package/package.json +1 -1
- package/scripts/bash/common.sh +13 -8
- package/scripts/bash/create-new-feature.sh +67 -11
- package/scripts/prepare-npm.mjs +1 -1
- package/templates/commands/analyze.md +1 -1
- package/templates/commands/clarify.md +31 -12
- package/templates/commands/constitution.md +3 -3
- package/templates/commands/plan.md +2 -2
- package/templates/commands/specify.md +14 -2
- package/templates/plan-template.md +1 -1
- package/templates/spec-template.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,123 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [0.16.0] - 2026-06-24
|
|
7
|
+
|
|
8
|
+
Correções P0/P1/P2 do re-teste KAN-20 v2 (DuCorre/FastBoy): fechar o **processo
|
|
9
|
+
de fidelidade** para o erro não nascer, não só ser pego no fim, e endurecer
|
|
10
|
+
rastreabilidade/determinismo nas pontas (resiliência de gate, hash unificado,
|
|
11
|
+
backbone de fidelidade instalado, clarify determinístico, drift acionável). Esta
|
|
12
|
+
versão também **entrega os dois deferidos** — constituição em **fonte única** e
|
|
13
|
+
**edições pós-implement auditadas** — e incorpora os achados de uma **revisão
|
|
14
|
+
integrada adversarial** que a suíte verde escondia (corrupção latente do
|
|
15
|
+
`audit.jsonl` por campo `undefined`; vazamento residual do laço de fidelidade).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`gate.design.anatomy`** (M3, _required_) — a `## Anatomy` (contrato de fidelidade)
|
|
20
|
+
precisa **nascer do pixel real**: cada tela com anatomia tem que registrar um
|
|
21
|
+
`figma_screenshot:` (capturado via `get_screenshot`) presente em disco e ≥ 1 KB. Sem
|
|
22
|
+
isso o gate bloqueia o verify — impede o contrato de nascer cego e o `implement`
|
|
23
|
+
construir fielmente a coisa errada (F10-2). Os prompts (`spec.md.hbs`, `/raptor-specify`)
|
|
24
|
+
agora mandam capturar o screenshot ANTES de autorar a anatomia.
|
|
25
|
+
- **Backbone de fidelidade instalado no projeto** — `init`/`upgrade` agora copiam
|
|
26
|
+
`scripts/review/{hex-scan,diff-tokens}.mjs` e `scripts/app/screenshot.sh` para
|
|
27
|
+
`.raptor/scripts/`, e o prompt do scorer referencia `.raptor/scripts/...`. Antes os
|
|
28
|
+
comandos do scorer apontavam para `scripts/...` que nenhum comando instalava — hex-scan
|
|
29
|
+
e o screenshot do app ficavam indisponíveis no caminho documentado (F8-1).
|
|
30
|
+
- **`implement --finalize` audita edições pós-implement** — quando o finalize absorve um
|
|
31
|
+
artefato que mudou após ser registrado (drift), emite o evento `implement.drift_detected`
|
|
32
|
+
(dedup por `(path|hash)`, idempotente), tornando a edição pós-Fase-7 visível no trilho de
|
|
33
|
+
audit em vez de reconciliada em silêncio. A detecção é pura (`reconstructImplLogFromAudit`
|
|
34
|
+
expõe os drifts); o `verify` segue read-only e o gate M7 segue puro (F7-untracked).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **`raptor design fidelity` virou laço fechado** — sai **não-zero** enquanto houver
|
|
39
|
+
região < threshold, scorecard com problema ou `design-decision` pendente, e **0** só na
|
|
40
|
+
convergência. Um laço de agente/CI itera até passar, em vez de a convergência depender
|
|
41
|
+
de disciplina manual (F10-3). O `evaluateFeatureFidelity` agora expõe
|
|
42
|
+
`pendingDesignDecisions`, e tanto o loop quanto a lente `raptor verify fidelity` passam a
|
|
43
|
+
reportar **não-ok** numa `design-decision` pendente — alinhados com `gate.design.fidelity`
|
|
44
|
+
(antes a lente dava verde enquanto o gate bloqueava).
|
|
45
|
+
- **`raptor design fidelity` converge ao veredito do gate (vazamento residual do F10-3)** — a
|
|
46
|
+
lente do laço só olhava os scorecards, então o comando podia sair **0** ("converged") enquanto
|
|
47
|
+
`raptor verify` falharia por falta de evidência de screenshot ou cobertura por região da
|
|
48
|
+
anatomia. Agora o comando roda `gate.design.fidelity` e `gate.design.anatomy` como autoridade
|
|
49
|
+
da convergência (só engajam com spec aprovada). Um laço de agente que parava no verde otimista
|
|
50
|
+
passa a iterar até o gate de fato passar.
|
|
51
|
+
- **Constituição em fonte única** — `.raptor/constitution.md` passa a ser a **única** cópia; o
|
|
52
|
+
espelho `.raptor/memory/constitution.md` foi removido. `init` não o escreve mais, `upgrade`
|
|
53
|
+
remove o órfão (com backup antes), e `preset add`/`doctor` só olham o canônico. Os prompts
|
|
54
|
+
(`templates/commands/*`, `plan.md.hbs`), o `scripts/bash/common.sh` (`CONSTITUTION_PATH`) e os
|
|
55
|
+
docs foram repointados; snapshots E2E regenerados. Fim da duplicação que o `upgrade` mantinha
|
|
56
|
+
em sync (F5-1b). _(Scripts PowerShell ficam como follow-up.)_
|
|
57
|
+
- **Skill `/raptor-clarify` alinhada ao caminho determinístico** — a skill 2.1.0 deixou de
|
|
58
|
+
hand-editar `## Clarifications` num formato (`### Session` + `- Q:→A:`) divergente do CLI;
|
|
59
|
+
agora emite `answers.json` e roda `raptor clarify --apply` (o CLI é o dono único da seção,
|
|
60
|
+
formato canônico `- **label** — answer`, UPSERT idempotente). Fim do spec híbrido de dois
|
|
61
|
+
escritores (F3-1/2). Os snapshots E2E passam a cobrir `clarify` (trava o formato).
|
|
62
|
+
- **Drift do impl-log agora é acionável** — `gate.m7.diff_hash_matches` já re-hasha o working
|
|
63
|
+
tree no `verify` e flaga arquivo que mudou/sumiu após ser registrado; a mensagem agora nomeia
|
|
64
|
+
a causa (**impl-log STALE** — edição pós-implement não re-logada) e a remediação
|
|
65
|
+
(`raptor implement --finalize` ou reverter) (F7-stale).
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **`.gitignore` não engole mais os assets do app** — o `--design-fetch` deixou de
|
|
70
|
+
ignorar os diretórios `assets/{images,icons,fonts}` inteiros; agora ignora apenas os
|
|
71
|
+
arquivos exatos que o RAPTOR baixou (bloco gerenciado único, idempotente). Assets
|
|
72
|
+
próprios do app (fontes, imagens de onboarding) param de sumir silenciosamente do
|
|
73
|
+
controle de versão (F1-2).
|
|
74
|
+
- **`/raptor-specify` não duplica mais uma feature já seedada** — `create-new-feature.sh`
|
|
75
|
+
ganhou guarda determinística por **slug** (espelhando o `raptor new`): um
|
|
76
|
+
`.raptor/specs/NNN-<slug>` existente é **reusado** (`REUSED: true`), não recriado com um
|
|
77
|
+
número novo que orfanaria a feature seedada (F2-1). `--number` e `--timestamp` optam por
|
|
78
|
+
fora.
|
|
79
|
+
- **`gate.design.ready` resiliente a YAML inválido** — um campo irmão malformado no
|
|
80
|
+
frontmatter de uma tela (ex.: um `screen_role` não-quotado escrito pelo agente) fazia o
|
|
81
|
+
`parseFrontmatter` lançar e o gate descartar o `name`, caindo no slug do arquivo que nunca
|
|
82
|
+
casa (`[screen: X]` reportado como ausente). Novo helper `frontmatterField()` lê o campo
|
|
83
|
+
por line-scan quando o parse YAML falha; usado em `gate.design.ready` e `gate.design.anatomy`
|
|
84
|
+
(F9-1).
|
|
85
|
+
- **Hash da constituição unificado** — dentro do mesmo comando, o prompt-context gravava o
|
|
86
|
+
hash whole-file enquanto `plan`/audit gravavam o hash do bloco RAPTOR-CORE. `buildCanonicalPrompt`
|
|
87
|
+
passa a registrar o **mesmo** core-block hash (helper `constitutionHashFromContent`), então
|
|
88
|
+
prompt e artefato concordam em "qual constituição governou isto" (F5-1).
|
|
89
|
+
- **Serializer do `audit.jsonl` robusto a `undefined`** — o `stableStringify` emitia
|
|
90
|
+
`"chave":undefined` (JSON inválido) para uma propriedade com valor `undefined`, corrompendo a
|
|
91
|
+
linha e a cadeia `prev_hash` a partir dela — e isso **disparava de fato** em `raptor plan`/`init`
|
|
92
|
+
num projeto sem preset. Agora omite chaves `undefined` e mapeia `undefined` em array para `null`
|
|
93
|
+
(espelhando `JSON.stringify`). Achado da revisão integrada adversarial.
|
|
94
|
+
- **`upgrade` não apaga mais o espelho da constituição sem backup** (`backup()` antes do `rmSync`),
|
|
95
|
+
e a regex que remove a chave morta `branch_prefix` foi **ancorada na coluna 0** (não casa uma
|
|
96
|
+
chave homônima aninhada sob outra seção). Achados da revisão integrada.
|
|
97
|
+
|
|
98
|
+
#### Polimento (P2)
|
|
99
|
+
|
|
100
|
+
- **`raptor approve` emite `spec.approved`** (evento distinto, não mais o genérico `spec.updated`) e
|
|
101
|
+
grava `audit_ref` no frontmatter apontando para o id da entrada do audit — link bidirecional
|
|
102
|
+
artefato↔audit (C4), antes sempre null (F4-1/F4-2).
|
|
103
|
+
- **`clarify --apply` faz backup do spec** antes de sobrescrever (a recuperação não depende mais só
|
|
104
|
+
do ledger/hashes) (F3-3).
|
|
105
|
+
- **Warning de scope não empurra mais `--design-scope file`** quando outras refs foram capturadas —
|
|
106
|
+
uma ref sem node-id agora vira aviso suave ("linke o frame específico"), e o escape hatch
|
|
107
|
+
file-wide só aparece quando NADA foi capturado (F1-warn).
|
|
108
|
+
- **Nome de tela duplicado é desambiguado** — dois frames Figma com o mesmo `name` ganham um sufixo
|
|
109
|
+
de node-id no 2º+ , para `[screen: X]` resolver para um arquivo só (F1-dup).
|
|
110
|
+
- **`audit` `file_keys` deduplicado**; **tabela `Post-Design Check` do `plan.md.hbs`** consertada
|
|
111
|
+
(separador/linha partidos quebravam a renderização) (F5-2).
|
|
112
|
+
- **`audit.jsonl` ganha cadeia tamper-evident** — cada evento grava `prev_hash` (sha256 da linha
|
|
113
|
+
anterior; null no genesis), e `raptor verify audit --strict` valida a cadeia e detecta adulteração
|
|
114
|
+
de eventos passados. Campo opcional no schema (back-compat com logs pré-cadeia) (prev_hash).
|
|
115
|
+
- **Branch do `/raptor-specify` (bash) alinhada à convenção do `raptor new`** — o branch agora carrega
|
|
116
|
+
o prefixo `feat/` (`feat/NNN-slug`); o diretório da spec continua o `NNN-slug` puro. `get_next_feature_number`
|
|
117
|
+
e `check_feature_branch` passam a entender o prefixo. `raptor upgrade` remove a chave morta
|
|
118
|
+
`branch_prefix` de projetos legados e o label "legacy cleanup" só aparece quando algo foi de fato
|
|
119
|
+
limpo (F2-2 / F0-2 / F0-3).
|
|
120
|
+
- **`raptor design status --check`** valida o token figma-rest de verdade (chamada `/v1/me`): um token
|
|
121
|
+
clobberado/revogado-mas-presente agora reporta NOT connected, em vez do falso "connected" por
|
|
122
|
+
presença (F0-1).
|
|
7
123
|
|
|
8
124
|
## [0.15.0] - 2026-06-22
|
|
9
125
|
|
package/README.md
CHANGED
|
@@ -237,7 +237,7 @@ Você os executa **dentro do seu agente de IA**. São prompts versionados e rico
|
|
|
237
237
|
|
|
238
238
|
### 🧭 `/raptor-constitution`
|
|
239
239
|
|
|
240
|
-
Cria ou atualiza os princípios do projeto (`.raptor/
|
|
240
|
+
Cria ou atualiza os princípios do projeto (`.raptor/constitution.md`). É o "contrato social" que todas as fases seguintes respeitam. Use no início, ou quando quiser ratificar uma mudança de regra (com bump de versão).
|
|
241
241
|
|
|
242
242
|
### 📝 `/raptor-specify`
|
|
243
243
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { hashString, hashFile } from "../audit/hash.js";
|
|
4
|
+
import { constitutionHashFromContent } from "../constitution/core.js";
|
|
4
5
|
import { wrapSpec, wrapUntrusted } from "./prompt-markers.js";
|
|
5
6
|
const PHASE_MAP = {
|
|
6
7
|
new: {
|
|
@@ -80,14 +81,14 @@ const PHASE_MAP = {
|
|
|
80
81
|
"2. Extraia do Figma os valores REAIS do nó com `get_design_context` / `get_variable_defs` /",
|
|
81
82
|
" `get_screenshot`. REGRA DE OURO: o nó vence a percepção — nunca chute px/cor/raio/peso.",
|
|
82
83
|
"3. Capture o app RODANDO (evidência, não invente): rode",
|
|
83
|
-
"
|
|
84
|
+
" `.raptor/scripts/app/screenshot.sh design/visual-review/_shots/<tela>.<estado>.app.png` e salve o",
|
|
84
85
|
" screenshot do Figma como `…<estado>.figma.png`.",
|
|
85
86
|
"4. Compare Figma×app POR MICRO-REGIÃO e pontue 60–100 em cada dimensão",
|
|
86
87
|
" (layout, spacing, color, typography, dimensions, icon, states). A nota da região = a",
|
|
87
88
|
" dimensão MAIS FRACA (o elo). Mínimo de aprovação = 95 (ou o threshold do preset).",
|
|
88
|
-
"5. Backbone determinístico (não falsifique): rode `node scripts/review/hex-scan.mjs src --json`",
|
|
89
|
+
"5. Backbone determinístico (não falsifique): rode `node .raptor/scripts/review/hex-scan.mjs src --json`",
|
|
89
90
|
" e preencha `deterministic.hex_hardcoded` com o resultado REAL; rode",
|
|
90
|
-
" `node scripts/review/diff-tokens.mjs <figma-tokens.json> design/tokens.json --json` p/ o drift.",
|
|
91
|
+
" `node .raptor/scripts/review/diff-tokens.mjs <figma-tokens.json> design/tokens.json --json` p/ o drift.",
|
|
91
92
|
"6. Registre cada divergência `{ dimension, figma, rn, fix, severity, status }`. `status`:",
|
|
92
93
|
" `resolved` (corrigida), `open` (ainda falha) ou `design-decision` (desvio semântico —",
|
|
93
94
|
" troca de ícone/cor/copy — que EXIGE aprovação humana antes de passar o gate).",
|
|
@@ -284,7 +285,10 @@ export function buildCanonicalPrompt(input) {
|
|
|
284
285
|
? join(raptorDir, "constitution.md")
|
|
285
286
|
: join(featureDir, inp.filename);
|
|
286
287
|
if (existsSync(path)) {
|
|
287
|
-
|
|
288
|
+
const hash = inp.filename === "constitution.md"
|
|
289
|
+
? constitutionHashFromContent(readFileSync(path, "utf8"))
|
|
290
|
+
: hashFile(path);
|
|
291
|
+
inputs.push({ path: inp.filename, hash, role: inp.role });
|
|
288
292
|
}
|
|
289
293
|
}
|
|
290
294
|
let body = phase.bodyTemplate;
|
|
@@ -57,15 +57,25 @@ export function nextSequence(jsonlPath) {
|
|
|
57
57
|
}
|
|
58
58
|
return lines.length + 1;
|
|
59
59
|
}
|
|
60
|
+
export function lastEventHash(jsonlPath) {
|
|
61
|
+
if (!existsSync(jsonlPath))
|
|
62
|
+
return null;
|
|
63
|
+
const trimmed = readFileSync(jsonlPath, "utf8").trimEnd();
|
|
64
|
+
if (trimmed.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
const lines = trimmed.split("\n");
|
|
67
|
+
return hashString(lines[lines.length - 1]);
|
|
68
|
+
}
|
|
60
69
|
function stableStringify(obj) {
|
|
61
70
|
if (obj === null || typeof obj !== "object")
|
|
62
71
|
return JSON.stringify(obj);
|
|
63
72
|
if (Array.isArray(obj))
|
|
64
|
-
return "[" +
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
return ("[" +
|
|
74
|
+
obj.map((v) => (v === undefined ? "null" : stableStringify(v))).join(",") +
|
|
75
|
+
"]");
|
|
76
|
+
const o = obj;
|
|
77
|
+
const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort();
|
|
78
|
+
const parts = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(o[k]));
|
|
69
79
|
return "{" + parts.join(",") + "}";
|
|
70
80
|
}
|
|
71
81
|
export function appendAuditEvent(jsonlPath, event) {
|
|
@@ -75,6 +85,7 @@ export function appendAuditEvent(jsonlPath, event) {
|
|
|
75
85
|
id: id ?? uuidv7(),
|
|
76
86
|
sequence: sequence ?? nextSequence(jsonlPath),
|
|
77
87
|
ts: ts ?? new Date().toISOString(),
|
|
88
|
+
prev_hash: lastEventHash(jsonlPath),
|
|
78
89
|
...rest,
|
|
79
90
|
};
|
|
80
91
|
assertC5(full);
|
|
@@ -27,6 +27,10 @@ export const auditEventSchema = {
|
|
|
27
27
|
format: "date-time",
|
|
28
28
|
description: "ISO 8601 with timezone, millisecond precision recommended.",
|
|
29
29
|
},
|
|
30
|
+
prev_hash: {
|
|
31
|
+
type: ["string", "null"],
|
|
32
|
+
description: "sha256 of the previous event's serialized line — a tamper-evident chain; null for the genesis event. Optional for back-compat with pre-chain logs.",
|
|
33
|
+
},
|
|
30
34
|
event: {
|
|
31
35
|
type: "string",
|
|
32
36
|
enum: [
|
|
@@ -34,6 +38,7 @@ export const auditEventSchema = {
|
|
|
34
38
|
"feature.archived",
|
|
35
39
|
"spec.generated",
|
|
36
40
|
"spec.updated",
|
|
41
|
+
"spec.approved",
|
|
37
42
|
"clarify.added",
|
|
38
43
|
"clarify.scanned",
|
|
39
44
|
"clarify.applied",
|
|
@@ -48,6 +53,7 @@ export const auditEventSchema = {
|
|
|
48
53
|
"checklist.generated",
|
|
49
54
|
"checklist.created",
|
|
50
55
|
"task.completed",
|
|
56
|
+
"implement.drift_detected",
|
|
51
57
|
"gate.evaluated",
|
|
52
58
|
"gate.skipped",
|
|
53
59
|
"gate.approved",
|
|
@@ -152,6 +158,10 @@ export const auditEventSchema = {
|
|
|
152
158
|
if: { properties: { event: { const: "code.generated" } } },
|
|
153
159
|
then: { required: ["outputs", "task"] },
|
|
154
160
|
},
|
|
161
|
+
{
|
|
162
|
+
if: { properties: { event: { const: "implement.drift_detected" } } },
|
|
163
|
+
then: { required: ["outputs", "detector"] },
|
|
164
|
+
},
|
|
155
165
|
{
|
|
156
166
|
if: {
|
|
157
167
|
properties: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { Ajv2020, } from "ajv/dist/2020.js";
|
|
3
3
|
import { auditEventSchema } from "./schema.js";
|
|
4
|
+
import { hashString } from "./hash.js";
|
|
4
5
|
let cachedValidator = null;
|
|
5
6
|
function getValidator() {
|
|
6
7
|
if (cachedValidator)
|
|
@@ -75,6 +76,7 @@ export function validateAuditFile(jsonlPath, opts = {}) {
|
|
|
75
76
|
let validLines = 0;
|
|
76
77
|
let prevSequence = 0;
|
|
77
78
|
let prevTs = "";
|
|
79
|
+
let prevRaw = null;
|
|
78
80
|
for (let i = 0; i < lines.length; i++) {
|
|
79
81
|
const lineNum = i + 1;
|
|
80
82
|
const raw = lines[i];
|
|
@@ -87,6 +89,8 @@ export function validateAuditFile(jsonlPath, opts = {}) {
|
|
|
87
89
|
});
|
|
88
90
|
continue;
|
|
89
91
|
}
|
|
92
|
+
const prevLine = prevRaw;
|
|
93
|
+
prevRaw = raw;
|
|
90
94
|
let parsed;
|
|
91
95
|
try {
|
|
92
96
|
parsed = JSON.parse(raw);
|
|
@@ -125,6 +129,17 @@ export function validateAuditFile(jsonlPath, opts = {}) {
|
|
|
125
129
|
keyword: "monotonicTs",
|
|
126
130
|
});
|
|
127
131
|
}
|
|
132
|
+
if (obj.prev_hash !== undefined) {
|
|
133
|
+
const expected = prevLine === null ? null : hashString(prevLine);
|
|
134
|
+
if (obj.prev_hash !== expected) {
|
|
135
|
+
errors.push({
|
|
136
|
+
line: lineNum,
|
|
137
|
+
path: "/prev_hash",
|
|
138
|
+
message: `prev_hash chain broken — expected ${expected ?? "null (genesis)"}, got ${obj.prev_hash ?? "null"}`,
|
|
139
|
+
keyword: "chain",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
128
143
|
prevSequence = obj.sequence;
|
|
129
144
|
prevTs = obj.ts;
|
|
130
145
|
}
|
|
@@ -89,6 +89,10 @@ export function extractCoreBlock(source) {
|
|
|
89
89
|
endIndex: endMarkerIdx + CORE_END_MARKER.length,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
export function constitutionHashFromContent(content) {
|
|
93
|
+
const extracted = extractCoreBlock(content);
|
|
94
|
+
return extracted ? extracted.declaredHash : hashString(content);
|
|
95
|
+
}
|
|
92
96
|
export function rewriteCoreBlock(source) {
|
|
93
97
|
const canonical = coreBlock();
|
|
94
98
|
const extracted = extractCoreBlock(source);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { CORE_VERSION, CORE_ARTICLES_TEXT, CORE_END_MARKER, CORE_START_MARKER_PREFIX, CORE_START_MARKER_RE, coreBlock, coreHash, extractCoreBlock, rewriteCoreBlock, } from "./core.js";
|
|
1
|
+
export { CORE_VERSION, CORE_ARTICLES_TEXT, CORE_END_MARKER, CORE_START_MARKER_PREFIX, CORE_START_MARKER_RE, coreBlock, coreHash, constitutionHashFromContent, extractCoreBlock, rewriteCoreBlock, } from "./core.js";
|
|
@@ -6,15 +6,19 @@ function warn(opts, message) {
|
|
|
6
6
|
else
|
|
7
7
|
process.stderr.write(`Warning: ${message}\n`);
|
|
8
8
|
}
|
|
9
|
-
function scopeSkipWarning(url) {
|
|
9
|
+
function scopeSkipWarning(url, someCaptured) {
|
|
10
|
+
if (someCaptured) {
|
|
11
|
+
return (`ref sem node-id (link sem ?node-id=); só ESTE link não foi capturado — ` +
|
|
12
|
+
`linke o frame específico no card para incluí-lo: ${url}`);
|
|
13
|
+
}
|
|
10
14
|
return (`ref sem node-id (link sem ?node-id=); telas/assets não capturados — ` +
|
|
11
|
-
`rode com --design-scope file para o arquivo inteiro
|
|
15
|
+
`linke o frame no card, ou rode com --design-scope file para o arquivo inteiro: ${url}`);
|
|
12
16
|
}
|
|
13
|
-
function warnOnce(opts, seen, url) {
|
|
17
|
+
function warnOnce(opts, seen, url, someCaptured) {
|
|
14
18
|
if (seen.has(url))
|
|
15
19
|
return;
|
|
16
20
|
seen.add(url);
|
|
17
|
-
warn(opts, scopeSkipWarning(url));
|
|
21
|
+
warn(opts, scopeSkipWarning(url, someCaptured));
|
|
18
22
|
}
|
|
19
23
|
export async function fetchDesignData(refs, client, opts = {}) {
|
|
20
24
|
const varMap = {};
|
|
@@ -59,9 +63,10 @@ async function captureScreens(refs, client, opts, scopeWarned) {
|
|
|
59
63
|
const scope = opts.scope ?? "node";
|
|
60
64
|
const out = [];
|
|
61
65
|
const seen = new Set();
|
|
66
|
+
const someCaptured = refs.some((r) => Boolean(r.nodeId));
|
|
62
67
|
for (const ref of refs) {
|
|
63
68
|
if (scope === "node" && !ref.nodeId) {
|
|
64
|
-
warnOnce(opts, scopeWarned, ref.url);
|
|
69
|
+
warnOnce(opts, scopeWarned, ref.url, someCaptured);
|
|
65
70
|
continue;
|
|
66
71
|
}
|
|
67
72
|
try {
|
|
@@ -90,10 +95,11 @@ async function captureAssets(refs, client, typography, opts, scopeWarned) {
|
|
|
90
95
|
const pending = [];
|
|
91
96
|
const seenPaths = new Set();
|
|
92
97
|
const scope = opts.scope ?? "node";
|
|
98
|
+
const someCaptured = refs.some((r) => Boolean(r.nodeId));
|
|
93
99
|
if (client.captureAssets) {
|
|
94
100
|
for (const ref of refs) {
|
|
95
101
|
if (scope === "node" && !ref.nodeId) {
|
|
96
|
-
warnOnce(opts, scopeWarned, ref.url);
|
|
102
|
+
warnOnce(opts, scopeWarned, ref.url, someCaptured);
|
|
97
103
|
continue;
|
|
98
104
|
}
|
|
99
105
|
try {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { isTokensEmpty, parseDesignTokens } from "./tokens.js";
|
|
4
4
|
import { parseAssetManifest } from "./manifest.js";
|
|
5
5
|
import { renderScreenMarkdown, screenSlug } from "./screens.js";
|
|
6
|
-
import { slugify } from "./slug.js";
|
|
6
|
+
import { normalizePath, slugify } from "./slug.js";
|
|
7
7
|
import { ASSET_MANIFEST_SCHEMA, } from "./types.js";
|
|
8
8
|
export const ASSET_DIRS = [
|
|
9
9
|
"assets/images",
|
|
@@ -81,28 +81,64 @@ export function writeDesignScaffold(featureDir, root, refs) {
|
|
|
81
81
|
created.push(`${dir}/.gitkeep`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
if (ensureAssetGitignore(root)) {
|
|
85
|
-
created.push(".gitignore");
|
|
86
|
-
}
|
|
87
84
|
return created;
|
|
88
85
|
}
|
|
89
86
|
const GITIGNORE_BEGIN = "# RAPTOR design assets";
|
|
90
87
|
const GITIGNORE_END = "# /RAPTOR design assets";
|
|
91
|
-
function ensureAssetGitignore(root) {
|
|
88
|
+
function ensureAssetGitignore(root, relPaths) {
|
|
92
89
|
try {
|
|
90
|
+
const wanted = [...new Set(relPaths.map((p) => normalizePath(p)).filter(Boolean))];
|
|
91
|
+
if (wanted.length === 0)
|
|
92
|
+
return false;
|
|
93
93
|
const gitignorePath = join(root, ".gitignore");
|
|
94
|
-
const existing = existsSync(gitignorePath)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
|
|
95
|
+
const lines = existing.split(/\r?\n/);
|
|
96
|
+
let begin = -1;
|
|
97
|
+
let end = -1;
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
if (begin === -1 && lines[i].trim() === GITIGNORE_BEGIN)
|
|
100
|
+
begin = i;
|
|
101
|
+
else if (begin !== -1 && lines[i].trim() === GITIGNORE_END) {
|
|
102
|
+
end = i;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const wellFormed = begin !== -1 && end !== -1;
|
|
107
|
+
if (begin !== -1 && end === -1)
|
|
108
|
+
end = lines.length;
|
|
109
|
+
const hasBlock = begin !== -1;
|
|
110
|
+
const blockEntries = hasBlock
|
|
111
|
+
? lines.slice(begin + 1, end).map((l) => l.trim()).filter(Boolean)
|
|
112
|
+
: [];
|
|
113
|
+
const outside = new Set((hasBlock ? [...lines.slice(0, begin), ...lines.slice(end + 1)] : lines).map((l) => l.trim()));
|
|
114
|
+
const managed = new Set(blockEntries);
|
|
115
|
+
for (const p of wanted) {
|
|
116
|
+
if (!outside.has(p) && !outside.has(`/${p}`))
|
|
117
|
+
managed.add(p);
|
|
118
|
+
}
|
|
119
|
+
const newEntries = [...managed].sort();
|
|
120
|
+
if (newEntries.length === 0)
|
|
98
121
|
return false;
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
if (missing.length === 0)
|
|
122
|
+
const oldSorted = [...new Set(blockEntries)].sort();
|
|
123
|
+
if (wellFormed && newEntries.length === oldSorted.length && newEntries.every((e, i) => e === oldSorted[i])) {
|
|
102
124
|
return false;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
}
|
|
126
|
+
let outLines;
|
|
127
|
+
if (hasBlock) {
|
|
128
|
+
outLines = [...lines.slice(0, begin), GITIGNORE_BEGIN, ...newEntries, GITIGNORE_END, ...lines.slice(end + 1)];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
outLines = existing.length ? [...lines] : [];
|
|
132
|
+
while (outLines.length && outLines[outLines.length - 1].trim() === "")
|
|
133
|
+
outLines.pop();
|
|
134
|
+
if (outLines.length)
|
|
135
|
+
outLines.push("");
|
|
136
|
+
outLines.push(GITIGNORE_BEGIN, ...newEntries, GITIGNORE_END);
|
|
137
|
+
}
|
|
138
|
+
let next = outLines.join("\n");
|
|
139
|
+
if (!next.endsWith("\n"))
|
|
140
|
+
next += "\n";
|
|
141
|
+
writeFileSync(gitignorePath, next);
|
|
106
142
|
return true;
|
|
107
143
|
}
|
|
108
144
|
catch {
|
|
@@ -145,6 +181,7 @@ export function seedDesignArtifacts(featureDir, root, capture, refs) {
|
|
|
145
181
|
}
|
|
146
182
|
const screensDir = join(designDir, "screens");
|
|
147
183
|
const used = new Set();
|
|
184
|
+
const usedNames = new Set();
|
|
148
185
|
for (const screen of capture.screens) {
|
|
149
186
|
const base = screenSlug(screen);
|
|
150
187
|
let slug = screen.nodeId ? `${base}-${slugify(screen.nodeId)}` : base;
|
|
@@ -154,9 +191,20 @@ export function seedDesignArtifacts(featureDir, root, capture, refs) {
|
|
|
154
191
|
n++;
|
|
155
192
|
}
|
|
156
193
|
used.add(slug);
|
|
157
|
-
|
|
194
|
+
const nameKey = slugify(screen.name);
|
|
195
|
+
const screenToRender = nameKey && usedNames.has(nameKey) && screen.nodeId
|
|
196
|
+
? { ...screen, name: `${screen.name} (${screen.nodeId})` }
|
|
197
|
+
: screen;
|
|
198
|
+
if (nameKey)
|
|
199
|
+
usedNames.add(nameKey);
|
|
200
|
+
if (writeIfMissing(join(screensDir, `${slug}.md`), renderScreenMarkdown(screenToRender))) {
|
|
158
201
|
result.screensWritten.push(slug);
|
|
159
202
|
}
|
|
160
203
|
}
|
|
204
|
+
const downloaded = capture.assets
|
|
205
|
+
.filter((a) => a.status === "downloaded")
|
|
206
|
+
.map((a) => a.path);
|
|
207
|
+
if (downloaded.length > 0)
|
|
208
|
+
ensureAssetGitignore(root, downloaded);
|
|
161
209
|
return result;
|
|
162
210
|
}
|
|
@@ -213,7 +213,11 @@ export function renderScreenMarkdown(screen) {
|
|
|
213
213
|
data["summary"] = screen.summary;
|
|
214
214
|
const display = bodyName(screen.name);
|
|
215
215
|
const body = `\n<!-- Screen captured from Figma. Flesh out: states, navigation, components, data, edge cases.\n` +
|
|
216
|
-
` Reference it from the spec/tasks as [screen: ${display}]
|
|
216
|
+
` Reference it from the spec/tasks as [screen: ${display}].\n` +
|
|
217
|
+
` Before writing the \`## Anatomy\` fidelity contract, CAPTURE this frame with the Figma MCP\n` +
|
|
218
|
+
` get_screenshot, save the PNG under design/screens/, and record its path in this file's\n` +
|
|
219
|
+
` frontmatter under \`figma_screenshot:\`. Derive every region/element from that real pixel —\n` +
|
|
220
|
+
` gate.design.anatomy blocks an anatomy authored without a captured reference. -->\n\n` +
|
|
217
221
|
`## ${display}\n\n` +
|
|
218
222
|
`_Placeholder — detail this screen during \`/raptor-specify\`._\n`;
|
|
219
223
|
return stringifyFrontmatter(data, body);
|
|
@@ -11,6 +11,32 @@ export function parseFrontmatter(source) {
|
|
|
11
11
|
const data = (yamlBlock.trim().length === 0 ? {} : (parseYaml(yamlBlock) ?? {}));
|
|
12
12
|
return { data, body, hasFrontmatter: true, raw: source };
|
|
13
13
|
}
|
|
14
|
+
export function frontmatterField(source, field) {
|
|
15
|
+
const m = source.match(FRONTMATTER_RE);
|
|
16
|
+
if (!m)
|
|
17
|
+
return undefined;
|
|
18
|
+
const block = m[1] ?? '';
|
|
19
|
+
try {
|
|
20
|
+
const data = parseYaml(block);
|
|
21
|
+
const v = data == null ? undefined : data[field];
|
|
22
|
+
if (typeof v === 'string')
|
|
23
|
+
return v;
|
|
24
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
25
|
+
return String(v);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
const lm = block.match(new RegExp(`^${escaped}[ \\t]*:[ \\t]*(.*\\S)[ \\t]*$`, 'm'));
|
|
31
|
+
if (!lm)
|
|
32
|
+
return undefined;
|
|
33
|
+
let val = lm[1].trim();
|
|
34
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
35
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
36
|
+
val = val.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
return val.trim() || undefined;
|
|
39
|
+
}
|
|
14
40
|
export function stringifyFrontmatter(data, body) {
|
|
15
41
|
const yamlBlock = stringifyYaml(data).trimEnd();
|
|
16
42
|
const trimmedBody = body.startsWith('\n') ? body : '\n' + body;
|