@aperant/framework 0.8.4 → 0.8.6
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 +156 -0
- package/agents/apt-planner.md +34 -3
- package/dist/cli/commands/coverage-check.d.mts.map +1 -1
- package/dist/cli/commands/coverage-check.mjs +74 -7
- package/dist/cli/commands/coverage-check.mjs.map +1 -1
- package/dist/cli/commands/detect-runtime.d.mts.map +1 -1
- package/dist/cli/commands/detect-runtime.mjs +18 -12
- package/dist/cli/commands/detect-runtime.mjs.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.d.mts +84 -0
- package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -0
- package/dist/cli/coverage-check/user-outcomes.mjs +265 -0
- package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -0
- package/dist/cli/host/detect.mjs +1 -1
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/install-from-source.mjs +3 -3
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/index.d.mts +1 -1
- package/dist/cli/verify-proof/idl/index.mjs +7 -8
- package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/types.d.ts +5 -4
- package/dist/cli/verify-proof/idl/types.d.ts.map +1 -1
- package/dist/cli/verify-proof/idl/types.js +4 -3
- package/dist/cli/verify-proof/idl/types.js.map +1 -1
- package/dist/cli/verify-proof/resolver.d.mts +63 -1
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
- package/dist/cli/verify-proof/resolver.mjs +164 -4
- package/dist/cli/verify-proof/resolver.mjs.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.d.mts +6 -3
- package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.mjs +104 -11
- package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
- package/dist/cli/verify-proof/trust.d.mts +1 -1
- package/dist/cli/verify-proof/trust.d.mts.map +1 -1
- package/dist/cli/verify-proof/trust.mjs +1 -1
- package/dist/driver-sdk/conformance.d.ts +41 -0
- package/dist/driver-sdk/conformance.d.ts.map +1 -0
- package/dist/driver-sdk/conformance.js +123 -0
- package/dist/driver-sdk/conformance.js.map +1 -0
- package/dist/driver-sdk/errors.d.ts +75 -0
- package/dist/driver-sdk/errors.d.ts.map +1 -0
- package/dist/driver-sdk/errors.js +80 -0
- package/dist/driver-sdk/errors.js.map +1 -0
- package/dist/driver-sdk/idl.d.ts +221 -0
- package/dist/driver-sdk/idl.d.ts.map +1 -0
- package/dist/driver-sdk/idl.js +140 -0
- package/dist/driver-sdk/idl.js.map +1 -0
- package/dist/driver-sdk/index.d.ts +28 -0
- package/dist/driver-sdk/index.d.ts.map +1 -0
- package/dist/driver-sdk/index.js +28 -0
- package/dist/driver-sdk/index.js.map +1 -0
- package/dist/driver-sdk/manifest.d.ts +93 -0
- package/dist/driver-sdk/manifest.d.ts.map +1 -0
- package/dist/driver-sdk/manifest.js +12 -0
- package/dist/driver-sdk/manifest.js.map +1 -0
- package/dist/driver-sdk/retry.d.ts +33 -0
- package/dist/driver-sdk/retry.d.ts.map +1 -0
- package/dist/driver-sdk/retry.js +50 -0
- package/dist/driver-sdk/retry.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/agents/apt-planner.md +34 -3
- package/dist/plugin/skills/apt-plan/SKILL.md +50 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -1
- package/dist/plugin/skills/apt-research/SKILL.md +526 -0
- package/dist/plugin/skills/apt-research/appendices/budget-loop.md +397 -0
- package/dist/plugin/skills/apt-research/appendices/claim-graph-schema.md +206 -0
- package/dist/plugin/skills/apt-research/appendices/domain-tools.md +236 -0
- package/dist/plugin/skills/apt-spar/SKILL.md +19 -7
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +41 -2
- package/dist/schemas/quick-task.d.ts +17 -17
- package/drivers/.gitkeep +0 -0
- package/drivers/api/README.md +40 -0
- package/drivers/api/driver.mjs +59 -0
- package/drivers/api/manifest.json +26 -0
- package/drivers/browser/README.md +105 -0
- package/drivers/browser/driver.mjs +134 -0
- package/drivers/browser/manifest.json +35 -0
- package/drivers/cli/README.md +44 -0
- package/drivers/cli/driver.mjs +62 -0
- package/drivers/cli/manifest.json +28 -0
- package/drivers/electron/README.md +64 -0
- package/drivers/electron/driver.mjs +87 -0
- package/drivers/electron/manifest.json +37 -0
- package/package.json +7 -3
- package/skills/apt-plan/SKILL.md +50 -0
- package/skills/apt-planner.md +16 -0
- package/skills/apt-pr-review/SKILL.md +1 -1
- package/skills/apt-research/SKILL.md +526 -0
- package/skills/apt-research/appendices/budget-loop.md +397 -0
- package/skills/apt-research/appendices/claim-graph-schema.md +206 -0
- package/skills/apt-research/appendices/domain-tools.md +236 -0
- package/skills/apt-spar/SKILL.md +19 -7
- package/skills/apt-verify-proof/SKILL.md +41 -2
- package/src/cli/commands/coverage-check.mjs +126 -44
- package/src/cli/commands/detect-runtime.mjs +23 -14
- package/src/cli/coverage-check/user-outcomes.mjs +273 -0
- package/src/cli/host/detect.mjs +1 -1
- package/src/cli/install/install-from-source.mjs +3 -3
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/verify-proof/idl/index.mjs +7 -8
- package/src/cli/verify-proof/idl/types.ts +5 -4
- package/src/cli/verify-proof/manifest-schema.json +2 -1
- package/src/cli/verify-proof/resolver.mjs +171 -4
- package/src/cli/verify-proof/runtime-detect.mjs +99 -10
- package/src/cli/verify-proof/trust.mjs +1 -1
- package/templates/proof-verification.md +32 -3
- package/workflows/verify-proof.md +78 -9
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coverage-check/user-outcomes.mjs — `## User Outcomes` PRD section parser.
|
|
3
|
+
*
|
|
4
|
+
* Pure, file-IO-free. Mirrors the `coverage-check/acceptance-criteria.mjs`
|
|
5
|
+
* pattern: takes spec.md content as a string and returns a structured
|
|
6
|
+
* `{header_present, body_kind, epic?, outcomes[], validation_warnings?}`
|
|
7
|
+
* envelope the coverage-check command consumes.
|
|
8
|
+
*
|
|
9
|
+
* The `## User Outcomes` section is the verify-proof input contract
|
|
10
|
+
* introduced in v0.8.6 (spec.md §ID-01 / §ID-07). The grammar:
|
|
11
|
+
*
|
|
12
|
+
* ## User Outcomes
|
|
13
|
+
*
|
|
14
|
+
* **Epic:** {one-sentence headline}
|
|
15
|
+
*
|
|
16
|
+
* - **ON** [surface]: {prose}
|
|
17
|
+
* - **ON** [surface]: {prose} *(requires ON N)*
|
|
18
|
+
*
|
|
19
|
+
* (refactor-only escape — INSTEAD of the outcome list:)
|
|
20
|
+
* _No user-observable changes — pure refactor._
|
|
21
|
+
*
|
|
22
|
+
* `surface` is the closed set: electron | web | cli | api | electron+web.
|
|
23
|
+
*
|
|
24
|
+
* **Positional ID synthesis (§ID-07 / M1).** The parser counts outcome
|
|
25
|
+
* rows in document order and synthesizes `id = "O" + (i+1)`. The literal
|
|
26
|
+
* `**ON**` regex stays — IDs are NOT author-written. The `requires N`
|
|
27
|
+
* digit captured from `*(requires ON N)*` is validated against the set
|
|
28
|
+
* of synthesized IDs; on mismatch the parser emits a
|
|
29
|
+
* `validation_warnings[]` entry of kind `unknown_requires_target`,
|
|
30
|
+
* skips the dep edge, but still includes the outcome row.
|
|
31
|
+
*
|
|
32
|
+
* See packages/framework/skills/apt-plan/SKILL.md Section 5.B for the
|
|
33
|
+
* authoring contract.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const VALID_SURFACES = new Set(['electron', 'web', 'cli', 'api', 'electron+web'])
|
|
37
|
+
|
|
38
|
+
const HEADING_RE = /^(#{1,6})\s+(.*)$/
|
|
39
|
+
const USER_OUTCOMES_HEADING_RE = /^user\s+outcomes\s*$/i
|
|
40
|
+
const EPIC_RE = /^\*\*Epic:\*\*\s+(.+)$/
|
|
41
|
+
const EMPTY_WITH_NOTE_RE = /^_No user-observable changes — pure refactor\._\s*$/
|
|
42
|
+
const OUTCOME_RE =
|
|
43
|
+
/^-\s+\*\*ON\*\*\s+\[(electron|web|cli|api|electron\+web)\]:\s+(.+?)(\s+\*\(requires\s+ON\s+(\d+)\)\*)?\s*$/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} UserOutcome
|
|
47
|
+
* @property {string} id — synthesized positional ID (O1, O2, ...)
|
|
48
|
+
* @property {('electron'|'web'|'cli'|'api'|'electron+web')} surface
|
|
49
|
+
* @property {string} text — outcome prose (sans the surface tag and requires suffix)
|
|
50
|
+
* @property {number} [requires] — the digit captured from `*(requires ON N)*`, when present and valid
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} ValidationWarning
|
|
55
|
+
* @property {'unknown_requires_target'} kind
|
|
56
|
+
* @property {string} outcome_id — synthesized ID of the outcome carrying the bad `requires`
|
|
57
|
+
* @property {number} requires — the offending digit
|
|
58
|
+
* @property {string} reason — human-readable explanation
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} UserOutcomesParseResult
|
|
63
|
+
* @property {boolean} header_present
|
|
64
|
+
* @property {('absent'|'empty'|'empty-with-note'|'outcomes')} body_kind
|
|
65
|
+
* @property {string} [epic]
|
|
66
|
+
* @property {UserOutcome[]} outcomes
|
|
67
|
+
* @property {ValidationWarning[]} [validation_warnings]
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse the `## User Outcomes` section from a spec.md string.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} specContent
|
|
74
|
+
* @returns {UserOutcomesParseResult}
|
|
75
|
+
*/
|
|
76
|
+
export function parseUserOutcomes(specContent) {
|
|
77
|
+
if (typeof specContent !== 'string' || specContent.length === 0) {
|
|
78
|
+
return { header_present: false, body_kind: 'absent', outcomes: [] }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines = specContent.split('\n')
|
|
82
|
+
|
|
83
|
+
// Find the `## User Outcomes` heading + scope to its body (until the next
|
|
84
|
+
// heading of equal or lower depth).
|
|
85
|
+
let headingLine = -1
|
|
86
|
+
let headingLevel = 0
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
const m = lines[i].trim().match(HEADING_RE)
|
|
89
|
+
if (m && USER_OUTCOMES_HEADING_RE.test(m[2])) {
|
|
90
|
+
headingLine = i
|
|
91
|
+
headingLevel = m[1].length
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (headingLine === -1) {
|
|
97
|
+
return { header_present: false, body_kind: 'absent', outcomes: [] }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Collect body lines from heading+1 until the next heading of ANY
|
|
101
|
+
// depth. The User Outcomes section is intentionally flat — a
|
|
102
|
+
// downstream sub-heading (e.g. `### Acceptance Criteria` following
|
|
103
|
+
// directly without a `## ` separator) ends the body. This matches
|
|
104
|
+
// how authors write the section and avoids accidentally pulling
|
|
105
|
+
// stray prose from later sections into the outcomes pool.
|
|
106
|
+
const bodyLines = []
|
|
107
|
+
for (let i = headingLine + 1; i < lines.length; i++) {
|
|
108
|
+
const trimmed = lines[i].trim()
|
|
109
|
+
if (HEADING_RE.test(trimmed)) break
|
|
110
|
+
bodyLines.push(lines[i])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Classify body shape.
|
|
114
|
+
const nonBlank = bodyLines.map((l) => l.trim()).filter((l) => l.length > 0)
|
|
115
|
+
|
|
116
|
+
if (nonBlank.length === 0) {
|
|
117
|
+
return { header_present: true, body_kind: 'empty', outcomes: [] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Empty-with-note escape — body MUST contain only the verbatim
|
|
121
|
+
// italic-underscore-em-dash-period sentinel (other lines may exist
|
|
122
|
+
// only if they are the Epic header or blank — but the spec says the
|
|
123
|
+
// sentinel REPLACES the outcome list, so we require it to be the only
|
|
124
|
+
// non-blank line OR the only non-Epic non-blank line).
|
|
125
|
+
let epic
|
|
126
|
+
const sentinelLines = []
|
|
127
|
+
const outcomeLines = []
|
|
128
|
+
for (const line of nonBlank) {
|
|
129
|
+
const epicMatch = line.match(EPIC_RE)
|
|
130
|
+
if (epicMatch) {
|
|
131
|
+
epic = epicMatch[1].trim()
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (EMPTY_WITH_NOTE_RE.test(line)) {
|
|
135
|
+
sentinelLines.push(line)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
outcomeLines.push(line)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If the sentinel is present and there are NO outcome lines, this is
|
|
142
|
+
// the empty-with-note branch. The sentinel may co-exist with an Epic
|
|
143
|
+
// header — that's not required but not forbidden either.
|
|
144
|
+
if (sentinelLines.length > 0 && outcomeLines.length === 0) {
|
|
145
|
+
/** @type {UserOutcomesParseResult} */
|
|
146
|
+
const result = {
|
|
147
|
+
header_present: true,
|
|
148
|
+
body_kind: 'empty-with-note',
|
|
149
|
+
outcomes: [],
|
|
150
|
+
}
|
|
151
|
+
if (epic) result.epic = epic
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// If neither the sentinel nor any outcome rows are present, the body
|
|
156
|
+
// is effectively empty (header present, no parseable content).
|
|
157
|
+
if (outcomeLines.length === 0) {
|
|
158
|
+
/** @type {UserOutcomesParseResult} */
|
|
159
|
+
const result = {
|
|
160
|
+
header_present: true,
|
|
161
|
+
body_kind: 'empty',
|
|
162
|
+
outcomes: [],
|
|
163
|
+
}
|
|
164
|
+
if (epic) result.epic = epic
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Parse outcome rows in document order. Skip non-matching lines (e.g.
|
|
169
|
+
// stray prose between outcomes) — they don't count as outcomes.
|
|
170
|
+
/** @type {UserOutcome[]} */
|
|
171
|
+
const outcomes = []
|
|
172
|
+
const rawRequires = []
|
|
173
|
+
for (const line of outcomeLines) {
|
|
174
|
+
const m = line.match(OUTCOME_RE)
|
|
175
|
+
if (!m) continue
|
|
176
|
+
const surface = /** @type {UserOutcome['surface']} */ (m[1])
|
|
177
|
+
if (!VALID_SURFACES.has(surface)) continue
|
|
178
|
+
const text = m[2].trim()
|
|
179
|
+
const id = 'O' + (outcomes.length + 1)
|
|
180
|
+
const outcome = /** @type {UserOutcome} */ ({ id, surface, text })
|
|
181
|
+
if (m[4]) {
|
|
182
|
+
const n = Number.parseInt(m[4], 10)
|
|
183
|
+
rawRequires.push({ outcomeIndex: outcomes.length, requires: n })
|
|
184
|
+
}
|
|
185
|
+
outcomes.push(outcome)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (outcomes.length === 0) {
|
|
189
|
+
// Header present, prose present, but no parseable `**ON**` rows —
|
|
190
|
+
// effectively empty.
|
|
191
|
+
/** @type {UserOutcomesParseResult} */
|
|
192
|
+
const result = {
|
|
193
|
+
header_present: true,
|
|
194
|
+
body_kind: 'empty',
|
|
195
|
+
outcomes: [],
|
|
196
|
+
}
|
|
197
|
+
if (epic) result.epic = epic
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate `requires` cross-refs against the set of synthesized IDs.
|
|
202
|
+
/** @type {ValidationWarning[]} */
|
|
203
|
+
const warnings = []
|
|
204
|
+
for (const r of rawRequires) {
|
|
205
|
+
if (r.requires >= 1 && r.requires <= outcomes.length) {
|
|
206
|
+
outcomes[r.outcomeIndex].requires = r.requires
|
|
207
|
+
} else {
|
|
208
|
+
warnings.push({
|
|
209
|
+
kind: 'unknown_requires_target',
|
|
210
|
+
outcome_id: outcomes[r.outcomeIndex].id,
|
|
211
|
+
requires: r.requires,
|
|
212
|
+
reason: `requires ON ${r.requires} but only ${outcomes.length} outcome(s) parsed`,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @type {UserOutcomesParseResult} */
|
|
218
|
+
const result = {
|
|
219
|
+
header_present: true,
|
|
220
|
+
body_kind: 'outcomes',
|
|
221
|
+
outcomes,
|
|
222
|
+
}
|
|
223
|
+
if (epic) result.epic = epic
|
|
224
|
+
if (warnings.length > 0) result.validation_warnings = warnings
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compute the coverage-check gate result for a parsed user-outcomes
|
|
230
|
+
* envelope. Per §ID-07 table — warn-not-block in v0.8.x, hard-block in
|
|
231
|
+
* v0.9.0.
|
|
232
|
+
*
|
|
233
|
+
* | header_present | body_kind | v0.8.x | v0.9.0 |
|
|
234
|
+
* |----------------|-------------------|--------|--------|
|
|
235
|
+
* | false | — | warn | block |
|
|
236
|
+
* | true | empty-with-note | pass | pass |
|
|
237
|
+
* | true | outcomes (≥1) | pass | pass |
|
|
238
|
+
* | true | empty | block | block |
|
|
239
|
+
*
|
|
240
|
+
* @param {UserOutcomesParseResult} parsed
|
|
241
|
+
* @param {string} frameworkVersion — semver string from the kernel's package.json (e.g. "0.8.6")
|
|
242
|
+
* @returns {('pass'|'warn'|'block')}
|
|
243
|
+
*/
|
|
244
|
+
export function userOutcomesGateResult(parsed, frameworkVersion) {
|
|
245
|
+
if (parsed.body_kind === 'empty-with-note' || parsed.body_kind === 'outcomes') {
|
|
246
|
+
return 'pass'
|
|
247
|
+
}
|
|
248
|
+
if (parsed.body_kind === 'empty') {
|
|
249
|
+
// Header present, body empty — author started the section and forgot
|
|
250
|
+
// to fill it in. Block at every version.
|
|
251
|
+
return 'block'
|
|
252
|
+
}
|
|
253
|
+
// Missing header (body_kind: 'absent'). Warn in 0.8.x, block in 0.9.0+.
|
|
254
|
+
const major = parseFrameworkMajorMinor(frameworkVersion)
|
|
255
|
+
if (major === null) return 'warn'
|
|
256
|
+
const { major: maj, minor: min } = major
|
|
257
|
+
if (maj > 0 || (maj === 0 && min >= 9)) return 'block'
|
|
258
|
+
return 'warn'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Parse the leading `major.minor` of a semver string. Returns null on
|
|
263
|
+
* malformed input (caller treats as "warn" — fail-soft).
|
|
264
|
+
*
|
|
265
|
+
* @param {string} version
|
|
266
|
+
* @returns {{major: number, minor: number} | null}
|
|
267
|
+
*/
|
|
268
|
+
function parseFrameworkMajorMinor(version) {
|
|
269
|
+
if (typeof version !== 'string') return null
|
|
270
|
+
const m = version.match(/^(\d+)\.(\d+)/)
|
|
271
|
+
if (!m) return null
|
|
272
|
+
return { major: Number.parseInt(m[1], 10), minor: Number.parseInt(m[2], 10) }
|
|
273
|
+
}
|
package/src/cli/host/detect.mjs
CHANGED
|
@@ -345,6 +345,6 @@ function detectRawCli(partner, pathEntries, exists) {
|
|
|
345
345
|
function rawCliInvocation(partner) {
|
|
346
346
|
if (partner === 'codex') return 'codex exec'
|
|
347
347
|
if (partner === 'claude') return 'claude -p'
|
|
348
|
-
if (partner === 'gemini') return 'gemini
|
|
348
|
+
if (partner === 'gemini') return 'gemini'
|
|
349
349
|
return partner
|
|
350
350
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `apt-tools install-from-source <project-dir>` for the Aperant-contributor
|
|
5
5
|
* self-host case. Resolves the workspace `@aperant/framework` chain — packs
|
|
6
|
-
* the framework itself AND every `workspace:*` dependency (
|
|
7
|
-
*
|
|
8
|
-
* list to a single `npm install --prefix` invocation so
|
|
6
|
+
* the framework itself AND every `workspace:*` dependency (none today after
|
|
7
|
+
* the 0.8.5 driver-sdk inline, but the walker is general). Hands the full
|
|
8
|
+
* tarball list to a single `npm install --prefix` invocation so any future
|
|
9
9
|
* unpublished-pair case stops blowing up with `EUNSUPPORTEDPROTOCOL`.
|
|
10
10
|
*
|
|
11
11
|
* Stable envelope:
|
|
@@ -109,6 +109,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
|
109
109
|
'agents/apt-prototype.md',
|
|
110
110
|
'agents/apt-quick.md',
|
|
111
111
|
'agents/apt-release-notes.md',
|
|
112
|
+
'agents/apt-research.md',
|
|
112
113
|
'agents/apt-resume.md',
|
|
113
114
|
'agents/apt-review.md',
|
|
114
115
|
'agents/apt-roadmap.md',
|
|
@@ -151,6 +152,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
|
151
152
|
'commands/apt-prototype.md',
|
|
152
153
|
'commands/apt-quick.md',
|
|
153
154
|
'commands/apt-release-notes.md',
|
|
155
|
+
'commands/apt-research.md',
|
|
154
156
|
'commands/apt-resume.md',
|
|
155
157
|
'commands/apt-review.md',
|
|
156
158
|
'commands/apt-roadmap.md',
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* idl/index.mjs — framework-side re-export of the IDL v1 runtime registry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* conformance kit)
|
|
8
|
-
*
|
|
4
|
+
* Single source of truth lives at packages/framework/src/driver-sdk/idl.ts
|
|
5
|
+
* (inlined in 0.8.5 — originally `@aperant/driver-sdk`). This file
|
|
6
|
+
* re-exports those runtime values so existing callers (bundled drivers,
|
|
7
|
+
* resolver, driver-doctor, conformance kit) keep working without a
|
|
8
|
+
* refactor of every callsite.
|
|
9
9
|
*
|
|
10
|
-
* If you add a verb, edit packages/framework
|
|
11
|
-
* file requires no maintenance.
|
|
10
|
+
* If you add a verb, edit packages/framework/src/driver-sdk/idl.ts.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
export {
|
|
@@ -21,4 +20,4 @@ export {
|
|
|
21
20
|
isVerbResult,
|
|
22
21
|
LIFECYCLE_VERBS,
|
|
23
22
|
validateVerbArgs,
|
|
24
|
-
} from '@aperant/driver-sdk'
|
|
23
|
+
} from '@aperant/framework/driver-sdk'
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* idl/types.ts — re-export the SDK's IDL types for framework-internal use.
|
|
3
3
|
*
|
|
4
|
-
* Single source of truth lives
|
|
5
|
-
* code imports from here so future moves
|
|
6
|
-
*
|
|
4
|
+
* Single source of truth lives at packages/framework/src/driver-sdk/idl.ts
|
|
5
|
+
* (inlined in 0.8.5). Framework code imports from here so future moves
|
|
6
|
+
* (e.g. an internal-only IDL extension or re-extraction) only touch this
|
|
7
|
+
* re-export, not every call site.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
export type {
|
|
@@ -38,4 +39,4 @@ export type {
|
|
|
38
39
|
VerbResult,
|
|
39
40
|
WaitForEventArgs,
|
|
40
41
|
WaitForIdleArgs,
|
|
41
|
-
} from '@aperant/driver-sdk'
|
|
42
|
+
} from '@aperant/framework/driver-sdk'
|
|
@@ -16,12 +16,24 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Ties are broken by the next dimension; if every dimension is equal the
|
|
18
18
|
* driver with the lexicographically smaller driverId wins (deterministic).
|
|
19
|
+
*
|
|
20
|
+
* v0.8.6 — `resolveDriversForOutcomes(opts)` adds per-outcome driver
|
|
21
|
+
* dispatch for cross-surface specs (per spec §ID-03). It accepts the
|
|
22
|
+
* positional `outcomes[]` produced by `parseUserOutcomes` and returns
|
|
23
|
+
* `Array<{outcome_id, surface, driver}>` — one entry per
|
|
24
|
+
* (outcome, surface) pair. For `[electron+web]` outcomes it emits TWO
|
|
25
|
+
* entries with the same outcome_id and different drivers. The
|
|
26
|
+
* outcome_id field is passed through verbatim from parser → resolver
|
|
27
|
+
* → workflow runner with no name rewriting (the positional-ID
|
|
28
|
+
* contract).
|
|
29
|
+
*
|
|
30
|
+
* Existing `resolveDriver()` and `scoreDriver()` exports are unchanged.
|
|
19
31
|
*/
|
|
20
32
|
|
|
21
|
-
import { UnsatisfiedRequiredCapabilityError } from '@aperant/driver-sdk'
|
|
33
|
+
import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'
|
|
22
34
|
|
|
23
35
|
/**
|
|
24
|
-
* @typedef {import('@aperant/driver-sdk').DriverManifest} DriverManifest
|
|
36
|
+
* @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
|
|
25
37
|
* @typedef {import('./runtime-detect.mjs').RuntimeCapabilities} RuntimeCapabilities
|
|
26
38
|
*/
|
|
27
39
|
|
|
@@ -131,8 +143,29 @@ export function resolveDriver(opts) {
|
|
|
131
143
|
stableOnly.push(d)
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
//
|
|
135
|
-
|
|
146
|
+
// Override is hard precedence (per opts.override docstring "explicit
|
|
147
|
+
// driverId precedence"): when set, try override-listed drivers in
|
|
148
|
+
// override order — the first one that satisfies every required
|
|
149
|
+
// capability wins, regardless of compatibility/priority/stability
|
|
150
|
+
// ranking. Non-matching override entries land in fallback. Only when
|
|
151
|
+
// every override entry misses a required cap do we fall through to
|
|
152
|
+
// score-based pick on the remaining drivers.
|
|
153
|
+
const overrideListed = stableOnly.filter((m) => overrideSet.has(m.driverId))
|
|
154
|
+
for (const candidate of overrideListed) {
|
|
155
|
+
const caps = new Set(candidate.capabilities ?? [])
|
|
156
|
+
const missing = required.filter((c) => !caps.has(c))
|
|
157
|
+
if (missing.length === 0) {
|
|
158
|
+
return { selected: candidate, fallback_attempts: fallback }
|
|
159
|
+
}
|
|
160
|
+
fallback.push({
|
|
161
|
+
driver_id: candidate.driverId,
|
|
162
|
+
reason: `override-listed but missing required capabilities: ${missing.join(', ')}`,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Score every non-override survivor against required capabilities.
|
|
167
|
+
const nonOverride = stableOnly.filter((m) => !overrideSet.has(m.driverId))
|
|
168
|
+
const scored = nonOverride.map((m) => ({ manifest: m, ...scoreDriver(m, runtime, required) }))
|
|
136
169
|
|
|
137
170
|
// Sort highest-first, tie-break on driverId for determinism.
|
|
138
171
|
scored.sort((a, b) => {
|
|
@@ -161,3 +194,137 @@ export function resolveDriver(opts) {
|
|
|
161
194
|
fallback,
|
|
162
195
|
)
|
|
163
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Map a User Outcomes surface tag to the runtime-capability flag a driver
|
|
200
|
+
* must carry (via `matchesRuntime`) to be eligible for that surface.
|
|
201
|
+
*
|
|
202
|
+
* Closed surface set: `electron | web | cli | api | electron+web`.
|
|
203
|
+
* `electron+web` is NOT a single flag — it's a cross-surface tag the
|
|
204
|
+
* workflow runner expands into two single-surface dispatches.
|
|
205
|
+
*
|
|
206
|
+
* @param {('electron'|'web'|'cli'|'api')} surface
|
|
207
|
+
* @returns {('is_electron'|'is_web_frontend'|'has_node')}
|
|
208
|
+
*/
|
|
209
|
+
export function surfaceToRequiredFlag(surface) {
|
|
210
|
+
switch (surface) {
|
|
211
|
+
case 'electron':
|
|
212
|
+
return 'is_electron'
|
|
213
|
+
case 'web':
|
|
214
|
+
return 'is_web_frontend'
|
|
215
|
+
case 'cli':
|
|
216
|
+
case 'api':
|
|
217
|
+
return 'has_node'
|
|
218
|
+
default:
|
|
219
|
+
throw new Error(`surfaceToRequiredFlag: unknown surface "${surface}"`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve a driver per outcome surface tag, NOT per verify-proof run
|
|
225
|
+
* (per spec §ID-03). For each input outcome the function returns one
|
|
226
|
+
* `{outcome_id, surface, driver}` entry; for `[electron+web]` outcomes
|
|
227
|
+
* it returns TWO entries (electron + web) sharing the same outcome_id.
|
|
228
|
+
*
|
|
229
|
+
* The `outcome_id` field on each returned entry is the positional ID
|
|
230
|
+
* produced by `parseUserOutcomes` (e.g. `O1`, `O2`). The resolver does
|
|
231
|
+
* NOT rewrite or regenerate IDs — they pass through verbatim so the
|
|
232
|
+
* workflow runner can correlate proof artifacts to spec lines.
|
|
233
|
+
*
|
|
234
|
+
* Each per-surface dispatch synthesizes a runtime-capabilities view
|
|
235
|
+
* that ONLY claims the surface's required flag (e.g. `is_electron`
|
|
236
|
+
* alone for an electron-surface outcome, even when the project also
|
|
237
|
+
* carries `is_web_frontend`). This forces the existing scoring to
|
|
238
|
+
* pick the per-surface driver — without it, a project with BOTH
|
|
239
|
+
* electron AND web capabilities would always select the highest-
|
|
240
|
+
* priority bundled driver (currently `browser`) for every outcome.
|
|
241
|
+
*
|
|
242
|
+
* Delegates to the existing `resolveDriver()` for the actual scoring
|
|
243
|
+
* — the scoring code stays in one place.
|
|
244
|
+
*
|
|
245
|
+
* @param {Object} opts
|
|
246
|
+
* @param {DriverManifest[]} opts.drivers
|
|
247
|
+
* @param {RuntimeCapabilities} opts.runtime
|
|
248
|
+
* @param {Array<{id: string, surface: 'electron'|'web'|'cli'|'api'|'electron+web'}>} opts.outcomes
|
|
249
|
+
* @param {string[]} [opts.override]
|
|
250
|
+
* @param {string[]} [opts.required_stability]
|
|
251
|
+
* @param {string[]} [opts.required_capabilities] — defaults to ['screenshot']
|
|
252
|
+
* @returns {Array<{outcome_id: string, surface: string, driver: DriverManifest, fallback_attempts: Array<{driver_id: string, reason: string}>}>}
|
|
253
|
+
*/
|
|
254
|
+
export function resolveDriversForOutcomes(opts) {
|
|
255
|
+
const drivers = opts.drivers ?? []
|
|
256
|
+
const runtime = opts.runtime
|
|
257
|
+
const outcomes = opts.outcomes ?? []
|
|
258
|
+
const required = opts.required_capabilities ?? ['screenshot']
|
|
259
|
+
const override = opts.override
|
|
260
|
+
const stability = opts.required_stability
|
|
261
|
+
|
|
262
|
+
/** @type {Array<{outcome_id: string, surface: string, driver: DriverManifest, fallback_attempts: Array<{driver_id: string, reason: string}>}>} */
|
|
263
|
+
const results = []
|
|
264
|
+
|
|
265
|
+
for (const outcome of outcomes) {
|
|
266
|
+
const surfacesToDispatch =
|
|
267
|
+
outcome.surface === 'electron+web' ? ['electron', 'web'] : [outcome.surface]
|
|
268
|
+
|
|
269
|
+
for (const surface of surfacesToDispatch) {
|
|
270
|
+
const flag = surfaceToRequiredFlag(/** @type {'electron'|'web'|'cli'|'api'} */ (surface))
|
|
271
|
+
// Synthesize a per-surface runtime view: only the surface's
|
|
272
|
+
// required flag is on; every other flag is masked off so the
|
|
273
|
+
// scoring prefers per-surface drivers (otherwise, on a
|
|
274
|
+
// multi-runtime project, the highest-priority bundled driver
|
|
275
|
+
// always wins). `has_node` stays on for cli/api since multiple
|
|
276
|
+
// drivers may target node.
|
|
277
|
+
const surfaceRuntime = maskRuntimeForSurface(runtime, flag)
|
|
278
|
+
const resolved = resolveDriver({
|
|
279
|
+
drivers,
|
|
280
|
+
runtime: surfaceRuntime,
|
|
281
|
+
required_capabilities: required,
|
|
282
|
+
...(override ? { override } : {}),
|
|
283
|
+
...(stability ? { required_stability: stability } : {}),
|
|
284
|
+
})
|
|
285
|
+
results.push({
|
|
286
|
+
outcome_id: outcome.id,
|
|
287
|
+
surface,
|
|
288
|
+
driver: resolved.selected,
|
|
289
|
+
fallback_attempts: resolved.fallback_attempts,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return results
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Return a copy of `runtime` with every flag false EXCEPT the one named
|
|
299
|
+
* by `flag`. Used to scope the resolver's compatibility check to a
|
|
300
|
+
* single surface during per-outcome dispatch.
|
|
301
|
+
*
|
|
302
|
+
* @param {RuntimeCapabilities} runtime
|
|
303
|
+
* @param {string} flag — one of: is_electron | is_web_frontend | has_node
|
|
304
|
+
* @returns {RuntimeCapabilities}
|
|
305
|
+
*/
|
|
306
|
+
function maskRuntimeForSurface(runtime, flag) {
|
|
307
|
+
/** @type {RuntimeCapabilities} */
|
|
308
|
+
const masked = {
|
|
309
|
+
is_electron: false,
|
|
310
|
+
is_tauri: false,
|
|
311
|
+
is_nextjs: false,
|
|
312
|
+
is_nuxt: false,
|
|
313
|
+
is_web_frontend: false,
|
|
314
|
+
is_expo: false,
|
|
315
|
+
is_react_native: false,
|
|
316
|
+
has_node: false,
|
|
317
|
+
}
|
|
318
|
+
// Only flip the surface's flag on if the project actually carries it
|
|
319
|
+
// (otherwise the resolver should genuinely fail — fail-closed).
|
|
320
|
+
if (flag === 'is_electron' && runtime.is_electron) masked.is_electron = true
|
|
321
|
+
if (flag === 'is_web_frontend' && runtime.is_web_frontend) {
|
|
322
|
+
masked.is_web_frontend = true
|
|
323
|
+
// Carry the framework hint too (Next.js / Nuxt) so drivers that
|
|
324
|
+
// target browser via those frameworks still match.
|
|
325
|
+
masked.is_nextjs = runtime.is_nextjs
|
|
326
|
+
masked.is_nuxt = runtime.is_nuxt
|
|
327
|
+
}
|
|
328
|
+
if (flag === 'has_node') masked.has_node = runtime.has_node
|
|
329
|
+
return masked
|
|
330
|
+
}
|