@ancleto/spec 0.6.26 → 0.6.27

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/README.md CHANGED
@@ -201,6 +201,9 @@ ancleto upgrade # Re-aplica templates y skills respetando tus personal
201
201
 
202
202
  ancleto check # Verifica la integridad de la instalación (faltantes / huérfanos)
203
203
 
204
+ ancleto specs check [--change <nombre>] [--json]
205
+ # Valida keywords canónicos en aspec/specs y en los deltas de un change
206
+
204
207
  ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
205
208
 
206
209
  ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
@@ -105,12 +105,13 @@ Do not treat archive as a blind file move.
105
105
  - `tasks.md`
106
106
  - `specs/` when the change includes delta specs
107
107
  3. Determine whether the change includes spec deltas that must be reflected in `aspec/specs/`
108
- 4. If needed, update the relevant source-of-truth specs so they reflect the completed change
109
- 5. If the change is not ready for archive, stop and report the inconsistency clearly
110
- 6. If the change is ready, archive it to `aspec/changes/archive/{YYYY-MM-DD}-{change-name}/`
111
- 7. Remove the original active change directory from `aspec/changes/{change-name}/` after a successful archive
112
- 8. If any files or folders remain in the original active change directory, report them as cleanup issues instead of leaving silent residue
113
- 9. Return a short structured summary of the finalization and archive result
108
+ 4. When deltas exist, run `ancleto specs check --change {change-name} --json` before merging. If a delta spec or its target source-of-truth spec is non-canonical (translated keywords), stop and report it instead of merging into it
109
+ 5. If needed, update the relevant source-of-truth specs so they reflect the completed change
110
+ 6. If the change is not ready for archive, stop and report the inconsistency clearly
111
+ 7. If the change is ready, archive it to `aspec/changes/archive/{YYYY-MM-DD}-{change-name}/`
112
+ 8. Remove the original active change directory from `aspec/changes/{change-name}/` after a successful archive
113
+ 9. If any files or folders remain in the original active change directory, report them as cleanup issues instead of leaving silent residue
114
+ 10. Return a short structured summary of the finalization and archive result
114
115
 
115
116
  ## Source-of-Truth Rules
116
117
 
@@ -44,8 +44,9 @@ using the `ancleto-workflow` skill and the conventions of:
44
44
  - `design.md`
45
45
  - `tasks.md`
46
46
  - `specs/{capability}/spec.md` as needed
47
- 6. When the delegation says the context was produced without a technical seed, declare that in the generated artifacts.
48
- 7. Return a brief summary with generated paths, scope, and open questions or risks
47
+ 6. Before returning, verify every generated spec uses the canonical keywords (`### Requirement:`, `#### Scenario:`, `**WHEN**`/`**THEN**`, `SHALL`); fix any translated heading or keyword in place — never leave a spec with translated keywords.
48
+ 7. When the delegation says the context was produced without a technical seed, declare that in the generated artifacts.
49
+ 8. Return a brief summary with generated paths, scope, and open questions or risks
49
50
 
50
51
  ## Change Naming
51
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,6 +31,8 @@ Read `aspec/changes/<name>/tasks.md` (when present); count `- [ ]` (incomplete)
31
31
 
32
32
  Check for delta specs at `aspec/changes/<name>/specs/`; if none, proceed without a sync prompt.
33
33
 
34
+ **Canonical keywords first:** when delta specs exist and the CLI is available, run `ancleto specs check --change <name> --json`. If a delta spec or its target source-of-truth spec is non-canonical (translated keywords), warn that requirement blocks cannot be matched reliably and ask whether to normalize the affected spec first or archive without syncing that capability. Never merge into a non-canonical spec silently.
35
+
34
36
  **If delta specs exist:**
35
37
 
36
38
  - Diff each delta against its main spec at `aspec/specs/<capability>/spec.md`; show a combined summary of adds/modifications/removals/renames.
@@ -26,6 +26,12 @@ Read under `aspec/changes/<name>/`: `tasks.md` (required), plus `proposal.md`, `
26
26
 
27
27
  **Tasks:** parse checkboxes (`- [ ]` incomplete vs `- [x]` complete) and count. Each incomplete → CRITICAL: "Complete task: `<description>`" or "Mark as done if already implemented".
28
28
 
29
+ **Canonical spec keywords:** run `ancleto specs check --change <name> --json` when the CLI is available. For every reported file:
30
+ - Delta spec (`aspec/changes/<name>/specs/...`) → CRITICAL: "Non-canonical spec: `<file>` (missing: `<keywords>`)" + "Normalize `<file>` to the canonical English keywords before archiving".
31
+ - Source-of-truth spec (`aspec/specs/...`) → WARNING with the same recommendation.
32
+
33
+ A non-canonical spec cannot be matched for coverage or merged reliably, so report it before the checks below.
34
+
29
35
  **Spec coverage:** for each `### Requirement:` in `specs/`, search the codebase for related keywords. Unimplemented → CRITICAL: "Requirement not found: `<requirement name>`" + "Implement requirement: `<description>`".
30
36
 
31
37
  ### 3. Correctness
package/src/cli/index.js CHANGED
@@ -4,7 +4,7 @@ import { cp, mkdir, access, writeFile, readFile, readdir, rename } from 'node:fs
4
4
  import { spawnSync } from 'node:child_process'
5
5
  import { createHash } from 'node:crypto'
6
6
  import { createInterface } from 'node:readline'
7
- import { join, dirname, resolve, basename } from 'node:path'
7
+ import { join, dirname, resolve, basename, relative } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { homedir, tmpdir } from 'node:os'
10
10
  import { createMemoryEngine, createReadonlyMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
@@ -40,6 +40,9 @@ Uso:
40
40
  ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
41
41
  ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
42
42
  Empaca el repo con Repomix y guarda estado
43
+ ancleto specs check [--change <name>] [--json]
44
+ Valida keywords canonicos (### Requirement:, WHEN, THEN)
45
+ en aspec/specs y, con --change, en los deltas del change
43
46
  ancleto memory context [--scope X] [--out file]
44
47
  Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
45
48
  ancleto memory list [--type X] [--scope X] [--all] [--json]
@@ -980,6 +983,77 @@ async function checkTierOrphan(cwd) {
980
983
  return warnings
981
984
  }
982
985
 
986
+ // Un spec es canonico cuando usa los keywords literales en ingles. Detecta specs
987
+ // traducidos (p. ej. "#### RF1:" + "**Dado**/**Cuando**/**Entonces**"), que rompen
988
+ // la cobertura por keywords y el merge de deltas.
989
+ function specIssues(content) {
990
+ const missing = []
991
+ if (!/^###\s+Requirement:/m.test(content)) missing.push('### Requirement:')
992
+ if (/^####\s+Scenario:/m.test(content)) {
993
+ if (!/\bWHEN\b/.test(content)) missing.push('WHEN')
994
+ if (!/\bTHEN\b/.test(content)) missing.push('THEN')
995
+ }
996
+ const unexpected = []
997
+ const re = /^####\s+(.+)$/gm
998
+ let m
999
+ while ((m = re.exec(content)) !== null) {
1000
+ if (!/^Scenario\b/.test(m[1])) unexpected.push(`#### ${m[1]}`)
1001
+ }
1002
+ return { missing, unexpected }
1003
+ }
1004
+
1005
+ async function collectSpecFiles(dir) {
1006
+ const out = []
1007
+ if (!(await exists(dir))) return out
1008
+ for (const e of await readdir(dir, { withFileTypes: true })) {
1009
+ const p = join(dir, e.name)
1010
+ if (e.isDirectory()) out.push(...(await collectSpecFiles(p)))
1011
+ else if (e.name === 'spec.md') out.push(p)
1012
+ }
1013
+ return out
1014
+ }
1015
+
1016
+ async function specsCheckCommand(args) {
1017
+ const cwd = process.cwd()
1018
+ const asJson = args.includes('--json')
1019
+ const ci = args.indexOf('--change')
1020
+ const change = ci >= 0 ? args[ci + 1] : null
1021
+ const files = await collectSpecFiles(join(cwd, CHANGES_ROOT, 'specs'))
1022
+ if (change) {
1023
+ const changeDir = join(cwd, CHANGES_ROOT, 'changes', change)
1024
+ if (!(await exists(changeDir))) {
1025
+ console.error(`ancleto: no existe el change "${change}" en ${CHANGES_ROOT}/changes/`)
1026
+ process.exit(1)
1027
+ }
1028
+ files.push(...(await collectSpecFiles(join(changeDir, 'specs'))))
1029
+ }
1030
+ const findings = []
1031
+ for (const f of files) {
1032
+ const { missing, unexpected } = specIssues(await readFile(f, 'utf8'))
1033
+ if (missing.length || unexpected.length) {
1034
+ findings.push({ file: relative(cwd, f).replace(/\\/g, '/'), missing, unexpected })
1035
+ }
1036
+ }
1037
+ const ok = findings.length === 0
1038
+ if (asJson) {
1039
+ console.log(JSON.stringify({ ok, scanned: files.length, nonCanonical: findings }, null, 2))
1040
+ } else if (files.length === 0) {
1041
+ console.log('ancleto: no hay specs para revisar')
1042
+ } else {
1043
+ for (const f of files) {
1044
+ const rel = relative(cwd, f).replace(/\\/g, '/')
1045
+ const found = findings.find((x) => x.file === rel)
1046
+ if (found) {
1047
+ console.log(` ✖ ${rel} (revisar: ${[...found.missing, ...found.unexpected].join(', ')})`)
1048
+ } else {
1049
+ console.log(` ✔ ${rel}`)
1050
+ }
1051
+ }
1052
+ console.log(ok ? `ancleto: ${files.length} spec(s) canonicos` : `ancleto: ${findings.length} de ${files.length} spec(s) sin keywords canonicos`)
1053
+ }
1054
+ process.exit(ok ? 0 : 1)
1055
+ }
1056
+
983
1057
  async function doctorCommand() {
984
1058
  let fatal = false
985
1059
 
@@ -1045,6 +1119,9 @@ switch (cmd) {
1045
1119
  case 'check':
1046
1120
  await checkCommand()
1047
1121
  break
1122
+ case 'specs':
1123
+ await specsCheckCommand(rest)
1124
+ break
1048
1125
  case 'doctor':
1049
1126
  await doctorCommand()
1050
1127
  break