@aperant/framework 0.8.7 → 0.10.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/agents/apt-pr-review-fixer.md +9 -4
  3. package/agents/apt-verifier.md +6 -3
  4. package/dist/cli/commands/init.d.mts.map +1 -1
  5. package/dist/cli/commands/init.mjs +157 -23
  6. package/dist/cli/commands/init.mjs.map +1 -1
  7. package/dist/cli/commands/toolchain-detect.d.mts.map +1 -1
  8. package/dist/cli/commands/toolchain-detect.mjs +44 -0
  9. package/dist/cli/commands/toolchain-detect.mjs.map +1 -1
  10. package/dist/cli/commands/triage.d.mts.map +1 -1
  11. package/dist/cli/commands/triage.mjs +346 -13
  12. package/dist/cli/commands/triage.mjs.map +1 -1
  13. package/dist/cli/commands/uninstall.d.mts.map +1 -1
  14. package/dist/cli/commands/uninstall.mjs +11 -8
  15. package/dist/cli/commands/uninstall.mjs.map +1 -1
  16. package/dist/cli/consistency/parse-review.mjs +5 -3
  17. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  18. package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
  19. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +10 -8
  20. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
  21. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  22. package/dist/cli/gate/gates/review-clean.mjs +4 -2
  23. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  24. package/dist/cli/install/mcp-provision.d.mts +26 -0
  25. package/dist/cli/install/mcp-provision.d.mts.map +1 -0
  26. package/dist/cli/install/mcp-provision.mjs +281 -0
  27. package/dist/cli/install/mcp-provision.mjs.map +1 -0
  28. package/dist/cli/install/mcp-server-specs.d.mts +80 -0
  29. package/dist/cli/install/mcp-server-specs.d.mts.map +1 -0
  30. package/dist/cli/install/mcp-server-specs.mjs +102 -0
  31. package/dist/cli/install/mcp-server-specs.mjs.map +1 -0
  32. package/dist/cli/install/runtime-detect.d.mts +27 -0
  33. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  34. package/dist/cli/install/runtime-detect.mjs +32 -0
  35. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  36. package/dist/cli/install/toml-merge.d.mts +40 -0
  37. package/dist/cli/install/toml-merge.d.mts.map +1 -0
  38. package/dist/cli/install/toml-merge.mjs +134 -0
  39. package/dist/cli/install/toml-merge.mjs.map +1 -0
  40. package/dist/cli/util/aperant-section.d.mts +12 -0
  41. package/dist/cli/util/aperant-section.d.mts.map +1 -1
  42. package/dist/cli/util/aperant-section.mjs +12 -0
  43. package/dist/cli/util/aperant-section.mjs.map +1 -1
  44. package/dist/cli/util/copy.d.mts +27 -0
  45. package/dist/cli/util/copy.d.mts.map +1 -1
  46. package/dist/cli/util/copy.mjs +49 -5
  47. package/dist/cli/util/copy.mjs.map +1 -1
  48. package/dist/cli/util/events-path.d.mts +8 -0
  49. package/dist/cli/util/events-path.d.mts.map +1 -0
  50. package/dist/cli/util/events-path.mjs +23 -0
  51. package/dist/cli/util/events-path.mjs.map +1 -0
  52. package/dist/cli/util/runtime-capabilities.d.mts +30 -0
  53. package/dist/cli/util/runtime-capabilities.d.mts.map +1 -0
  54. package/dist/cli/util/runtime-capabilities.mjs +214 -0
  55. package/dist/cli/util/runtime-capabilities.mjs.map +1 -0
  56. package/dist/cli/verify-proof/audit.d.mts +2 -7
  57. package/dist/cli/verify-proof/audit.d.mts.map +1 -1
  58. package/dist/cli/verify-proof/audit.mjs +11 -17
  59. package/dist/cli/verify-proof/audit.mjs.map +1 -1
  60. package/dist/cli/verify-proof/idl/index.d.mts +12 -1
  61. package/dist/cli/verify-proof/idl/index.d.mts.map +1 -1
  62. package/dist/cli/verify-proof/idl/index.mjs +50 -1
  63. package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
  64. package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
  65. package/dist/cli/verify-proof/resolver.mjs +51 -1
  66. package/dist/cli/verify-proof/resolver.mjs.map +1 -1
  67. package/dist/cli/verify-proof/runtime-detect.d.mts +2 -29
  68. package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
  69. package/dist/cli/verify-proof/runtime-detect.mjs +8 -203
  70. package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
  71. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  72. package/dist/plugin/agents/apt-pr-review-fixer.md +9 -4
  73. package/dist/plugin/agents/apt-verifier.md +6 -3
  74. package/dist/plugin/skills/apt-pr-review/SKILL.md +106 -15
  75. package/dist/plugin/skills/apt-setup/SKILL.md +7 -6
  76. package/dist/plugin/skills/apt-triage/SKILL.md +8 -6
  77. package/dist/plugin/skills/apt-update/SKILL.md +1 -1
  78. package/dist/plugin/skills/apt-verify/SKILL.md +1 -1
  79. package/dist/plugin/skills/apt-verify-proof/SKILL.md +25 -1
  80. package/drivers/browser/skill/agent-browser/SKILL.md +51 -0
  81. package/drivers/tauri/README.md +107 -0
  82. package/drivers/tauri/driver.mjs +88 -0
  83. package/drivers/tauri/manifest.json +37 -0
  84. package/drivers/tauri/scripts/tauri-mcp.sh +41 -0
  85. package/package.json +1 -1
  86. package/skills/apt-pr-review/SKILL.md +106 -15
  87. package/skills/apt-setup/SKILL.md +7 -6
  88. package/skills/apt-triage/SKILL.md +8 -6
  89. package/skills/apt-update/SKILL.md +1 -1
  90. package/skills/apt-verify/SKILL.md +1 -1
  91. package/skills/apt-verify-proof/SKILL.md +25 -1
  92. package/src/cli/commands/init.mjs +163 -19
  93. package/src/cli/commands/toolchain-detect.mjs +44 -0
  94. package/src/cli/commands/triage.mjs +352 -11
  95. package/src/cli/commands/uninstall.mjs +11 -8
  96. package/src/cli/consistency/parse-review.mjs +5 -3
  97. package/src/cli/consistency/rules/r5-verdict-consistency.mjs +14 -9
  98. package/src/cli/gate/gates/review-clean.mjs +4 -2
  99. package/src/cli/install/mcp-provision.mjs +293 -0
  100. package/src/cli/install/mcp-server-specs.mjs +101 -0
  101. package/src/cli/install/runtime-detect.mjs +36 -0
  102. package/src/cli/install/toml-merge.mjs +139 -0
  103. package/src/cli/util/aperant-section.mjs +14 -0
  104. package/src/cli/util/copy.mjs +53 -8
  105. package/src/cli/util/events-path.mjs +24 -0
  106. package/src/cli/util/runtime-capabilities.mjs +216 -0
  107. package/src/cli/verify-proof/audit.mjs +11 -17
  108. package/src/cli/verify-proof/idl/index.mjs +49 -11
  109. package/src/cli/verify-proof/resolver.mjs +49 -2
  110. package/src/cli/verify-proof/runtime-detect.mjs +11 -204
  111. package/templates/config.json +2 -1
@@ -22,6 +22,20 @@ import { discoverSkills } from '../route/skill-discover.mjs'
22
22
 
23
23
  const SECTION_START = '<!-- APT:framework-start -->'
24
24
  const SECTION_END = '<!-- APT:framework-end -->'
25
+
26
+ /**
27
+ * Matches the full APT managed block (start marker through end marker,
28
+ * inclusive). Exported so callers in the same package can reference a
29
+ * single source rather than inlining duplicate regex literals.
30
+ *
31
+ * `APT_SECTION_RE` — no trailing newline; used for in-place replace.
32
+ * `APT_SECTION_STRIP_RE` — includes optional trailing newline; used for
33
+ * strip operations that want to remove the newline
34
+ * the block was preceded/followed by.
35
+ */
36
+ export const APT_SECTION_RE = /<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->/
37
+ export const APT_SECTION_STRIP_RE =
38
+ /<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->\n?/
25
39
  const SECTION_HEADING = '## Aperant Framework'
26
40
  const APPENDIX_REL_PATH = ['templates', 'aperant-claude-md-appendix.md']
27
41
 
@@ -20,7 +20,21 @@ import {
20
20
  writeFileSync,
21
21
  } from 'node:fs'
22
22
  import { join } from 'node:path'
23
- import { extractAptSectionRowIds } from './aperant-section.mjs'
23
+ import { APT_SECTION_RE, extractAptSectionRowIds } from './aperant-section.mjs'
24
+
25
+ /**
26
+ * Return the row IDs present in `existing` that are NOT present in
27
+ * `aptSection`. An empty array means no foreign rows (safe to overwrite).
28
+ *
29
+ * @param {string} existing Current on-disk file contents.
30
+ * @param {string} aptSection Output of buildAptSection().
31
+ * @returns {string[]}
32
+ */
33
+ function computeForeignRowIds(existing, aptSection) {
34
+ const existingIds = new Set(extractAptSectionRowIds(existing))
35
+ const generatedIds = new Set(extractAptSectionRowIds(aptSection))
36
+ return [...existingIds].filter((id) => !generatedIds.has(id))
37
+ }
24
38
 
25
39
  export function copyDirRecursive(src, dest) {
26
40
  mkdirSync(dest, { recursive: true })
@@ -35,6 +49,42 @@ export function copyDirRecursive(src, dest) {
35
49
  }
36
50
  }
37
51
 
52
+ /**
53
+ * Preflight-only sibling of `injectInstructionFile`. Runs the same
54
+ * foreign-row defensive check against `filePath` but NEVER writes — used by
55
+ * the multi-target init flow to check every instruction target up-front so a
56
+ * single aborting target leaves NO file partially written (US-03 / AC3).
57
+ *
58
+ * Returns the same status vocabulary as `injectInstructionFile` minus the
59
+ * write side-effect: `'created'` (file absent → would be appended-to),
60
+ * `'updated'` (markers present, no foreign rows → would be replaced),
61
+ * `'aborted-foreign-rows'` (markers present with rows not in the generated
62
+ * set → init must abort). `force` short-circuits the foreign-row gate exactly
63
+ * as the writer does.
64
+ *
65
+ * @param {string} filePath
66
+ * @param {Object} opts
67
+ * @param {string} opts.aptSection Output of buildAptSection().
68
+ * @param {boolean} [opts.force=false]
69
+ * Bypass the foreign-row check.
70
+ * @returns {{ status: 'created' | 'updated' | 'aborted-foreign-rows', foreignRowIds?: string[] }}
71
+ */
72
+ export function checkInstructionFile(filePath, { aptSection, force = false }) {
73
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : ''
74
+
75
+ if (existing.includes('<!-- APT:framework-start -->')) {
76
+ if (!force) {
77
+ const foreignRowIds = computeForeignRowIds(existing, aptSection)
78
+ if (foreignRowIds.length > 0) {
79
+ return { status: 'aborted-foreign-rows', foreignRowIds }
80
+ }
81
+ }
82
+ return { status: 'updated' }
83
+ }
84
+
85
+ return { status: 'created' }
86
+ }
87
+
38
88
  /**
39
89
  * Write the (already-generated) APT section into `filePath`. Creates the
40
90
  * file if missing; replaces the section in-place if the markers are
@@ -60,17 +110,12 @@ export function injectInstructionFile(filePath, { aptSection, force = false }) {
60
110
 
61
111
  if (existing.includes('<!-- APT:framework-start -->')) {
62
112
  if (!force) {
63
- const existingIds = new Set(extractAptSectionRowIds(existing))
64
- const generatedIds = new Set(extractAptSectionRowIds(aptSection))
65
- const foreignRowIds = [...existingIds].filter((id) => !generatedIds.has(id))
113
+ const foreignRowIds = computeForeignRowIds(existing, aptSection)
66
114
  if (foreignRowIds.length > 0) {
67
115
  return { status: 'aborted-foreign-rows', written: false, foreignRowIds }
68
116
  }
69
117
  }
70
- const updated = existing.replace(
71
- /<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->/,
72
- aptSection,
73
- )
118
+ const updated = existing.replace(APT_SECTION_RE, aptSection)
74
119
  writeFileSync(filePath, updated, 'utf-8')
75
120
  return { status: 'updated', written: true }
76
121
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * util/events-path.mjs — canonical path computation for the per-day event
3
+ * log at `.aperant/events/{YYYY-MM-DD}.jsonl` (ID-05).
4
+ *
5
+ * Lives in util/ so both the install layer (install/mcp-provision.mjs) and the
6
+ * verify-proof layer (verify-proof/audit.mjs) compute the same path WITHOUT
7
+ * install/ importing from verify-proof/ (the layer-boundary rule). Pure
8
+ * function — no fs, no exec.
9
+ */
10
+
11
+ import { join, resolve } from 'node:path'
12
+
13
+ /**
14
+ * Compute today's events filename relative to projectDir.
15
+ * @param {string} projectDir
16
+ * @param {Date} [now]
17
+ * @returns {string}
18
+ */
19
+ export function dailyEventsPath(projectDir, now = new Date()) {
20
+ const yyyy = now.getUTCFullYear()
21
+ const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
22
+ const dd = String(now.getUTCDate()).padStart(2, '0')
23
+ return join(resolve(projectDir), '.aperant/events', `${yyyy}-${mm}-${dd}.jsonl`)
24
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * util/runtime-capabilities.mjs — pure project-shape capability detection.
3
+ *
4
+ * Extracted from packages/core/src/prompts/prompt-loader.ts:572-595 so the
5
+ * framework CLI is self-contained (no @aperant/core dep). Same heuristics:
6
+ * read package.json's dependencies + devDependencies, return a capability
7
+ * record the resolver can score against.
8
+ *
9
+ * Lives in util/ so BOTH the verify-proof layer (verify-proof/runtime-detect.mjs
10
+ * re-exports it) AND the install layer (install/runtime-detect.mjs re-exports
11
+ * it) can share one implementation without install/ importing from
12
+ * verify-proof/ (the layer-boundary rule).
13
+ *
14
+ * Pattern: pure data in → pure data out. No filesystem walking, no exec.
15
+ * Use loadRuntimeCapabilities(projectDir) for the cwd-rooted variant.
16
+ *
17
+ * v0.8.6 — `loadRuntimeCapabilities` now ALSO walks `apps/*` by default
18
+ * (configurable via the second arg `{monorepo_globs}`) and OR-merges
19
+ * each match's `package.json` capabilities with the root. The default
20
+ * `['apps/*']` matches the canonical Aperant monorepo layout; adopters
21
+ * whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
22
+ * custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
23
+ * walk entirely (root-only escape hatch).
24
+ */
25
+
26
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
27
+ import { join, resolve } from 'node:path'
28
+
29
+ /**
30
+ * @typedef {Object} RuntimeCapabilities
31
+ * @property {boolean} is_electron
32
+ * @property {boolean} is_tauri
33
+ * @property {boolean} is_nextjs
34
+ * @property {boolean} is_nuxt
35
+ * @property {boolean} is_web_frontend
36
+ * @property {boolean} is_expo
37
+ * @property {boolean} is_react_native
38
+ * @property {boolean} has_node
39
+ */
40
+
41
+ /** A capability record with every flag defaulted to false. */
42
+ function emptyCapabilities() {
43
+ return {
44
+ is_electron: false,
45
+ is_tauri: false,
46
+ is_nextjs: false,
47
+ is_nuxt: false,
48
+ is_web_frontend: false,
49
+ is_expo: false,
50
+ is_react_native: false,
51
+ has_node: false,
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Detect runtime capabilities from a parsed package.json object.
57
+ *
58
+ * @param {Record<string, unknown> | null} packageJson
59
+ * @returns {RuntimeCapabilities}
60
+ */
61
+ export function detectFromPackageJson(packageJson) {
62
+ const capabilities = emptyCapabilities()
63
+ if (packageJson === null || typeof packageJson !== 'object') return capabilities
64
+
65
+ const allDeps = {
66
+ ...(packageJson.dependencies ?? {}) /** @type {Record<string,string>} */,
67
+ ...(packageJson.devDependencies ?? {}) /** @type {Record<string,string>} */,
68
+ }
69
+ const deps = new Set(Object.keys(allDeps).map((k) => k.toLowerCase()))
70
+
71
+ if (deps.has('electron') || [...deps].some((d) => d.startsWith('@electron'))) {
72
+ capabilities.is_electron = true
73
+ }
74
+ if (deps.has('@tauri-apps/api') || deps.has('tauri')) {
75
+ capabilities.is_tauri = true
76
+ }
77
+ if (deps.has('expo')) capabilities.is_expo = true
78
+ if (deps.has('react-native')) capabilities.is_react_native = true
79
+
80
+ // Web frontends — react/vue/svelte/angular/solid → frontend. BUT: an
81
+ // Electron app commonly bundles React as its renderer; in that case the
82
+ // runtime is "electron", not "web_frontend". Electron + react-native +
83
+ // expo + tauri all win over the web flag.
84
+ const isNativeContainer =
85
+ capabilities.is_electron ||
86
+ capabilities.is_tauri ||
87
+ capabilities.is_react_native ||
88
+ capabilities.is_expo
89
+ const webFrameworks = ['react', 'vue', 'svelte', 'angular', 'solid']
90
+ if (!isNativeContainer && webFrameworks.some((f) => deps.has(f))) {
91
+ capabilities.is_web_frontend = true
92
+ }
93
+
94
+ if (deps.has('next')) {
95
+ capabilities.is_nextjs = true
96
+ capabilities.is_web_frontend = true
97
+ }
98
+ if (deps.has('nuxt')) {
99
+ capabilities.is_nuxt = true
100
+ capabilities.is_web_frontend = true
101
+ }
102
+ if (deps.has('vite') && !isNativeContainer) {
103
+ capabilities.is_web_frontend = true
104
+ }
105
+
106
+ // Node-ish? Anything with a package.json + an entry point counts.
107
+ if (typeof packageJson.main === 'string' || typeof packageJson.module === 'string') {
108
+ capabilities.has_node = true
109
+ }
110
+ if (!capabilities.has_node && packageJson.scripts && typeof packageJson.scripts === 'object') {
111
+ capabilities.has_node = true
112
+ }
113
+
114
+ return capabilities
115
+ }
116
+
117
+ /**
118
+ * Load and detect capabilities from a project directory's package.json.
119
+ * v0.8.6 — also walks `monorepo_globs` (default `['apps/*']`) and
120
+ * OR-merges each match's capabilities with the root.
121
+ *
122
+ * @param {string} projectDir
123
+ * @param {{monorepo_globs?: string[]}} [opts]
124
+ * @returns {RuntimeCapabilities}
125
+ */
126
+ export function loadRuntimeCapabilities(projectDir, opts) {
127
+ const root = resolve(projectDir)
128
+ const rootPkgPath = join(root, 'package.json')
129
+ let rootCaps = emptyCapabilities()
130
+ if (existsSync(rootPkgPath)) {
131
+ try {
132
+ rootCaps = detectFromPackageJson(JSON.parse(readFileSync(rootPkgPath, 'utf-8')))
133
+ } catch {
134
+ /* fall through with empty caps */
135
+ }
136
+ }
137
+
138
+ // Default to the canonical Aperant monorepo layout. Passing `[]`
139
+ // disables the walk entirely (root-only escape hatch per spec ID-05).
140
+ const globs = opts && Array.isArray(opts.monorepo_globs) ? opts.monorepo_globs : ['apps/*']
141
+ if (globs.length === 0) return rootCaps
142
+
143
+ let merged = rootCaps
144
+ for (const glob of globs) {
145
+ const matches = expandSimpleGlob(root, glob)
146
+ for (const dir of matches) {
147
+ const childPkgPath = join(dir, 'package.json')
148
+ if (!existsSync(childPkgPath)) continue
149
+ try {
150
+ const childCaps = detectFromPackageJson(JSON.parse(readFileSync(childPkgPath, 'utf-8')))
151
+ merged = orMergeCapabilities(merged, childCaps)
152
+ } catch {
153
+ /* skip malformed package.json */
154
+ }
155
+ }
156
+ }
157
+ return merged
158
+ }
159
+
160
+ /**
161
+ * Expand a simple `prefix/*` glob against the root. Supports ONLY the
162
+ * `<dir>/*` shape (matches each direct subdirectory of `<dir>`). Other
163
+ * glob syntax is intentionally rejected — v1 keeps the dep surface
164
+ * small. Fails closed on malformed patterns (returns []).
165
+ *
166
+ * @param {string} root
167
+ * @param {string} glob
168
+ * @returns {string[]}
169
+ */
170
+ function expandSimpleGlob(root, glob) {
171
+ if (typeof glob !== 'string' || !glob.endsWith('/*')) return []
172
+ const prefix = glob.slice(0, -2)
173
+ if (prefix.length === 0 || prefix.includes('*')) return []
174
+ const dir = join(root, prefix)
175
+ if (!existsSync(dir)) return []
176
+ let entries
177
+ try {
178
+ entries = readdirSync(dir)
179
+ } catch {
180
+ return []
181
+ }
182
+ const matches = []
183
+ for (const name of entries) {
184
+ const sub = join(dir, name)
185
+ try {
186
+ if (statSync(sub).isDirectory()) matches.push(sub)
187
+ } catch {
188
+ /* skip unreadable entries */
189
+ }
190
+ }
191
+ return matches
192
+ }
193
+
194
+ /**
195
+ * Boolean OR-merge of two capability records. Any flag true in either
196
+ * input is true in the output. The "Electron wins over web" rule from
197
+ * `detectFromPackageJson` is preserved when this is called repeatedly —
198
+ * each child's flags are honest about that child's own shape, and the
199
+ * OR-merge just unions them.
200
+ *
201
+ * @param {RuntimeCapabilities} a
202
+ * @param {RuntimeCapabilities} b
203
+ * @returns {RuntimeCapabilities}
204
+ */
205
+ function orMergeCapabilities(a, b) {
206
+ return {
207
+ is_electron: a.is_electron || b.is_electron,
208
+ is_tauri: a.is_tauri || b.is_tauri,
209
+ is_nextjs: a.is_nextjs || b.is_nextjs,
210
+ is_nuxt: a.is_nuxt || b.is_nuxt,
211
+ is_web_frontend: a.is_web_frontend || b.is_web_frontend,
212
+ is_expo: a.is_expo || b.is_expo,
213
+ is_react_native: a.is_react_native || b.is_react_native,
214
+ has_node: a.has_node || b.has_node,
215
+ }
216
+ }
@@ -12,21 +12,14 @@
12
12
  * lines but no line is ever overwritten.
13
13
  */
14
14
 
15
- import { appendFileSync, mkdirSync } from 'node:fs'
16
- import { dirname, join, resolve } from 'node:path'
15
+ import { appendJsonl } from '../util/atomic-write.mjs'
16
+ import { dailyEventsPath } from '../util/events-path.mjs'
17
17
 
18
- /**
19
- * Compute today's events filename relative to projectDir.
20
- * @param {string} projectDir
21
- * @param {Date} [now]
22
- * @returns {string}
23
- */
24
- export function dailyEventsPath(projectDir, now = new Date()) {
25
- const yyyy = now.getUTCFullYear()
26
- const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
27
- const dd = String(now.getUTCDate()).padStart(2, '0')
28
- return join(resolve(projectDir), '.aperant/events', `${yyyy}-${mm}-${dd}.jsonl`)
29
- }
18
+ // dailyEventsPath now lives in util/events-path.mjs (so install/ can share it
19
+ // without crossing into verify-proof/). Re-exported here byte-for-byte so
20
+ // every existing `import { dailyEventsPath } from '../verify-proof/audit.mjs'`
21
+ // call site keeps working.
22
+ export { dailyEventsPath }
30
23
 
31
24
  /**
32
25
  * Append a driver-invocation audit line.
@@ -45,11 +38,12 @@ export function dailyEventsPath(projectDir, now = new Date()) {
45
38
  */
46
39
  export function appendDriverEvent(projectDir, entry) {
47
40
  const path = dailyEventsPath(projectDir)
48
- mkdirSync(dirname(path), { recursive: true })
49
- const line = JSON.stringify({
41
+ // Single canonical jsonl-append primitive for the event log (appendJsonl
42
+ // creates parent dirs, fsyncs, and locks on Windows). Keeps every writer to
43
+ // .aperant/events/{date}.jsonl on the same atomic path.
44
+ appendJsonl(path, {
50
45
  ts: new Date().toISOString(),
51
46
  kind: 'driver_invocation',
52
47
  ...entry,
53
48
  })
54
- appendFileSync(path, line + '\n', 'utf-8')
55
49
  }
@@ -8,16 +8,54 @@
8
8
  * refactor of every callsite.
9
9
  *
10
10
  * If you add a verb, edit packages/framework/src/driver-sdk/idl.ts.
11
+ *
12
+ * Defense-in-depth (issue #248). `@aperant/framework/driver-sdk` resolves
13
+ * via the package `exports` map to `dist/driver-sdk/index.js`. A *static*
14
+ * `export … from '@aperant/framework/driver-sdk'` here made this barrel —
15
+ * and therefore the whole apt-tools CLI, since `dispatch.mjs` eagerly
16
+ * imports `driver-doctor.mjs` which imports this barrel — crash at
17
+ * module-resolution time with an opaque `ERR_MODULE_NOT_FOUND` when the
18
+ * dist was not built (e.g. a CI step that skipped `pnpm build`), before
19
+ * any command body could run. Resolving the registry via a *dynamic*
20
+ * `await import()` wrapped in try/catch keeps that dependency off the
21
+ * hard module-link path: dist present (tests, post-build CI, published
22
+ * tarball) → the REAL registry values are re-exported unchanged; dist
23
+ * absent → empty/no-op fallbacks let the CLI load (so commands that don't
24
+ * touch the IDL registry, like `route`, still run) instead of crashing.
11
25
  */
12
26
 
13
- export {
14
- ASSERTION_VERBS,
15
- ASYNC_VERBS,
16
- EVIDENCE_VERBS,
17
- IDL_VERB_ARGS,
18
- IDL_VERB_NAMES,
19
- INTERACTION_VERBS,
20
- isVerbResult,
21
- LIFECYCLE_VERBS,
22
- validateVerbArgs,
23
- } from '@aperant/framework/driver-sdk'
27
+ /** @type {typeof import('@aperant/framework/driver-sdk')} */
28
+ let _sdk
29
+ try {
30
+ _sdk = await import('@aperant/framework/driver-sdk')
31
+ } catch (err) {
32
+ // Only the intended "dist not built" condition degrades. A corrupt or
33
+ // partially-written dist (SyntaxError, broken transitive load) raises a
34
+ // different code and MUST surface, not be masked into an empty registry.
35
+ if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err
36
+ // dist/driver-sdk absent — degrade to empty registry so module load
37
+ // does not crash. Any code path that actually needs a verb registry
38
+ // (driver-doctor, the bundled drivers) requires a built dist anyway;
39
+ // commands that don't (e.g. `route`) keep working.
40
+ _sdk = {
41
+ ASSERTION_VERBS: [],
42
+ ASYNC_VERBS: [],
43
+ EVIDENCE_VERBS: [],
44
+ IDL_VERB_ARGS: {},
45
+ IDL_VERB_NAMES: [],
46
+ INTERACTION_VERBS: [],
47
+ isVerbResult: () => false,
48
+ LIFECYCLE_VERBS: [],
49
+ validateVerbArgs: () => ({ ok: true, errors: [] }),
50
+ }
51
+ }
52
+
53
+ export const ASSERTION_VERBS = _sdk.ASSERTION_VERBS
54
+ export const ASYNC_VERBS = _sdk.ASYNC_VERBS
55
+ export const EVIDENCE_VERBS = _sdk.EVIDENCE_VERBS
56
+ export const IDL_VERB_ARGS = _sdk.IDL_VERB_ARGS
57
+ export const IDL_VERB_NAMES = _sdk.IDL_VERB_NAMES
58
+ export const INTERACTION_VERBS = _sdk.INTERACTION_VERBS
59
+ export const isVerbResult = _sdk.isVerbResult
60
+ export const LIFECYCLE_VERBS = _sdk.LIFECYCLE_VERBS
61
+ export const validateVerbArgs = _sdk.validateVerbArgs
@@ -30,13 +30,60 @@
30
30
  * Existing `resolveDriver()` and `scoreDriver()` exports are unchanged.
31
31
  */
32
32
 
33
- import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'
34
-
35
33
  /**
36
34
  * @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
37
35
  * @typedef {import('./runtime-detect.mjs').RuntimeCapabilities} RuntimeCapabilities
38
36
  */
39
37
 
38
+ /**
39
+ * Defense-in-depth (issue #248). The driver-sdk error class lives in
40
+ * `@aperant/framework/driver-sdk`, whose `exports` map resolves to
41
+ * `dist/driver-sdk/index.js`. An eager *static* top-level
42
+ * `import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'`
43
+ * made the WHOLE apt-tools CLI crash at module-resolution time when the
44
+ * dist was not built (e.g. a CI step that skipped `pnpm build`): the
45
+ * static import is resolved at module-link, so an opaque
46
+ * `ERR_MODULE_NOT_FOUND` was raised before any command body — or any
47
+ * `--no-fail` downgrade in run-route-eval.mjs — could run.
48
+ *
49
+ * We instead resolve the class via a *dynamic* `await import()` that is
50
+ * wrapped in try/catch. Dynamic import is still evaluated at module
51
+ * load (top-level await), but its failure is now swallowable rather than
52
+ * a hard link-time crash:
53
+ * - dist present (tests, post-build CI, published tarball) → the REAL
54
+ * class is bound, so any thrown error stays
55
+ * `instanceof UnsatisfiedRequiredCapabilityError` for every consumer
56
+ * (including the resolver suites that import the class from the same
57
+ * `@aperant/framework/driver-sdk` ESM specifier);
58
+ * - dist absent → we fall back to a structurally identical local class
59
+ * (same `.name` / `.capability` / `.attempts` shape, which
60
+ * `detect-runtime.mjs` inspects via `.attempts`) so the CLI degrades
61
+ * to a clear, catchable error instead of crashing at load.
62
+ *
63
+ * `resolveDriver` stays fully synchronous — the class is already bound by
64
+ * the time any importer of this module finishes linking.
65
+ *
66
+ * @type {new (message: string, capability?: string, attempts?: ReadonlyArray<{driver_id: string, reason: string}>) => Error}
67
+ */
68
+ let UnsatisfiedRequiredCapabilityError
69
+ try {
70
+ ;({ UnsatisfiedRequiredCapabilityError } = await import('@aperant/framework/driver-sdk'))
71
+ } catch (err) {
72
+ // Only the intended "dist not built" condition degrades. A corrupt or
73
+ // partially-written dist (SyntaxError, broken transitive load) raises a
74
+ // different code and MUST surface, not be masked into a silent fallback.
75
+ if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err
76
+ // dist/driver-sdk absent — degrade instead of crashing the CLI.
77
+ UnsatisfiedRequiredCapabilityError = class UnsatisfiedRequiredCapabilityError extends Error {
78
+ constructor(message, capability, attempts = []) {
79
+ super(message)
80
+ this.name = 'UnsatisfiedRequiredCapabilityError'
81
+ this.capability = capability
82
+ this.attempts = attempts
83
+ }
84
+ }
85
+ }
86
+
40
87
  const STABILITY_RANK = { ga: 3, beta: 2, experimental: 1 }
41
88
  const LOCALITY_RANK = { bundled: 3, user: 2, registry: 1 }
42
89