raptor-aios 0.6.3 → 0.7.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 CHANGED
@@ -3,6 +3,17 @@
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
+ ## [0.7.0] - 2026-06-08
7
+
8
+ ### Added
9
+
10
+ - **Governed gate waivers sourced from ADRs.** `raptor verify` (and `checklist`) now read `.raptor/memory/decisions.md`: an accepted ADR that declares an `Overrides: gate.id` line downgrades a failing **required/advisory** gate to a non-blocking `overridden` result, recorded in the report as `⊝ overridden by ADR-NNN: <justification>`. **Critical** gates can never be waived this way — they keep requiring human sign-off (C4/C5), and an ADR aimed at one is explicitly reported as ignored. The ADR shape is deliberately tolerant (any heading level, `Status:` defaults to accepted, multiple `Overrides:` lines per ADR), but gate ids are matched strictly (`gate.<seg>.<seg>`) so prose bullets and version tokens (`v1.2`, `ADR-1.2`) are never parsed as gates.
11
+ - **Honest M1 store opt-out.** A feature that genuinely touches no restricted OS API no longer needs invented permissions: keep the lists empty and declare `stores.no_restricted_apis: "<reason>"`. The M1 gate (`gate.mobile.stores`) then passes — mirroring `a11y.wcag_level: "n/a"` (M3) and `perf_budget.scope: "none"` (M5). An empty `stores:` block with no opt-out and no ADR still fails.
12
+
13
+ ### Internal
14
+
15
+ - New `gates/adr-overrides.ts` (`parseAdrOverrides` / `readAdrOverrides`); `runGates` gains an `overrides` option and an `overridden` status/summary count; `GateOverride` type added. `verify` warns when an ADR override targets an unknown gate id. The runner no longer mutates a gate's own result object when reporting an ignored critical override. Spec templates and the mobile-opinionated preset document both opt-out paths.
16
+
6
17
  ## [0.6.3] - 2026-06-06
7
18
 
8
19
  ### Fixed
package/README.md CHANGED
@@ -360,7 +360,7 @@ Cada gate exige campos no *frontmatter* de `spec.md` ou `plan.md` — declarativ
360
360
 
361
361
  | Gate | Nível | Exige no frontmatter | Verificável por |
362
362
  | --------------------------------------------- | ----- | ------------------------------------------------------------ | ------------------ |
363
- | **M1 — App/Play Store Compliance** | 🟡 | `spec.stores.ios_permissions`, `.android_permissions` | `verify stores` |
363
+ | **M1 — App/Play Store Compliance** | 🟡 | `spec.stores.ios_permissions`, `.android_permissions` (ou `stores.no_restricted_apis: "<motivo>"` p/ opt-out honesto) | `verify stores` |
364
364
  | **M2 — Privacidade & Residência (LGPD/GDPR)** | 🟡 | `plan.privacy.lawful_basis`, `.residency`, `.retention` | — |
365
365
  | **M3 — Acessibilidade (WCAG AA)** | 🟡 | `spec.a11y.wcag_level`, `.criteria` | `verify a11y` |
366
366
  | **M4 — Matriz de SO** | 🟡 | `plan.os_matrix.ios_min`, `.android_min` | `verify os-matrix` |
@@ -419,6 +419,17 @@ observability:
419
419
 
420
420
  </details>
421
421
 
422
+ > 🧯 **Feature sem APIs restritas?** Não invente permissões. Há dois caminhos honestos e auditáveis para o M1:
423
+ > 1. **Opt-out no spec** — deixe as listas vazias e declare `stores.no_restricted_apis: "<motivo>"` (espelha `a11y.wcag_level: "n/a"` e `perf_budget.scope: "none"`).
424
+ > 2. **Override por ADR** — registre um ADR aceito em `.raptor/memory/decisions.md` com `Overrides: gate.mobile.stores`; o `verify` o lê em runtime e marca o gate como `⊝ overridden by ADR-NNN`. Gates **críticos** nunca são waivados por ADR (exigem assinatura humana).
425
+ >
426
+ > ```markdown
427
+ > ## ADR-001: Feature sem APIs restritas de OS
428
+ > - Status: accepted
429
+ > - Overrides: gate.mobile.stores
430
+ > - Justification: Regras de negócio puras; não toca camera/location/etc.
431
+ > ```
432
+
422
433
  ---
423
434
 
424
435
  ## 🔬 A ponta de verificação (`verify`)
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const INACTIVE_STATUS = new Set([
4
+ "proposed",
5
+ "draft",
6
+ "rejected",
7
+ "superseded",
8
+ "deprecated",
9
+ "withdrawn",
10
+ ]);
11
+ function extractAdrLabel(heading) {
12
+ const m = heading.match(/\bADR[-\s]?([\w.]+)/i);
13
+ if (!m)
14
+ return null;
15
+ return `ADR-${m[1]}`;
16
+ }
17
+ const HEADING_RE = /^#{1,}\s+(.*)$/;
18
+ const OVERRIDES_RE = /^[-*\s]*overrides\s*:\s*(.+)$/i;
19
+ const STATUS_RE = /^[-*\s]*status\s*:\s*(.+)$/i;
20
+ const JUSTIFICATION_RE = /^[-*\s]*justification\s*:\s*(.+)$/i;
21
+ const GATE_ID_RE = /^gate\.[a-z0-9_]+(?:\.[a-z0-9_]+)+$/i;
22
+ function fieldValue(line, re) {
23
+ const m = line.match(re);
24
+ return m ? m[1].trim() : null;
25
+ }
26
+ function parseGateIds(values) {
27
+ const ids = [];
28
+ for (const value of values) {
29
+ for (const t of value.split(/[,\s]+/)) {
30
+ const id = t.trim();
31
+ if (GATE_ID_RE.test(id) && !ids.includes(id))
32
+ ids.push(id);
33
+ }
34
+ }
35
+ return ids;
36
+ }
37
+ export function parseAdrOverrides(markdown) {
38
+ const lines = markdown.split(/\r?\n/);
39
+ const out = [];
40
+ let label = null;
41
+ let status = null;
42
+ let overridesLines = [];
43
+ let justification = null;
44
+ const flush = () => {
45
+ if (!label || overridesLines.length === 0)
46
+ return;
47
+ if (status && INACTIVE_STATUS.has(status.toLowerCase()))
48
+ return;
49
+ const gates = parseGateIds(overridesLines);
50
+ const reason = justification?.trim() || label;
51
+ for (const gate of gates) {
52
+ out.push({ gate, adr: label, justification: reason });
53
+ }
54
+ };
55
+ for (const line of lines) {
56
+ const heading = line.match(HEADING_RE);
57
+ if (heading) {
58
+ flush();
59
+ label = extractAdrLabel(heading[1]);
60
+ status = null;
61
+ overridesLines = [];
62
+ justification = null;
63
+ continue;
64
+ }
65
+ if (!label)
66
+ continue;
67
+ const ov = fieldValue(line, OVERRIDES_RE);
68
+ if (ov !== null)
69
+ overridesLines.push(ov);
70
+ const st = fieldValue(line, STATUS_RE);
71
+ if (st !== null)
72
+ status = st;
73
+ const ju = fieldValue(line, JUSTIFICATION_RE);
74
+ if (ju !== null)
75
+ justification = ju;
76
+ }
77
+ flush();
78
+ return out;
79
+ }
80
+ export function readAdrOverrides(projectRoot) {
81
+ const path = join(projectRoot, ".raptor", "memory", "decisions.md");
82
+ if (!existsSync(path))
83
+ return {};
84
+ const raw = readFileSync(path, "utf8");
85
+ const map = {};
86
+ for (const o of parseAdrOverrides(raw)) {
87
+ map[o.gate] = { adr: o.adr, justification: o.justification };
88
+ }
89
+ return map;
90
+ }
@@ -6,3 +6,4 @@ export * from "./phase-gates.js";
6
6
  export * from "./m7-gates.js";
7
7
  export * from "./design-gates.js";
8
8
  export { runGates, formatReport } from "./runner.js";
9
+ export { parseAdrOverrides, readAdrOverrides, } from "./adr-overrides.js";
@@ -1,5 +1,6 @@
1
1
  export async function runGates(gates, ctx, opts = {}) {
2
2
  const skip = opts.skip ?? new Set();
3
+ const overrides = opts.overrides ?? {};
3
4
  const results = [];
4
5
  for (const gate of gates) {
5
6
  if (skip.has(gate.id)) {
@@ -26,6 +27,23 @@ export async function runGates(gates, ctx, opts = {}) {
26
27
  }
27
28
  try {
28
29
  const res = await gate.run(ctx);
30
+ const ovr = overrides[gate.id];
31
+ if (res.status === "fail" && ovr) {
32
+ if (gate.level === "critical") {
33
+ results.push({
34
+ ...res,
35
+ message: `${res.message ?? "failed"} — ADR override ${ovr.adr} ignored: critical gates require human sign-off (C4/C5)`,
36
+ });
37
+ }
38
+ else {
39
+ results.push({
40
+ ...res,
41
+ status: "overridden",
42
+ message: `overridden by ${ovr.adr}: ${ovr.justification}`,
43
+ });
44
+ }
45
+ continue;
46
+ }
29
47
  results.push(res);
30
48
  }
31
49
  catch (err) {
@@ -43,6 +61,7 @@ export async function runGates(gates, ctx, opts = {}) {
43
61
  pass: results.filter((r) => r.status === "pass").length,
44
62
  fail: results.filter((r) => r.status === "fail").length,
45
63
  skipped: results.filter((r) => r.status === "skipped").length,
64
+ overridden: results.filter((r) => r.status === "overridden").length,
46
65
  };
47
66
  const blocked = results.some((r) => r.status === "fail" && (r.level === "critical" || r.level === "required"));
48
67
  return { results, blocked, summary };
@@ -50,7 +69,13 @@ export async function runGates(gates, ctx, opts = {}) {
50
69
  export function formatReport(report) {
51
70
  const lines = [];
52
71
  for (const r of report.results) {
53
- const mark = r.status === "pass" ? "✓" : r.status === "skipped" ? "⊘" : "✗";
72
+ const mark = r.status === "pass"
73
+ ? "✓"
74
+ : r.status === "skipped"
75
+ ? "⊘"
76
+ : r.status === "overridden"
77
+ ? "⊝"
78
+ : "✗";
54
79
  const art = r.article ? ` [${r.article}]` : "";
55
80
  const lvl = r.level.toUpperCase();
56
81
  lines.push(`${mark} ${lvl.padEnd(9)}${art.padEnd(5)} ${r.id} — ${r.title}`);
@@ -58,6 +83,9 @@ export function formatReport(report) {
58
83
  lines.push(` ${r.message}`);
59
84
  }
60
85
  lines.push("");
61
- lines.push(`Summary: ${report.summary.pass} pass, ${report.summary.fail} fail, ${report.summary.skipped} skipped${report.blocked ? " — BLOCKED" : ""}`);
86
+ const overriddenPart = report.summary.overridden > 0
87
+ ? `, ${report.summary.overridden} overridden`
88
+ : "";
89
+ lines.push(`Summary: ${report.summary.pass} pass, ${report.summary.fail} fail, ${report.summary.skipped} skipped${overriddenPart}${report.blocked ? " — BLOCKED" : ""}`);
62
90
  return lines.join("\n");
63
91
  }
@@ -55,10 +55,16 @@ export const gateMobileStores = {
55
55
  if (!spec.exists)
56
56
  return fail(this, "spec.md missing");
57
57
  const s = spec.data.stores;
58
- const iosCount = s?.ios_permissions?.length ?? 0;
59
- const androidCount = s?.android_permissions?.length ?? 0;
60
- if (!s || (iosCount === 0 && androidCount === 0)) {
61
- return fail(this, "spec frontmatter missing stores.ios_permissions / stores.android_permissions");
58
+ if (!s)
59
+ return fail(this, "spec frontmatter missing the stores block");
60
+ const iosCount = s.ios_permissions?.length ?? 0;
61
+ const androidCount = s.android_permissions?.length ?? 0;
62
+ if (iosCount === 0 && androidCount === 0) {
63
+ const optOutRaw = s.no_restricted_apis;
64
+ const optOut = typeof optOutRaw === "string" ? optOutRaw.trim() : "";
65
+ if (optOut)
66
+ return pass(this, { no_restricted_apis: optOut });
67
+ return fail(this, 'spec declares no store permissions — list them under stores.ios_permissions / stores.android_permissions, or declare stores.no_restricted_apis: "<why this feature touches no restricted OS API>" to opt out (M1)');
62
68
  }
63
69
  return pass(this, { ios: iosCount, android: androidCount });
64
70
  },
@@ -5,10 +5,10 @@ const articles = [
5
5
  title: "App Store & Play Store Compliance",
6
6
  statement: "Every feature that touches restricted OS APIs (camera, microphone, contacts, location, photos, notifications, biometrics, health, HomeKit, etc.) MUST declare the exact store-level permission strings and the corresponding `NSxxxUsageDescription` / Android manifest permission in its `spec.md` frontmatter under `stores:`.",
7
7
  rationale: "App Store and Play Store rejections are the single largest source of mobile release delays and often surface only during submission review — after the code has been merged and the release train is in motion. Declaring permissions at spec time forces the conversation before engineering cost is sunk.",
8
- enforcement: "`gate.mobile.stores` (level `required`) validates that `spec.stores.ios_permissions` and `spec.stores.android_permissions` are declared and non-empty whenever a feature targets mobile.",
8
+ enforcement: '`gate.mobile.stores` (level `required`) validates that `spec.stores.ios_permissions` / `spec.stores.android_permissions` are declared whenever a feature touches restricted OS APIs. A feature that genuinely touches none opts out honestly by declaring `stores.no_restricted_apis: "<reason>"` — mirroring `a11y.wcag_level: "n/a"` (M3) and `perf_budget.scope: "none"` (M5).',
9
9
  violation: {
10
10
  level: "required",
11
- description: "Spec cannot be approved until the `stores:` frontmatter is populated. An ADR in `memory/decisions.md` MAY override with explicit justification (e.g., pure business logic with no platform API).",
11
+ description: 'Spec cannot be approved with an empty `stores:` block unless it declares `stores.no_restricted_apis` with a reason. Alternatively, an accepted ADR in `memory/decisions.md` that lists `Overrides: gate.mobile.stores` waives the gate at verify time (the override is recorded as `overridden by ADR-NNN`).',
12
12
  },
13
13
  },
14
14
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raptor/core",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js"
6
6
  }
@@ -12,6 +12,8 @@ acceptance:
12
12
  stores:
13
13
  ios_permissions: []
14
14
  android_permissions: []
15
+ # If the feature touches NO restricted OS API, keep the lists empty and add:
16
+ # no_restricted_apis: "<why — e.g. pure business logic, no camera/location>"
15
17
  a11y:
16
18
  wcag_level: "AA"
17
19
  criteria: []
@@ -20,7 +22,7 @@ a11y:
20
22
  <!--
21
23
  REQUIRED FRONTMATTER (gates read the keys above — fill before `raptor approve`):
22
24
  acceptance.ids: [AC-1, AC-2] # every AC heading must be listed (analyze + M7)
23
- stores.ios_permissions / android_permissions # M1, mobile: "PERMISSION — why"
25
+ stores.ios_permissions / android_permissions # M1, mobile: "PERMISSION — why" (or stores.no_restricted_apis: "<reason>")
24
26
  a11y.wcag_level + a11y.criteria # M3 (use "n/a" + justification when non-visual)
25
27
  (Frontmatter comments are stripped on approve, so this guidance lives in the body.)
26
28
  -->
@@ -2,7 +2,7 @@ import { Args, Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
- import { appendAuditEvent, buildCanonicalPrompt, discoverChecklists, formatReport, gateChecklistFrontmatter, gateChecklistLinksToDecisions, gateChecklistNonEmpty, generateChecklistFrontmatter, hashString, loadAgentsConfig, parseChecklistItems, parsePlan, runGates, selectAgent, summarizeChecklist, } from "../_core/dist/index.js";
5
+ import { appendAuditEvent, buildCanonicalPrompt, discoverChecklists, formatReport, gateChecklistFrontmatter, gateChecklistLinksToDecisions, gateChecklistNonEmpty, generateChecklistFrontmatter, hashString, loadAgentsConfig, parseChecklistItems, parsePlan, readAdrOverrides, runGates, selectAgent, summarizeChecklist, } from "../_core/dist/index.js";
6
6
  import { parseFrontmatter } from "../_core/dist/index.js";
7
7
  import { currentActor, featureDir, requireProjectRoot, } from "../shared/project.js";
8
8
  import { writeFeaturePrompt } from "../shared/feature-prompt.js";
@@ -171,7 +171,7 @@ export default class Checklist extends BaseCommand {
171
171
  const report = await runGates(gates, {
172
172
  projectRoot: root,
173
173
  featureDir: dir,
174
- });
174
+ }, { overrides: readAdrOverrides(root) });
175
175
  this.log(`=== Checklist Gates for ${featureName} ===`);
176
176
  this.log(formatReport(report));
177
177
  if (report.blocked)
@@ -2,7 +2,7 @@ import { Args, Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
3
  import { existsSync, readdirSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
- import { BUILTIN_GATES, formatReport, gateById, getPreset, PROJECT_GATES, runGates, } from "../_core/dist/index.js";
5
+ import { BUILTIN_GATES, formatReport, gateById, getPreset, PROJECT_GATES, readAdrOverrides, runGates, } from "../_core/dist/index.js";
6
6
  const PROJECT_GATE_IDS = new Set(PROJECT_GATES.map((g) => g.id));
7
7
  import { featureDir, readConfig, requireProjectRoot, } from "../shared/project.js";
8
8
  export default class Verify extends BaseCommand {
@@ -43,8 +43,14 @@ export default class Verify extends BaseCommand {
43
43
  if (flags.justification)
44
44
  for (const id of skipSet)
45
45
  skipJustifications[id] = flags.justification;
46
+ const overrides = readAdrOverrides(root);
47
+ const knownGateIds = new Set([...BUILTIN_GATES, ...PROJECT_GATES, ...presetGates].map((g) => g.id));
48
+ for (const id of Object.keys(overrides)) {
49
+ if (!knownGateIds.has(id))
50
+ this.warn(`ADR override targets unknown gate id "${id}" — ignored (check the spelling in memory/decisions.md).`);
51
+ }
46
52
  if (!args.feature) {
47
- const projectReport = await runGates(PROJECT_GATES, { projectRoot: root }, { skip: skipSet, skipJustifications });
53
+ const projectReport = await runGates(PROJECT_GATES, { projectRoot: root }, { skip: skipSet, skipJustifications, overrides });
48
54
  this.log("=== Project gates ===");
49
55
  this.log(formatReport(projectReport));
50
56
  const specs = join(root, ".raptor", "specs");
@@ -63,7 +69,7 @@ export default class Verify extends BaseCommand {
63
69
  ...BUILTIN_GATES.filter((g) => !PROJECT_GATE_IDS.has(g.id)),
64
70
  ...presetGates,
65
71
  ];
66
- const r = await runGates(featureGates, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications });
72
+ const r = await runGates(featureGates, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications, overrides });
67
73
  this.log(`\n--- ${f} ---`);
68
74
  this.log(formatReport(r));
69
75
  if (r.blocked)
@@ -88,7 +94,7 @@ export default class Verify extends BaseCommand {
88
94
  this.warn(`--skip=${id} ignored: critical gates cannot be skipped (see C4/C5).`);
89
95
  }
90
96
  }
91
- const report = await runGates(gatesToRun, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications });
97
+ const report = await runGates(gatesToRun, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications, overrides });
92
98
  this.log(`=== Gates for feature ${featureName}${preset ? ` (preset: ${preset.id})` : ""} ===`);
93
99
  this.log(formatReport(report));
94
100
  if (report.blocked)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "raptor-aios",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Raptor — Spec-Driven Development (SDD) CLI for modern mobile apps. Constitutional gates, audit trail, real verification (a11y/perf/stores/OS matrix), and AI-agent slash commands.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@ const CLI = join(ROOT, "packages", "cli");
29
29
  const CORE = join(ROOT, "packages", "core");
30
30
  const OUT = join(ROOT, "build", "npm");
31
31
 
32
- const VERSION = "0.6.3";
32
+ const VERSION = "0.7.0";
33
33
 
34
34
  function log(msg) {
35
35
  process.stdout.write(` ${msg}\n`);
@@ -10,9 +10,12 @@ audit_ref: ~
10
10
  # --- Gates read the keys below. Fill them before `raptor approve`. ---
11
11
  acceptance:
12
12
  ids: [] # e.g. [AC-1, AC-2] — every AC heading below must also appear here (analyze + M7)
13
- stores: # M1 — store-level permission strings (mobile). Use [] when not applicable.
13
+ stores: # M1 — store-level permission strings (mobile).
14
14
  ios_permissions: [] # e.g. ["NSCameraUsageDescription — scan delivery QR codes"]
15
15
  android_permissions: [] # e.g. ["android.permission.CAMERA — scan delivery QR codes"]
16
+ # If this feature touches NO restricted OS API, leave the lists empty AND set
17
+ # no_restricted_apis to the reason (honest opt-out — mirrors a11y "n/a"):
18
+ # no_restricted_apis: "Pure business logic — no camera/location/etc."
16
19
  a11y: # M3 — WCAG target (set wcag_level: "n/a" + a justification when truly non-visual)
17
20
  wcag_level: "AA"
18
21
  criteria: [] # e.g. ["All controls have accessible labels", "Contrast >= 4.5:1"]