raptor-aios 0.6.2 → 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 +21 -0
- package/README.md +12 -1
- package/dist/_core/dist/gates/adr-overrides.js +90 -0
- package/dist/_core/dist/gates/index.js +1 -0
- package/dist/_core/dist/gates/runner.js +30 -2
- package/dist/_core/dist/presets/gates.js +10 -4
- package/dist/_core/dist/presets/mobile-opinionated.js +2 -2
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/spec.md.hbs +3 -1
- package/dist/commands/checklist.js +2 -2
- package/dist/commands/new.js +29 -13
- package/dist/commands/verify.js +10 -4
- package/package.json +1 -1
- package/scripts/prepare-npm.mjs +1 -1
- package/templates/spec-template.md +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
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
|
+
|
|
17
|
+
## [0.6.3] - 2026-06-06
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`raptor new --jira=<KEY>` no longer creates an empty, mislabelled spec when the Jira card can't be read.** Previously, when a ticket was given but Jira was not connected (or the token had expired, or the issue was unreachable), `new` warned and then degraded to seeding a spec stamped with the Jira ID but **no card content** — and created the branch too. Now, when a fetch was expected (the default), an unreadable card is a **hard stop**: `new` aborts **before any side effect** (no spec, no directory, no branch) with an actionable message that states the reason and points to `raptor jira connect` (or configuring the Jira MCP server). The intentional offline path is unchanged — pass `--no-jira-fetch` to record the ticket ID only.
|
|
22
|
+
|
|
23
|
+
### Internal
|
|
24
|
+
|
|
25
|
+
- `New.fetchJiraContext` now returns a discriminated result (`{ ok, context }` | `{ ok: false, reason }`) so the caller can abort with the specific failure reason instead of silently degrading. Removed the now-unreachable `"id only (fetch unavailable)"` status tag.
|
|
26
|
+
|
|
6
27
|
## [0.6.2] - 2026-06-06
|
|
7
28
|
|
|
8
29
|
### Changed
|
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`
|
|
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
|
+
}
|
|
@@ -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"
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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:
|
|
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:
|
|
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
|
{
|
package/dist/_core/package.json
CHANGED
|
@@ -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)
|
package/dist/commands/new.js
CHANGED
|
@@ -113,7 +113,26 @@ export default class New extends BaseCommand {
|
|
|
113
113
|
const specNeedsSeeding = !existsSync(specPath);
|
|
114
114
|
let jiraContext = null;
|
|
115
115
|
if (specNeedsSeeding && flags.jira && flags["jira-fetch"]) {
|
|
116
|
-
|
|
116
|
+
const result = await this.fetchJiraContext(root, flags.jira);
|
|
117
|
+
if (result.ok) {
|
|
118
|
+
jiraContext = result.context;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.error([
|
|
122
|
+
`${result.reason}`,
|
|
123
|
+
``,
|
|
124
|
+
`Aborting: --jira=${flags.jira} was given but its Jira card could not be read,`,
|
|
125
|
+
`so there is no card content to seed the spec from. No spec or branch was`,
|
|
126
|
+
`created — a spec stamped with a Jira ID but no Jira content would be misleading.`,
|
|
127
|
+
``,
|
|
128
|
+
`To fix, do one of:`,
|
|
129
|
+
` • Connect Jira, then retry:`,
|
|
130
|
+
` raptor jira connect`,
|
|
131
|
+
` (or configure the Jira MCP server under 'jira:' in .raptor/raptor.yml)`,
|
|
132
|
+
` • Record the ticket ID only, without fetching the card:`,
|
|
133
|
+
` raptor new ${args.slug} --jira=${flags.jira} --no-jira-fetch`,
|
|
134
|
+
].join("\n"), { exit: 1 });
|
|
135
|
+
}
|
|
117
136
|
}
|
|
118
137
|
const designText = specNeedsSeeding
|
|
119
138
|
? [
|
|
@@ -271,9 +290,7 @@ export default class New extends BaseCommand {
|
|
|
271
290
|
? "seeded spec"
|
|
272
291
|
: !specNeedsSeeding
|
|
273
292
|
? "id recorded — spec already authored, not re-seeded"
|
|
274
|
-
:
|
|
275
|
-
? "id only (fetch unavailable)"
|
|
276
|
-
: "id only";
|
|
293
|
+
: "id only";
|
|
277
294
|
this.log(` Jira: ${flags.jira} (${tag})`);
|
|
278
295
|
}
|
|
279
296
|
if (designContext.hasDesign) {
|
|
@@ -416,28 +433,27 @@ export default class New extends BaseCommand {
|
|
|
416
433
|
async fetchJiraContext(root, key) {
|
|
417
434
|
const conn = jiraConn(readConfig(root));
|
|
418
435
|
if (!conn) {
|
|
419
|
-
|
|
420
|
-
return null;
|
|
436
|
+
return { ok: false, reason: `Jira is not connected.` };
|
|
421
437
|
}
|
|
422
438
|
if (!jiraReadyNonInteractive(conn)) {
|
|
423
|
-
|
|
424
|
-
return null;
|
|
439
|
+
return { ok: false, reason: `Jira credentials are missing.` };
|
|
425
440
|
}
|
|
426
441
|
let client;
|
|
427
442
|
try {
|
|
428
443
|
client = await openJiraClient(conn);
|
|
429
444
|
}
|
|
430
445
|
catch {
|
|
431
|
-
|
|
432
|
-
return null;
|
|
446
|
+
return { ok: false, reason: `Jira session expired.` };
|
|
433
447
|
}
|
|
434
448
|
try {
|
|
435
449
|
const issue = await client.getJiraIssue(conn.cloudId, key);
|
|
436
|
-
return mapIssueToSpecContext(issue);
|
|
450
|
+
return { ok: true, context: mapIssueToSpecContext(issue) };
|
|
437
451
|
}
|
|
438
452
|
catch (err) {
|
|
439
|
-
|
|
440
|
-
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
reason: `Could not fetch ${key} from Jira (${err instanceof Error ? err.message : String(err)}).`,
|
|
456
|
+
};
|
|
441
457
|
}
|
|
442
458
|
finally {
|
|
443
459
|
await client.close();
|
package/dist/commands/verify.js
CHANGED
|
@@ -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.
|
|
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": {
|
package/scripts/prepare-npm.mjs
CHANGED
|
@@ -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).
|
|
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"]
|