sinapse-ai 1.15.0 → 1.17.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/.claude/hooks/doc-first-gate.cjs +156 -0
- package/.claude/hooks/enforce-story-gate.cjs +6 -2
- package/.claude/rules/hook-governance.md +1 -0
- package/.sinapse-ai/constitution.md +1 -1
- package/.sinapse-ai/core/atlas/atlas-data.js +278 -0
- package/.sinapse-ai/core/atlas/flows-pt.js +253 -0
- package/.sinapse-ai/core/atlas/flows.js +266 -0
- package/.sinapse-ai/core/atlas/index.js +62 -0
- package/.sinapse-ai/core/atlas/render-html.js +216 -0
- package/.sinapse-ai/core/atlas/render-markdown.js +313 -0
- package/.sinapse-ai/core/atlas/render-research-card.js +164 -0
- package/.sinapse-ai/core/orchestration/build-command.js +136 -0
- package/.sinapse-ai/core/orchestration/doc-first-resolver.js +322 -0
- package/.sinapse-ai/core/orchestration/route-command.js +97 -0
- package/.sinapse-ai/data/entity-registry.yaml +113 -16
- package/.sinapse-ai/development/agents/snps-orqx.md +9 -0
- package/.sinapse-ai/development/tasks/create-doc.md +1 -1
- package/.sinapse-ai/development/tasks/create-next-story.md +2 -3
- package/.sinapse-ai/development/tasks/spec-analyze.md +460 -0
- package/.sinapse-ai/development/tasks/spec-clarify.md +374 -0
- package/.sinapse-ai/development/tasks/spec-gather-requirements.md +11 -0
- package/.sinapse-ai/development/templates/approval-table.md +106 -0
- package/.sinapse-ai/development/workflows/spec-pipeline.yaml +122 -7
- package/.sinapse-ai/install-manifest.yaml +70 -18
- package/.sinapse-ai/product/templates/prd-tmpl.yaml +6 -2
- package/.sinapse-ai/product/templates/spec-tmpl.md +18 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +18 -3
- package/bin/sinapse.js +121 -0
- package/docs/framework/atlas/OPERATING-ATLAS.md +537 -0
- package/docs/framework/atlas/README.md +34 -0
- package/docs/framework/atlas/atlas-data.json +2810 -0
- package/docs/framework/atlas/atlas.html +392 -0
- package/package.json +1 -1
- package/packages/installer/src/wizard/ide-config-generator.js +6 -0
- package/scripts/install-chrome-brain.sh +2 -2
- package/scripts/validate-no-personal-leaks.js +24 -9
- package/squads/squad-copy/knowledge-base/consequence-headline-patterns.md +2 -2
- package/docs/guides/hooks-two-layers.md +0 -66
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render — SINAPSE research card (PT-BR).
|
|
3
|
+
*
|
|
4
|
+
* Emits the SINAPSE framework as a case-study card in the same template as the
|
|
5
|
+
* engineering-software research corpus (domain cards + LOOPS-*-diagramas.md),
|
|
6
|
+
* fully in Portuguese. One source (the live atlas `data` + flows-pt translations)
|
|
7
|
+
* → two markdown files, so the card never drifts from how the framework actually
|
|
8
|
+
* works. Regenerated by `sinapse atlas --research-card`.
|
|
9
|
+
*
|
|
10
|
+
* Pure string builder: no fs, no Date — caller passes data.generatedAt and writes.
|
|
11
|
+
*
|
|
12
|
+
* @module core/atlas/render-research-card
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { FRAMEWORK_FLOWS_PT } = require('./flows-pt');
|
|
18
|
+
|
|
19
|
+
/** Roman-numeral severity → short PT label is not needed; severity is kept verbatim. */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the SINAPSE case-study card markdown (PT-BR).
|
|
23
|
+
* @param {object} data - atlas data (buildAtlasData output)
|
|
24
|
+
* @param {Record<string,object>} pt - flow id → PT translation
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function buildCard(data, pt) {
|
|
28
|
+
const c = data.counts;
|
|
29
|
+
const date = (data.generatedAt || '').slice(0, 10);
|
|
30
|
+
const flows = data.flows || [];
|
|
31
|
+
|
|
32
|
+
const flowList = flows
|
|
33
|
+
.map((f) => {
|
|
34
|
+
const t = pt[f.id] || { titulo: f.title, proposito: f.purpose };
|
|
35
|
+
return `- **${t.titulo}** — ${t.proposito}`;
|
|
36
|
+
})
|
|
37
|
+
.join('\n');
|
|
38
|
+
|
|
39
|
+
const articleRows = (data.articles || [])
|
|
40
|
+
.map((a) => `| ${a.number} | ${a.title} | ${a.severity} |`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
|
|
43
|
+
return `---
|
|
44
|
+
titulo: "SINAPSE — Framework de Engenharia Orientada por IA (estudo de caso)"
|
|
45
|
+
tipo: estudo-de-caso
|
|
46
|
+
dominio: sinapse-framework
|
|
47
|
+
status: instancia-viva
|
|
48
|
+
prioridade: alta
|
|
49
|
+
created: ${date}
|
|
50
|
+
gerado_por: "sinapse atlas --research-card (auto-regenerável a partir do disco)"
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# SINAPSE — Framework de Engenharia Orientada por IA
|
|
54
|
+
|
|
55
|
+
> O SINAPSE é a instância viva dos domínios desta pesquisa: um meta-framework que impõe processo (spec antes de código, gates inegociáveis, delegação a especialistas) e orquestra ${c.agentsTotal} agentes em ${c.squads} squads para construir software com rigor.
|
|
56
|
+
> Este card é gerado do próprio framework (\`sinapse atlas\`), então descreve o que ele **realmente** faz hoje — não uma intenção desatualizada.
|
|
57
|
+
|
|
58
|
+
## Por que este estudo de caso importa
|
|
59
|
+
|
|
60
|
+
Os ${60} domínios desta pesquisa descrevem **como** construir software de qualidade com IA. O SINAPSE é uma resposta concreta a "como operacionalizar tudo isso de forma reproduzível": transforma princípios (Building Effective Agents, spec-driven development, context engineering, evals como gate) em **enforcement automático** — hooks que bloqueiam, gates que barram, orquestradores que delegam. Ler este card lado a lado com os domínios mostra a ponte entre teoria de engenharia aumentada por IA e um sistema sociotécnico em produção.
|
|
61
|
+
|
|
62
|
+
## O que é o SINAPSE (números exatos, do disco)
|
|
63
|
+
|
|
64
|
+
| Dimensão | Quantidade |
|
|
65
|
+
|---|---:|
|
|
66
|
+
| Squads | ${c.squads} |
|
|
67
|
+
| Agentes (total) | ${c.agentsTotal} |
|
|
68
|
+
| — de framework | ${c.frameworkAgents} |
|
|
69
|
+
| — de squad | ${c.squadAgents} |
|
|
70
|
+
| Orquestradores (-orqx) | ${c.orqx} |
|
|
71
|
+
| Workflows | ${c.workflowsTotal} |
|
|
72
|
+
| Artigos da constituição | ${c.articles} |
|
|
73
|
+
| Regras ativas | ${c.rules} |
|
|
74
|
+
|
|
75
|
+
## Como funciona — a cada pedido, prompt, execução e orquestração
|
|
76
|
+
|
|
77
|
+
O SINAPSE é descrito por ${flows.length} meta-fluxos. Cada um é um mecanismo real do framework (não há invenção — todos rastreiam para uma regra/hook). Os diagramas em PT-BR estão em \`LOOPS-sinapse-diagramas.md\`.
|
|
78
|
+
|
|
79
|
+
${flowList}
|
|
80
|
+
|
|
81
|
+
## Princípios constitucionais (gates inegociáveis)
|
|
82
|
+
|
|
83
|
+
A constituição tem ${c.articles} artigos. Gates automáticos bloqueiam violações.
|
|
84
|
+
|
|
85
|
+
| Art. | Princípio | Severidade |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
${articleRows}
|
|
88
|
+
|
|
89
|
+
## Catálogo de workflows
|
|
90
|
+
|
|
91
|
+
${c.workflowsTotal} workflows no total — ${c.frameworkWorkflows} de framework (orquestração, doc-first, greenfield/brownfield, QA) e ${c.squadWorkflows} de squad (expertise de domínio). O catálogo completo (com arquivo-fonte) vive no Atlas em \`docs/framework/atlas/OPERATING-ATLAS.md\`.
|
|
92
|
+
|
|
93
|
+
## Os meta-fluxos visuais
|
|
94
|
+
|
|
95
|
+
Todos os ${flows.length} diagramas Mermaid, em PT-BR, em **\`LOOPS-sinapse-diagramas.md\`** — no mesmo formato dos arquivos \`LOOPS-*-diagramas.md\` desta pesquisa.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
*Card gerado por \`sinapse atlas --research-card\` em ${date}. Fonte única → não editar à mão (re-rode o gerador).*
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the LOOPS-sinapse-diagramas markdown (PT-BR): every flow as a Mermaid block.
|
|
105
|
+
* @param {object} data - atlas data
|
|
106
|
+
* @param {Record<string,object>} pt - flow id → PT translation
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
function buildLoops(data, pt) {
|
|
110
|
+
const date = (data.generatedAt || '').slice(0, 10);
|
|
111
|
+
const flows = data.flows || [];
|
|
112
|
+
|
|
113
|
+
const blocks = flows
|
|
114
|
+
.map((f, i) => {
|
|
115
|
+
const t = pt[f.id] || { titulo: f.title, proposito: f.purpose, mermaid: f.mermaid };
|
|
116
|
+
const mermaid = t.mermaid || f.mermaid;
|
|
117
|
+
return `## MF${i + 1} — ${t.titulo}
|
|
118
|
+
|
|
119
|
+
> ${t.proposito}
|
|
120
|
+
|
|
121
|
+
\`\`\`mermaid
|
|
122
|
+
${mermaid}
|
|
123
|
+
\`\`\`
|
|
124
|
+
`;
|
|
125
|
+
})
|
|
126
|
+
.join('\n');
|
|
127
|
+
|
|
128
|
+
return `---
|
|
129
|
+
titulo: "LOOPS — Meta-fluxos do SINAPSE (estudo de caso)"
|
|
130
|
+
tipo: loops-diagramas
|
|
131
|
+
dominio: sinapse-framework
|
|
132
|
+
status: instancia-viva
|
|
133
|
+
created: ${date}
|
|
134
|
+
total_diagramas: ${flows.length}
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
# LOOPS — Meta-fluxos do SINAPSE
|
|
138
|
+
|
|
139
|
+
> Os ${flows.length} fluxos operacionais do framework SINAPSE em Mermaid — como ele funciona a cada pedido, prompt, execução e orquestração. Renderizam direto no GitHub.
|
|
140
|
+
> Gerado por \`sinapse atlas --research-card\`; fonte única no próprio framework.
|
|
141
|
+
|
|
142
|
+
${blocks}
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
*Gerado em ${date}. Re-rode \`sinapse atlas --research-card\` para atualizar.*
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render both PT-BR research artifacts from atlas data.
|
|
151
|
+
* @param {object} data - atlas data (buildAtlasData output)
|
|
152
|
+
* @param {object} [opts]
|
|
153
|
+
* @param {Record<string,object>} [opts.pt] - flow translations (defaults to FRAMEWORK_FLOWS_PT)
|
|
154
|
+
* @returns {{ card: string, loops: string }}
|
|
155
|
+
*/
|
|
156
|
+
function renderResearchCard(data, opts = {}) {
|
|
157
|
+
const pt = opts.pt || FRAMEWORK_FLOWS_PT;
|
|
158
|
+
return {
|
|
159
|
+
card: buildCard(data, pt),
|
|
160
|
+
loops: buildLoops(data, pt),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { renderResearchCard };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Command — executable sibling of `sinapse route`.
|
|
3
|
+
*
|
|
4
|
+
* `route()` SHOWS the deterministic doc-first decision for a briefing (type →
|
|
5
|
+
* workflow → required docs → gate). `build()` RUNS it: it drives the
|
|
6
|
+
* BobOrchestrator decision-tree engine (Projeto Bob) from a briefing in GUIDED
|
|
7
|
+
* mode — it detects project state, routes to the greenfield / brownfield /
|
|
8
|
+
* existing-project handler, and surfaces the next action/checkpoint instead of
|
|
9
|
+
* spawning agents headlessly.
|
|
10
|
+
*
|
|
11
|
+
* Congruence guarantee: both `route()` and `build()` start from
|
|
12
|
+
* `resolveDocFirstState`, so the conversation, the doc-first gate hook, `route`
|
|
13
|
+
* and `build` can never disagree about type/workflow/gate.
|
|
14
|
+
*
|
|
15
|
+
* The heavy orchestration index (locks, observability panel, status writer) is
|
|
16
|
+
* required LAZILY — `--dry-run` never constructs it, so it stays side-effect-free.
|
|
17
|
+
*
|
|
18
|
+
* @module core/orchestration/build-command
|
|
19
|
+
* @version 1.0.0
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const { resolveDocFirstState } = require('./doc-first-resolver');
|
|
25
|
+
|
|
26
|
+
let chalk;
|
|
27
|
+
try {
|
|
28
|
+
chalk = require('chalk');
|
|
29
|
+
} catch {
|
|
30
|
+
const id = (s) => s;
|
|
31
|
+
chalk = {
|
|
32
|
+
green: id, red: id, yellow: Object.assign(id, { bold: id }),
|
|
33
|
+
cyan: Object.assign(id, { bold: id }), gray: id, bold: id, dim: id,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render the compact doc-first routing header (the "plan"), shared with `route`
|
|
39
|
+
* so `build` is visibly congruent with it.
|
|
40
|
+
* @param {string} brief
|
|
41
|
+
* @param {object} state - resolveDocFirstState() result
|
|
42
|
+
*/
|
|
43
|
+
function printPlanHeader(brief, state) {
|
|
44
|
+
const ok = (b) => (b ? chalk.green('✓') : chalk.red('✗'));
|
|
45
|
+
const briefLabel = brief ? `"${brief}"` : '(no brief — using project state only)';
|
|
46
|
+
|
|
47
|
+
console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════'));
|
|
48
|
+
console.log(chalk.cyan.bold(' 🛠️ SINAPSE Build — guided orchestration'));
|
|
49
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════\n'));
|
|
50
|
+
|
|
51
|
+
console.log(`Brief: ${chalk.gray(briefLabel)}`);
|
|
52
|
+
console.log(`Project type: ${chalk.bold(state.projectType)}`);
|
|
53
|
+
console.log(`Workflow: ${chalk.bold(state.workflow || 'SDC (no upstream re-doc)')}`);
|
|
54
|
+
console.log(
|
|
55
|
+
`Doc-first: ${state.gate.satisfied
|
|
56
|
+
? chalk.green('✅ gate satisfied')
|
|
57
|
+
: chalk.red('🚫 gate blocked — missing: ' + state.gate.missing.join(', '))}\n`,
|
|
58
|
+
);
|
|
59
|
+
void ok;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render the BobOrchestrator result in a guided, scannable way.
|
|
64
|
+
* @param {object} result - OrchestrationResult from BobOrchestrator.orchestrate
|
|
65
|
+
*/
|
|
66
|
+
function printEngineResult(result) {
|
|
67
|
+
const r = result || {};
|
|
68
|
+
console.log(chalk.bold('Engine decision:'));
|
|
69
|
+
console.log(` Project state: ${chalk.bold(r.projectState || 'unknown')}`);
|
|
70
|
+
console.log(` Action: ${chalk.bold(r.action || 'n/a')}`);
|
|
71
|
+
|
|
72
|
+
const data = r.data || {};
|
|
73
|
+
const nextStep = r.nextStep || data.nextStep;
|
|
74
|
+
const message = data.formattedMessage || data.message || data.summary;
|
|
75
|
+
if (message) {
|
|
76
|
+
console.log('\n' + chalk.gray(String(message).trim()));
|
|
77
|
+
}
|
|
78
|
+
if (nextStep) {
|
|
79
|
+
console.log(`\n${chalk.cyan('▶ Next step:')} ${chalk.bold(nextStep)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('');
|
|
83
|
+
if (r.success) {
|
|
84
|
+
console.log(chalk.green.bold(' ✅ Orchestration step completed.\n'));
|
|
85
|
+
} else {
|
|
86
|
+
console.log(chalk.red.bold(` 🚫 Orchestration did not complete: ${r.error || r.action || 'unknown'}\n`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Execute the `build` command.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} brief - User briefing (e.g. "criar um site pra X")
|
|
94
|
+
* @param {Object} [options]
|
|
95
|
+
* @param {string} [options.projectRoot] - Project root path
|
|
96
|
+
* @param {string} [options.type] - Explicit project_type override
|
|
97
|
+
* @param {boolean} [options.dryRun] - Show the plan + engine entry without running it
|
|
98
|
+
* @returns {Promise<{success:boolean, exitCode:number, state:object, result?:object}>}
|
|
99
|
+
*/
|
|
100
|
+
async function build(brief, options = {}) {
|
|
101
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
102
|
+
const state = resolveDocFirstState({ projectRoot, brief, projectType: options.type });
|
|
103
|
+
|
|
104
|
+
printPlanHeader(brief, state);
|
|
105
|
+
|
|
106
|
+
if (options.dryRun) {
|
|
107
|
+
console.log(chalk.yellow.bold(' (dry-run) '), 'would run BobOrchestrator.orchestrate({ userGoal })');
|
|
108
|
+
console.log(
|
|
109
|
+
chalk.gray(' No engine constructed — no locks, no panel, no side effects.\n'),
|
|
110
|
+
);
|
|
111
|
+
return { success: true, exitCode: 0, state };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Lazily require the heavy orchestration index ONLY for the real run.
|
|
115
|
+
let BobOrchestrator;
|
|
116
|
+
try {
|
|
117
|
+
({ BobOrchestrator } = require('./index'));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.log(chalk.red(` ✗ Could not load orchestration engine: ${error.message}\n`));
|
|
120
|
+
return { success: false, exitCode: 1, state };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const bob = new BobOrchestrator(projectRoot, { userGoal: brief });
|
|
124
|
+
const result = await bob.orchestrate({ userGoal: brief, storyPath: options.storyPath });
|
|
125
|
+
|
|
126
|
+
printEngineResult(result);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
success: !!(result && result.success),
|
|
130
|
+
exitCode: result && result.success === false ? 1 : 0,
|
|
131
|
+
state,
|
|
132
|
+
result,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { build };
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc-First Resolver — deterministic "what does this project need before code?"
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the Documentation-First contract (Constitution
|
|
5
|
+
* Article III). Given a project root (and optionally a user brief), it answers,
|
|
6
|
+
* with NO LLM reasoning and NO side effects:
|
|
7
|
+
*
|
|
8
|
+
* - projectType : site | lp | app | platform | saas | api | service | feature | fix
|
|
9
|
+
* - workflow : which greenfield workflow applies (reuses greenfield-handler map)
|
|
10
|
+
* - artifacts : the upstream docs required before stories (brief/prd/spec/arch)
|
|
11
|
+
* - gate : is the minimal doc-first gate satisfied? (PRD + epic + a Ready story)
|
|
12
|
+
*
|
|
13
|
+
* Two consumers share this module so they never drift:
|
|
14
|
+
* 1. `sinapse route "<brief>"` — read-only observability of the routing decision.
|
|
15
|
+
* 2. The doc-first PreToolUse gate hook — blocks code writes when `gate.satisfied`
|
|
16
|
+
* is false. The module is intentionally dependency-light (fs/path + the
|
|
17
|
+
* greenfield-handler constants only) so it is safe and fast to require inside
|
|
18
|
+
* a hook. It NEVER constructs the heavy BobOrchestrator (locks, panels, etc.).
|
|
19
|
+
*
|
|
20
|
+
* @module core/orchestration/doc-first-resolver
|
|
21
|
+
* @version 1.0.0
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
GREENFIELD_WORKFLOW_BY_TYPE,
|
|
31
|
+
resolveGreenfieldWorkflow,
|
|
32
|
+
} = require('./greenfield-handler');
|
|
33
|
+
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
// PROJECT TYPE CLASSIFICATION
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Keyword → project_type map. Mirrors the classification matrix in
|
|
40
|
+
* `.claude/rules/documentation-first.md` and `project-intelligence.md`.
|
|
41
|
+
* Order matters: more specific buckets are tested before broader ones.
|
|
42
|
+
* @constant
|
|
43
|
+
*/
|
|
44
|
+
const TYPE_KEYWORDS = Object.freeze([
|
|
45
|
+
{ type: 'fix', words: ['bug', 'conserta', 'corrige', 'corrigir', 'ajusta', 'ajustar', 'tweak', 'hotfix'] },
|
|
46
|
+
{ type: 'refactor', words: ['refatora', 'refatorar', 'refactor', 'renomeia', 'renomear', 'limpa', 'cleanup'] },
|
|
47
|
+
{ type: 'lp', words: ['landing page', 'landing', 'lp', 'sales page', 'captura', 'squeeze'] },
|
|
48
|
+
{ type: 'saas', words: ['saas', 'software as a service', 'multi-tenant', 'multitenant'] },
|
|
49
|
+
{ type: 'platform', words: ['plataforma', 'platform', 'dashboard', 'admin', 'portal', 'painel'] },
|
|
50
|
+
{ type: 'app', words: ['app mobile', 'mobile', 'ios', 'android', 'react native', 'flutter', 'aplicativo'] },
|
|
51
|
+
{ type: 'api', words: ['api', 'backend', 'microservice', 'microsserviço', 'rest', 'graphql'] },
|
|
52
|
+
{ type: 'service', words: ['worker', 'cron', 'etl', 'integration', 'integração', 'automation', 'automação'] },
|
|
53
|
+
{ type: 'site', words: ['site', 'website', 'web site', 'institucional', 'página', 'pagina'] },
|
|
54
|
+
{ type: 'feature', words: ['feature', 'funcionalidade', 'implementa', 'implementar', 'adiciona'] },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** Escape a string for safe use inside a RegExp. */
|
|
58
|
+
function escapeRegExp(s) {
|
|
59
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Classify a brief string into a project_type. Returns `null` when the brief is
|
|
64
|
+
* empty or no keyword matches (caller decides the fallback). Matching is by whole
|
|
65
|
+
* word/phrase boundary (so "Asaas" does NOT match the keyword "saas").
|
|
66
|
+
* @param {string} [brief]
|
|
67
|
+
* @returns {string|null}
|
|
68
|
+
*/
|
|
69
|
+
function classifyProjectType(brief) {
|
|
70
|
+
if (!brief || typeof brief !== 'string') return null;
|
|
71
|
+
const haystack = brief.toLowerCase();
|
|
72
|
+
for (const { type, words } of TYPE_KEYWORDS) {
|
|
73
|
+
if (words.some((w) => new RegExp(`\\b${escapeRegExp(w)}\\b`, 'i').test(haystack))) {
|
|
74
|
+
return type;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
81
|
+
// REQUIRED ARTIFACTS PER PROJECT TYPE
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Upstream artifacts required BEFORE the first story, keyed by greenfield
|
|
86
|
+
* workflow. Mirrors the "Required upstream artifacts per project type" table in
|
|
87
|
+
* `documentation-first.md`. `feature` / `fix` / `refactor` do not require the
|
|
88
|
+
* upstream re-doc chain (they ride on an existing project).
|
|
89
|
+
* @constant
|
|
90
|
+
*/
|
|
91
|
+
const REQUIRED_ARTIFACTS_BY_WORKFLOW = Object.freeze({
|
|
92
|
+
'greenfield-ui': [
|
|
93
|
+
{ id: 'brief', path: 'docs/project-brief.md' },
|
|
94
|
+
{ id: 'prd', path: 'docs/prd.md' },
|
|
95
|
+
{ id: 'front-end-spec', path: 'docs/front-end-spec.md' },
|
|
96
|
+
{ id: 'architecture', path: 'docs/front-end-architecture.md' },
|
|
97
|
+
],
|
|
98
|
+
'greenfield-fullstack': [
|
|
99
|
+
{ id: 'brief', path: 'docs/project-brief.md' },
|
|
100
|
+
{ id: 'prd', path: 'docs/prd.md' },
|
|
101
|
+
{ id: 'front-end-spec', path: 'docs/front-end-spec.md' },
|
|
102
|
+
{ id: 'architecture', path: 'docs/fullstack-architecture.md' },
|
|
103
|
+
],
|
|
104
|
+
'greenfield-service': [
|
|
105
|
+
{ id: 'brief', path: 'docs/project-brief.md' },
|
|
106
|
+
{ id: 'prd', path: 'docs/prd.md' },
|
|
107
|
+
{ id: 'architecture', path: 'docs/service-architecture.md' },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Project types that bypass the upstream greenfield doc-chain. */
|
|
112
|
+
const LIGHT_TYPES = Object.freeze(['feature', 'fix', 'refactor']);
|
|
113
|
+
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
115
|
+
// PROJECT MATURITY
|
|
116
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
117
|
+
|
|
118
|
+
/** Directories whose presence (with real files) signals existing application code. */
|
|
119
|
+
const CODE_DIRS = Object.freeze(['src', 'app', 'packages', 'components', 'pages', 'lib']);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Assess project maturity for gate scoping. Two signals matter:
|
|
123
|
+
*
|
|
124
|
+
* - isFrameworkRepo : this IS the SINAPSE framework's own repo (package.json
|
|
125
|
+
* name === 'sinapse-ai'). Framework development operates
|
|
126
|
+
* above the story layer (Constitution Art. III exception),
|
|
127
|
+
* so the doc-first gate must never block it.
|
|
128
|
+
* - isGreenfield : the project has no substantial application code yet — a
|
|
129
|
+
* brand-new project. The hard doc-first gate applies HERE
|
|
130
|
+
* (this is the "criar um site → straight to code" case).
|
|
131
|
+
* Existing projects defer to the lighter story-gate.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} projectRoot
|
|
134
|
+
* @returns {{ isFrameworkRepo: boolean, isGreenfield: boolean }}
|
|
135
|
+
*/
|
|
136
|
+
function assessProjectMaturity(projectRoot) {
|
|
137
|
+
let isFrameworkRepo = false;
|
|
138
|
+
try {
|
|
139
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
140
|
+
if (pkg && pkg.name === 'sinapse-ai') isFrameworkRepo = true;
|
|
141
|
+
} catch {
|
|
142
|
+
// no package.json — fine
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Greenfield = no substantial application code yet. A lone package.json does
|
|
146
|
+
// NOT flip this off — otherwise scaffolding package.json first (an exempt
|
|
147
|
+
// file) would bypass the gate. Only populated code dirs count as "existing".
|
|
148
|
+
const hasCode = CODE_DIRS.some((d) => dirHasAnyFile(path.join(projectRoot, d)));
|
|
149
|
+
const isGreenfield = !hasCode;
|
|
150
|
+
|
|
151
|
+
return { isFrameworkRepo, isGreenfield };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
155
|
+
// MINIMAL DOC-FIRST GATE
|
|
156
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
|
|
158
|
+
const VALID_STORY_STATUSES = Object.freeze(['ready', 'inprogress', 'inreview', 'done']);
|
|
159
|
+
|
|
160
|
+
/** True if a `docs/prd.md` (or sharded `docs/prd/`) exists and is non-trivial. */
|
|
161
|
+
function hasPrd(projectRoot) {
|
|
162
|
+
const file = path.join(projectRoot, 'docs', 'prd.md');
|
|
163
|
+
if (fileHasContent(file)) return true;
|
|
164
|
+
return dirHasMarkdown(path.join(projectRoot, 'docs', 'prd'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** True if at least one epic exists under `docs/epics/` or `docs/stories/epics/`. */
|
|
168
|
+
function hasEpic(projectRoot) {
|
|
169
|
+
return (
|
|
170
|
+
dirHasMarkdown(path.join(projectRoot, 'docs', 'epics')) ||
|
|
171
|
+
dirHasMarkdown(path.join(projectRoot, 'docs', 'stories', 'epics'))
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** True if at least one story under `docs/stories/` is at a >= Ready status. */
|
|
176
|
+
function hasReadyStory(projectRoot) {
|
|
177
|
+
const dir = path.join(projectRoot, 'docs', 'stories');
|
|
178
|
+
return walkMarkdown(dir).some((f) => storyStatusIsReady(f));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse a story file's frontmatter `status:` and decide whether it is >= Ready.
|
|
183
|
+
* Uses a bounded, non-greedy regex (the greedy `[\w\s]*` form historically
|
|
184
|
+
* captured the next line — see the story-gate memory).
|
|
185
|
+
* @param {string} file
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
function storyStatusIsReady(file) {
|
|
189
|
+
let content;
|
|
190
|
+
try {
|
|
191
|
+
content = fs.readFileSync(file, 'utf8');
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const m = content.match(/status:\s*["']?([a-z][a-z ]*?)["']?\s*(?:\r?\n|$)/i);
|
|
196
|
+
if (!m) return false;
|
|
197
|
+
const status = m[1].toLowerCase().replace(/\s+/g, '');
|
|
198
|
+
return VALID_STORY_STATUSES.includes(status);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
202
|
+
// MAIN RESOLVER
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the full doc-first picture for a project.
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} [opts]
|
|
209
|
+
* @param {string} [opts.projectRoot=process.cwd()] - Project root.
|
|
210
|
+
* @param {string} [opts.brief] - User briefing (used to classify project type).
|
|
211
|
+
* @param {string} [opts.projectType] - Explicit type override (skips classification).
|
|
212
|
+
* @returns {{
|
|
213
|
+
* brief: string|null,
|
|
214
|
+
* projectType: string,
|
|
215
|
+
* classified: boolean,
|
|
216
|
+
* isLightType: boolean,
|
|
217
|
+
* workflow: string|null,
|
|
218
|
+
* artifacts: Array<{id:string, path:string, exists:boolean}>,
|
|
219
|
+
* gate: { prd:boolean, epic:boolean, readyStory:boolean, satisfied:boolean, missing:string[] }
|
|
220
|
+
* }}
|
|
221
|
+
*/
|
|
222
|
+
function resolveDocFirstState(opts = {}) {
|
|
223
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
224
|
+
const brief = opts.brief || null;
|
|
225
|
+
const classifiedType = opts.projectType || classifyProjectType(brief);
|
|
226
|
+
const projectType = classifiedType || 'site'; // safe default: assume a full project needs docs
|
|
227
|
+
const isLightType = LIGHT_TYPES.includes(projectType);
|
|
228
|
+
|
|
229
|
+
const workflow = isLightType ? null : resolveGreenfieldWorkflow(projectType);
|
|
230
|
+
|
|
231
|
+
const artifacts = (REQUIRED_ARTIFACTS_BY_WORKFLOW[workflow] || []).map((a) => ({
|
|
232
|
+
id: a.id,
|
|
233
|
+
path: a.path,
|
|
234
|
+
exists: fileHasContent(path.join(projectRoot, a.path)),
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
const prd = hasPrd(projectRoot);
|
|
238
|
+
const epic = hasEpic(projectRoot);
|
|
239
|
+
const readyStory = hasReadyStory(projectRoot);
|
|
240
|
+
|
|
241
|
+
// Light types ride on an existing project: the minimal gate is "a Ready story"
|
|
242
|
+
// (the existing story-gate). Full projects additionally need PRD + epic.
|
|
243
|
+
const missing = [];
|
|
244
|
+
if (!readyStory) missing.push('story (status >= Ready)');
|
|
245
|
+
if (!isLightType) {
|
|
246
|
+
if (!prd) missing.push('PRD (docs/prd.md)');
|
|
247
|
+
if (!epic) missing.push('epic (docs/epics/)');
|
|
248
|
+
}
|
|
249
|
+
const satisfied = missing.length === 0;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
brief,
|
|
253
|
+
projectType,
|
|
254
|
+
classified: Boolean(classifiedType),
|
|
255
|
+
isLightType,
|
|
256
|
+
workflow,
|
|
257
|
+
artifacts,
|
|
258
|
+
gate: { prd, epic, readyStory, satisfied, missing },
|
|
259
|
+
maturity: assessProjectMaturity(projectRoot),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// FS HELPERS (lean, no deps)
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
266
|
+
|
|
267
|
+
function fileHasContent(file) {
|
|
268
|
+
try {
|
|
269
|
+
const stat = fs.statSync(file);
|
|
270
|
+
return stat.isFile() && stat.size > 0;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function dirHasMarkdown(dir) {
|
|
277
|
+
try {
|
|
278
|
+
return fs.readdirSync(dir).some((f) => f.toLowerCase().endsWith('.md'));
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** True if `dir` exists and contains at least one regular file (any depth-1 entry). */
|
|
285
|
+
function dirHasAnyFile(dir) {
|
|
286
|
+
try {
|
|
287
|
+
return fs.readdirSync(dir, { withFileTypes: true }).some((e) => e.isFile() || e.isDirectory());
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Recursively collect `.md` files under a dir (bounded depth, fail-safe). */
|
|
294
|
+
function walkMarkdown(dir, depth = 0) {
|
|
295
|
+
if (depth > 4) return [];
|
|
296
|
+
let entries;
|
|
297
|
+
try {
|
|
298
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
299
|
+
} catch {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
const out = [];
|
|
303
|
+
for (const e of entries) {
|
|
304
|
+
const full = path.join(dir, e.name);
|
|
305
|
+
if (e.isDirectory()) {
|
|
306
|
+
out.push(...walkMarkdown(full, depth + 1));
|
|
307
|
+
} else if (e.name.toLowerCase().endsWith('.md')) {
|
|
308
|
+
out.push(full);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = {
|
|
315
|
+
classifyProjectType,
|
|
316
|
+
resolveDocFirstState,
|
|
317
|
+
assessProjectMaturity,
|
|
318
|
+
REQUIRED_ARTIFACTS_BY_WORKFLOW,
|
|
319
|
+
TYPE_KEYWORDS,
|
|
320
|
+
// re-exported for callers that want the raw map
|
|
321
|
+
GREENFIELD_WORKFLOW_BY_TYPE,
|
|
322
|
+
};
|