@aperant/framework 0.6.6 → 0.6.7

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 (88) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/dist/cli/commands/check-version.d.mts.map +1 -1
  3. package/dist/cli/commands/check-version.mjs +76 -1
  4. package/dist/cli/commands/check-version.mjs.map +1 -1
  5. package/dist/cli/commands/ci-watch.d.mts.map +1 -1
  6. package/dist/cli/commands/ci-watch.mjs +34 -4
  7. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  8. package/dist/cli/commands/init.d.mts +8 -1
  9. package/dist/cli/commands/init.d.mts.map +1 -1
  10. package/dist/cli/commands/init.mjs +91 -9
  11. package/dist/cli/commands/init.mjs.map +1 -1
  12. package/dist/cli/commands/release-notes.d.mts +11 -0
  13. package/dist/cli/commands/release-notes.d.mts.map +1 -0
  14. package/dist/cli/commands/release-notes.mjs +173 -0
  15. package/dist/cli/commands/release-notes.mjs.map +1 -0
  16. package/dist/cli/config/load.d.mts +14 -0
  17. package/dist/cli/config/load.d.mts.map +1 -1
  18. package/dist/cli/config/load.mjs +40 -0
  19. package/dist/cli/config/load.mjs.map +1 -1
  20. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  21. package/dist/cli/config/upgrade-gitignore.mjs +6 -0
  22. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  23. package/dist/cli/consistency/parse-review.d.mts.map +1 -1
  24. package/dist/cli/consistency/parse-review.mjs +4 -1
  25. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  26. package/dist/cli/dispatch.d.mts.map +1 -1
  27. package/dist/cli/dispatch.mjs +2 -0
  28. package/dist/cli/dispatch.mjs.map +1 -1
  29. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  30. package/dist/cli/gate/gates/review-clean.mjs +4 -2
  31. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  32. package/dist/cli/install/update-chips.d.mts +23 -0
  33. package/dist/cli/install/update-chips.d.mts.map +1 -1
  34. package/dist/cli/install/update-chips.mjs +60 -0
  35. package/dist/cli/install/update-chips.mjs.map +1 -1
  36. package/dist/cli/release-notes/compile.d.mts +38 -0
  37. package/dist/cli/release-notes/compile.d.mts.map +1 -0
  38. package/dist/cli/release-notes/compile.mjs +244 -0
  39. package/dist/cli/release-notes/compile.mjs.map +1 -0
  40. package/dist/cli/release-notes/draft.d.mts +73 -0
  41. package/dist/cli/release-notes/draft.d.mts.map +1 -0
  42. package/dist/cli/release-notes/draft.mjs +120 -0
  43. package/dist/cli/release-notes/draft.mjs.map +1 -0
  44. package/dist/cli/release-notes/output-dir.d.mts +20 -0
  45. package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
  46. package/dist/cli/release-notes/output-dir.mjs +42 -0
  47. package/dist/cli/release-notes/output-dir.mjs.map +1 -0
  48. package/dist/cli/release-notes/persona-filter.d.mts +51 -0
  49. package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
  50. package/dist/cli/release-notes/persona-filter.mjs +127 -0
  51. package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
  52. package/dist/cli/release-notes/publish.d.mts +23 -0
  53. package/dist/cli/release-notes/publish.d.mts.map +1 -0
  54. package/dist/cli/release-notes/publish.mjs +125 -0
  55. package/dist/cli/release-notes/publish.mjs.map +1 -0
  56. package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
  57. package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
  58. package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
  59. package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
  60. package/dist/types/config.d.ts +11 -7
  61. package/dist/types/config.d.ts.map +1 -1
  62. package/package.json +133 -133
  63. package/skills/apt-close-task/SKILL.md +25 -0
  64. package/skills/apt-diagram/SKILL.md +45 -9
  65. package/skills/apt-release-notes/SKILL.md +193 -0
  66. package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
  67. package/skills/apt-setup/SKILL.md +146 -3
  68. package/skills/apt-ship/SKILL.md +47 -0
  69. package/skills/apt-update/SKILL.md +51 -9
  70. package/skills/apt-watch-ci/SKILL.md +2 -2
  71. package/src/cli/commands/check-version.mjs +73 -1
  72. package/src/cli/commands/ci-watch.mjs +35 -4
  73. package/src/cli/commands/init.mjs +101 -9
  74. package/src/cli/commands/release-notes.mjs +187 -0
  75. package/src/cli/config/load.mjs +37 -0
  76. package/src/cli/config/upgrade-gitignore.mjs +6 -0
  77. package/src/cli/consistency/parse-review.mjs +3 -1
  78. package/src/cli/dispatch.mjs +2 -0
  79. package/src/cli/gate/gates/review-clean.mjs +4 -2
  80. package/src/cli/install/update-chips.mjs +57 -0
  81. package/src/cli/release-notes/compile.mjs +261 -0
  82. package/src/cli/release-notes/draft.mjs +133 -0
  83. package/src/cli/release-notes/output-dir.mjs +52 -0
  84. package/src/cli/release-notes/persona-filter.mjs +126 -0
  85. package/src/cli/release-notes/publish.mjs +128 -0
  86. package/src/cli/release-notes/ship-autodraft.mjs +106 -0
  87. package/templates/config.json +28 -3
  88. package/workflows/docs.md +12 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * release-notes/publish.mjs — `gh release create/edit` shell.
3
+ *
4
+ * Reads `<output_dir>/<tag>.md` and shells `gh release create <tag>
5
+ * --notes-file <path>` (or `gh release edit` if a release already exists
6
+ * for that tag). When `gh` is unavailable on the host, returns a graceful
7
+ * `{status: 'skipped', reason: 'gh-not-found'}` envelope rather than
8
+ * crashing — AC4.
9
+ *
10
+ * Tests inject a custom shell via the GH_BIN env var (mocked to a no-op
11
+ * shell script) so the publish branch of TD-5 can run without making a
12
+ * real network call.
13
+ */
14
+
15
+ import { execFileSync, spawnSync } from 'node:child_process'
16
+ import { existsSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+
19
+ import { resolveOutputDir, validateTag } from './output-dir.mjs'
20
+
21
+ /**
22
+ * @typedef {Object} PublishInput
23
+ * @property {string} projectDir
24
+ * @property {string} tag
25
+ * @property {string | null} outputDir
26
+ * @property {boolean} dryRun
27
+ */
28
+
29
+ /**
30
+ * @typedef {{
31
+ * status: 'ok',
32
+ * mode: 'created' | 'edited' | 'dry-run',
33
+ * notesPath: string,
34
+ * } | {
35
+ * status: 'skipped',
36
+ * reason: 'gh-not-found' | 'notes-not-found' | 'gh-failed',
37
+ * notesPath?: string,
38
+ * exit_code?: number | null,
39
+ * stderr?: string | null,
40
+ * }} PublishResult
41
+ */
42
+
43
+ /**
44
+ * Resolve which `gh` binary to use. Defaults to `gh` on PATH; tests
45
+ * override via the GH_BIN env var (only honored in NODE_ENV=test).
46
+ *
47
+ * @returns {string}
48
+ */
49
+ function ghBin() {
50
+ if (process.env.NODE_ENV === 'test') {
51
+ const override = process.env.GH_BIN
52
+ if (override && override.length > 0) return override
53
+ }
54
+ return 'gh'
55
+ }
56
+
57
+ /**
58
+ * Detect whether `gh` is available on the host. Uses `which` semantics:
59
+ * `gh --version` exit-0 → available. Any error path → unavailable.
60
+ *
61
+ * @returns {boolean}
62
+ */
63
+ function ghAvailable() {
64
+ try {
65
+ const result = spawnSync(ghBin(), ['--version'], { stdio: ['ignore', 'ignore', 'ignore'] })
66
+ return result.status === 0
67
+ } catch {
68
+ return false
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Check whether a release already exists for the given tag.
74
+ *
75
+ * @param {string} projectDir
76
+ * @param {string} tag
77
+ * @returns {boolean}
78
+ */
79
+ function releaseExists(projectDir, tag) {
80
+ try {
81
+ const result = spawnSync(ghBin(), ['release', 'view', tag, '--json', 'tagName'], {
82
+ cwd: projectDir,
83
+ stdio: ['ignore', 'pipe', 'ignore'],
84
+ })
85
+ return result.status === 0
86
+ } catch {
87
+ return false
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @param {PublishInput} input
93
+ * @returns {PublishResult}
94
+ */
95
+ export function publishTag(input) {
96
+ validateTag(input.tag)
97
+ const outputDir = resolveOutputDir(input.projectDir, input.outputDir)
98
+ const notesPath = join(outputDir, `${input.tag}.md`)
99
+ if (!existsSync(notesPath)) {
100
+ return { status: 'skipped', reason: 'notes-not-found', notesPath }
101
+ }
102
+ if (input.dryRun) {
103
+ return { status: 'ok', mode: 'dry-run', notesPath }
104
+ }
105
+ if (!ghAvailable()) {
106
+ return { status: 'skipped', reason: 'gh-not-found', notesPath }
107
+ }
108
+ const exists = releaseExists(input.projectDir, input.tag)
109
+ const verb = exists ? 'edit' : 'create'
110
+ const args = ['release', verb, input.tag, '--notes-file', notesPath]
111
+ // `gh release edit` only takes --notes-file; `gh release create` takes
112
+ // --notes-file too and a positional tag — same argv order works.
113
+ try {
114
+ execFileSync(ghBin(), args, {
115
+ cwd: input.projectDir,
116
+ stdio: ['ignore', 'pipe', 'pipe'],
117
+ })
118
+ } catch (err) {
119
+ return {
120
+ status: 'skipped',
121
+ reason: 'gh-failed',
122
+ notesPath,
123
+ exit_code: err.status ?? null,
124
+ stderr: err.stderr?.toString() ?? null,
125
+ }
126
+ }
127
+ return { status: 'ok', mode: exists ? 'edited' : 'created', notesPath }
128
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * release-notes/ship-autodraft.mjs — apt-ship Section 2.6 helper.
3
+ *
4
+ * Deterministic, unit-testable shape: given a feature-registry diff +
5
+ * personas envelope + persona_filter mode, decide whether the PR body
6
+ * should grow a `Release note: …` line OR ship should block.
7
+ *
8
+ * This module is intentionally pure — the apt-ship SKILL.md body shells
9
+ * `apt-tools features-audit . --diff-since <base> --json` and feeds the
10
+ * envelope here. Keeping the decision pure makes TD-4 trivial: fixture in,
11
+ * decision out, no I/O.
12
+ */
13
+
14
+ import { buildPersonaTierMap, classifyFragment } from './persona-filter.mjs'
15
+
16
+ /**
17
+ * @typedef {Object} FeatureDiffEntry
18
+ * @property {string} feature_id
19
+ * @property {string} [area]
20
+ * @property {string[]} [personas]
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} AutodraftInput
25
+ * @property {FeatureDiffEntry[]} featureDiff
26
+ * @property {{ personas?: Array<{ ui_json?: unknown }> } | null} personasEnvelope
27
+ * @property {'primary' | 'primary+secondary' | 'all'} personaFilter
28
+ * @property {boolean} requirePrField
29
+ */
30
+
31
+ /**
32
+ * @typedef {{
33
+ * status: 'draft',
34
+ * line: string,
35
+ * persona: string,
36
+ * matchedFeatures: string[],
37
+ * } | {
38
+ * status: 'block',
39
+ * reason: 'no-feature-match' | 'no-persona-signal',
40
+ * message: string,
41
+ * } | {
42
+ * status: 'warn',
43
+ * reason: 'no-feature-match' | 'no-persona-signal',
44
+ * message: string,
45
+ * }} AutodraftResult
46
+ */
47
+
48
+ const BLOCK_MESSAGE =
49
+ '[APT] No release-note signal found — author manual note in PR body or label PR no-notes'
50
+
51
+ /**
52
+ * Compute the autodraft decision.
53
+ *
54
+ * @param {AutodraftInput} input
55
+ * @returns {AutodraftResult}
56
+ */
57
+ export function decideAutodraft(input) {
58
+ const tierMap = buildPersonaTierMap(input.personasEnvelope)
59
+
60
+ if (!Array.isArray(input.featureDiff) || input.featureDiff.length === 0) {
61
+ return failClassify(input, 'no-feature-match')
62
+ }
63
+
64
+ // Walk the diff; the first feature whose persona resolves into the
65
+ // filter mode wins. (Multi-persona dispatch is intentionally out of
66
+ // scope here — host LLM-side narrative can refine per persona; the
67
+ // CLI just picks a clean signal so ship is decisive.)
68
+ for (const feature of input.featureDiff) {
69
+ if (!Array.isArray(feature.personas) || feature.personas.length === 0) continue
70
+ for (const personaTag of feature.personas) {
71
+ const decision = classifyFragment(personaTag, tierMap, input.personaFilter)
72
+ if (decision.bucket === 'body' && decision.resolvedTier) {
73
+ return {
74
+ status: 'draft',
75
+ line: `Release note: ${feature.feature_id} — ships for ${personaTag}.`,
76
+ persona: personaTag,
77
+ matchedFeatures: [feature.feature_id],
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // No persona signal anywhere — features exist but none carry a tag
84
+ // that the roster + filter accept.
85
+ return failClassify(input, 'no-persona-signal')
86
+ }
87
+
88
+ /**
89
+ * Translate a failure reason into the right envelope shape based on
90
+ * `require_pr_field`. require=true → block; require=false → warn (skill
91
+ * proceeds without the line).
92
+ *
93
+ * @param {AutodraftInput} input
94
+ * @param {'no-feature-match' | 'no-persona-signal'} reason
95
+ * @returns {AutodraftResult}
96
+ */
97
+ function failClassify(input, reason) {
98
+ if (input.requirePrField) {
99
+ return { status: 'block', reason, message: BLOCK_MESSAGE }
100
+ }
101
+ return {
102
+ status: 'warn',
103
+ reason,
104
+ message: `[APT] auto-draft skipped (${reason}) — release_notes.require_pr_field is false; proceeding without a Release note line.`,
105
+ }
106
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 4,
2
+ "version": 5,
3
3
  "project_type": "unknown",
4
4
  "share": {
5
5
  "$comment": "Artifact-sharing policy (C35). Source of truth: packages/framework/src/cli/config/share-policy.mjs. visibility = 'solo' | 'team' | 'oss' preset; individual share.* toggles override it. share.digests = null inherits from config.digests.enabled.",
@@ -107,12 +107,37 @@
107
107
  "debate_mode": "auto"
108
108
  },
109
109
  "diagram": {
110
- "mode": "file",
111
- "mcpConsent": null
110
+ "mode": "file"
112
111
  },
113
112
  "docs_policy": {
114
113
  "$comment": "C33 docs-coherence policy. Override via /apt:setup Batch 7. Keys are globs/paths; values are 'auto-update' | 'flag-drift' | 'skip'. Missing block → defaults from packages/framework/templates/docs-policy-defaults.json apply."
115
114
  },
115
+ "changelog": {
116
+ "$comment": "v5 — persona-aware changelog + release-notes system. Two surfaces: dev changelog (CHANGELOG.md from commits, default ON) and user-facing release notes (docs/releases, default OFF). Opt in per project via /apt:setup Batch 9. See packages/framework/skills/apt-release-notes/SKILL.md.",
117
+ "dev": {
118
+ "enabled": true,
119
+ "file": "CHANGELOG.md",
120
+ "format": "keep-a-changelog",
121
+ "source": "commits"
122
+ },
123
+ "release_notes": {
124
+ "enabled": false,
125
+ "audience": null,
126
+ "output_dir": "docs/releases",
127
+ "persona_filter": "primary+secondary",
128
+ "require_pr_field": true,
129
+ "draft": {
130
+ "on_pr_merge": true,
131
+ "fragments_dir": "docs/releases/_unreleased"
132
+ },
133
+ "compile": {
134
+ "on_tag": true
135
+ },
136
+ "publish": {
137
+ "targets": ["github-release"]
138
+ }
139
+ }
140
+ },
116
141
  "router": {
117
142
  "llm": {
118
143
  "enabled": true,
package/workflows/docs.md CHANGED
@@ -90,6 +90,18 @@ Gather facts from the actual codebase. The depth depends on doc type:
90
90
  - Conventional commit messages to categorize changes
91
91
  - Breaking changes (commits with `BREAKING CHANGE` or `!:`)
92
92
 
93
+ > **Two-skill split (v0.7.0+).** `/apt:docs changelog` owns the
94
+ > dev-flavored `CHANGELOG.md` surface — internal task IDs, file paths,
95
+ > schema-bump notes, process trivia. For the user-facing complement,
96
+ > projects can opt in to `/apt:release-notes` via `/apt:setup` →
97
+ > Batch 9. That skill produces persona-voice-tuned release notes
98
+ > (towncrier-style fragments at PR-merge time, compiled per tag, pushed
99
+ > to GitHub Releases). The two skills coexist intentionally: a single
100
+ > `--audience` flag inside `/apt:docs changelog` drifts to a
101
+ > lowest-common-denominator artifact that satisfies neither audience.
102
+ > See `packages/framework/skills/apt-release-notes/SKILL.md` for the
103
+ > opt-in surface and the three modes (draft / compile / publish).
104
+
93
105
  ### For Contributing
94
106
  - Development setup steps (verified by checking file existence)
95
107
  - Test commands (verified by running them)