@aperant/framework 0.13.0-next.0 → 0.15.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 +89 -3
- package/README.md +11 -1
- package/agents/apt-pr-review-i18n.md +190 -0
- package/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/cli/commands/audit-branch-current.d.mts +27 -5
- package/dist/cli/commands/audit-branch-current.d.mts.map +1 -1
- package/dist/cli/commands/audit-branch-current.mjs +3 -3
- package/dist/cli/commands/audit-branch-current.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +6 -4
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/fan-out.d.mts.map +1 -1
- package/dist/cli/commands/fan-out.mjs +3 -1
- package/dist/cli/commands/fan-out.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +6 -1
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/followups.d.mts +5 -0
- package/dist/cli/commands/followups.d.mts.map +1 -0
- package/dist/cli/commands/followups.mjs +266 -0
- package/dist/cli/commands/followups.mjs.map +1 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +3 -1
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/merge-detect.d.mts +29 -0
- package/dist/cli/commands/merge-detect.d.mts.map +1 -0
- package/dist/cli/commands/merge-detect.mjs +196 -0
- package/dist/cli/commands/merge-detect.mjs.map +1 -0
- package/dist/cli/commands/merge-integrate.d.mts +28 -0
- package/dist/cli/commands/merge-integrate.d.mts.map +1 -0
- package/dist/cli/commands/merge-integrate.mjs +383 -0
- package/dist/cli/commands/merge-integrate.mjs.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts +58 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs +980 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -0
- package/dist/cli/commands/pr-review.d.mts.map +1 -1
- package/dist/cli/commands/pr-review.mjs +11 -2
- package/dist/cli/commands/pr-review.mjs.map +1 -1
- package/dist/cli/commands/precompact-hook.d.mts.map +1 -1
- package/dist/cli/commands/precompact-hook.mjs +10 -6
- package/dist/cli/commands/precompact-hook.mjs.map +1 -1
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +24 -11
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/seed.d.mts.map +1 -1
- package/dist/cli/commands/seed.mjs +3 -1
- package/dist/cli/commands/seed.mjs.map +1 -1
- package/dist/cli/commands/state.d.mts.map +1 -1
- package/dist/cli/commands/state.mjs +9 -3
- package/dist/cli/commands/state.mjs.map +1 -1
- package/dist/cli/commands/statusline.d.mts.map +1 -1
- package/dist/cli/commands/statusline.mjs +5 -1
- package/dist/cli/commands/statusline.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +126 -23
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/worktree.d.mts +1 -1
- package/dist/cli/commands/worktree.d.mts.map +1 -1
- package/dist/cli/commands/worktree.mjs +282 -83
- package/dist/cli/commands/worktree.mjs.map +1 -1
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +3 -2
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/config/share-policy.d.mts +3 -1
- package/dist/cli/config/share-policy.d.mts.map +1 -1
- package/dist/cli/config/share-policy.mjs +16 -3
- package/dist/cli/config/share-policy.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +27 -5
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-qa.mjs +12 -1
- package/dist/cli/consistency/parse-qa.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts +23 -0
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +36 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/consistency/parse-spec.d.mts +16 -0
- package/dist/cli/consistency/parse-spec.d.mts.map +1 -1
- package/dist/cli/consistency/parse-spec.mjs +44 -0
- package/dist/cli/consistency/parse-spec.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +5 -2
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +6 -4
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +4 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/followups/derive.d.mts +63 -0
- package/dist/cli/followups/derive.d.mts.map +1 -0
- package/dist/cli/followups/derive.mjs +115 -0
- package/dist/cli/followups/derive.mjs.map +1 -0
- package/dist/cli/followups/schema.d.mts +165 -0
- package/dist/cli/followups/schema.d.mts.map +1 -0
- package/dist/cli/followups/schema.mjs +251 -0
- package/dist/cli/followups/schema.mjs.map +1 -0
- package/dist/cli/gate/context.d.mts +1 -1
- package/dist/cli/gate/gates/followups-adjudicated.d.mts +69 -0
- package/dist/cli/gate/gates/followups-adjudicated.d.mts.map +1 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs +141 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs.map +1 -0
- package/dist/cli/gate/gates/index.mjs +1 -0
- package/dist/cli/gate/gates/index.mjs.map +1 -1
- package/dist/cli/git/materialize-env.d.mts +39 -0
- package/dist/cli/git/materialize-env.d.mts.map +1 -0
- package/dist/cli/git/materialize-env.mjs +217 -0
- package/dist/cli/git/materialize-env.mjs.map +1 -0
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +9 -1
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +4 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
- package/dist/cli/roadmap/lifecycle.mjs +4 -1
- package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +9 -0
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +9 -0
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/cli/task/index-md.d.mts +5 -0
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +12 -4
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/cli/util/io.d.mts +6 -0
- package/dist/cli/util/io.d.mts.map +1 -1
- package/dist/cli/util/io.mjs +16 -1
- package/dist/cli/util/io.mjs.map +1 -1
- package/dist/cli/util/project.d.mts +7 -2
- package/dist/cli/util/project.d.mts.map +1 -1
- package/dist/cli/util/project.mjs +11 -14
- package/dist/cli/util/project.mjs.map +1 -1
- package/dist/cli/util/state-paths.d.mts +19 -0
- package/dist/cli/util/state-paths.d.mts.map +1 -0
- package/dist/cli/util/state-paths.mjs +45 -0
- package/dist/cli/util/state-paths.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +4 -1
- package/dist/plugin/agents/apt-pr-review-i18n.md +190 -0
- package/dist/plugin/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/plugin/skills/apt/SKILL.md +13 -6
- package/dist/plugin/skills/apt-close-task/SKILL.md +48 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +19 -9
- package/dist/plugin/skills/apt-execute/SKILL.md +3 -1
- package/dist/plugin/skills/apt-fan-out/SKILL.md +65 -1
- package/dist/plugin/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/dist/plugin/skills/apt-pr-review/SCHEMA.md +4 -2
- package/dist/plugin/skills/apt-pr-review/SKILL.md +117 -29
- package/dist/plugin/skills/apt-quick/SKILL.md +24 -1
- package/dist/plugin/skills/apt-review/SKILL.md +6 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -1
- package/dist/plugin/skills/apt-setup/SKILL.md +21 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +32 -11
- package/dist/plugin/skills/apt-terminal/SKILL.md +8 -0
- package/dist/plugin/skills/apt-triage/SKILL.md +4 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +24 -0
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +3 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +138 -138
- package/prompts/conductor-framework-context.md +1 -1
- package/prompts/github/pr_i18n_agent.md +279 -0
- package/prompts/github/pr_xplatform_agent.md +283 -0
- package/prompts/inbox-clarification.md +18 -11
- package/skills/apt/SKILL.md +9 -3
- package/skills/apt-close-task/SKILL.md +48 -0
- package/skills/apt-debug/SKILL.md +19 -9
- package/skills/apt-execute/SKILL.md +3 -1
- package/skills/apt-fan-out/SKILL.md +65 -1
- package/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/skills/apt-pr-review/SCHEMA.md +4 -2
- package/skills/apt-pr-review/SKILL.md +117 -29
- package/skills/apt-quick/SKILL.md +24 -1
- package/skills/apt-review/SKILL.md +6 -0
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +21 -3
- package/skills/apt-ship/SKILL.md +32 -11
- package/skills/apt-terminal/SKILL.md +8 -0
- package/skills/apt-triage/SKILL.md +4 -0
- package/skills/apt-verify/SKILL.md +24 -0
- package/skills/apt-watch-ci/SKILL.md +3 -1
- package/src/cli/commands/audit-branch-current.mjs +3 -3
- package/src/cli/commands/event.mjs +6 -4
- package/src/cli/commands/fan-out.mjs +3 -1
- package/src/cli/commands/features-audit.mjs +6 -1
- package/src/cli/commands/followups.mjs +303 -0
- package/src/cli/commands/health-check.mjs +3 -1
- package/src/cli/commands/merge-detect.mjs +203 -0
- package/src/cli/commands/merge-integrate.mjs +392 -0
- package/src/cli/commands/pr-review/__fixtures__/android-strings/app/src/main/res/values-fr/strings.xml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/babel-js/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/c-ifdef/src/main.c +4 -0
- package/src/cli/commands/pr-review/__fixtures__/ci-matrix/.github/workflows/ci.yml +10 -0
- package/src/cli/commands/pr-review/__fixtures__/cmake-win/CMakeLists.txt +5 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/add-arb.patch +9 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-dep.patch +13 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-list-item-dep.patch +17 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/lone-weak-t.patch +8 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/monorepo-b.patch +7 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/two-weak-xpl.patch +12 -0
- package/src/cli/commands/pr-review/__fixtures__/dropped-underscore/src/app.js +2 -0
- package/src/cli/commands/pr-review/__fixtures__/electron-app/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/i18n-locale-file/src/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/a/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/b/src/index.ts +1 -0
- package/src/cli/commands/pr-review/__fixtures__/py-babel/pyproject.toml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/qt-i18n-dir/i18n/app_fr.ts +11 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/Cargo.toml +7 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/src/main.rs +1 -0
- package/src/cli/commands/pr-review/__fixtures__/ts-dir/ts/foo.ts +3 -0
- package/src/cli/commands/pr-review/detect-specialists.mjs +1023 -0
- package/src/cli/commands/pr-review.mjs +12 -2
- package/src/cli/commands/precompact-hook.mjs +10 -6
- package/src/cli/commands/roadmap.mjs +24 -11
- package/src/cli/commands/seed.mjs +3 -1
- package/src/cli/commands/state.mjs +9 -3
- package/src/cli/commands/statusline.mjs +5 -1
- package/src/cli/commands/task.mjs +135 -23
- package/src/cli/commands/worktree.mjs +298 -81
- package/src/cli/config/post-merge-sweep.mjs +3 -2
- package/src/cli/config/share-policy.mjs +16 -3
- package/src/cli/config/upgrade-gitignore.mjs +27 -5
- package/src/cli/consistency/parse-qa.mjs +11 -1
- package/src/cli/consistency/parse-review.mjs +36 -0
- package/src/cli/consistency/parse-spec.mjs +43 -0
- package/src/cli/coordination/agent-identity.mjs +6 -4
- package/src/cli/dispatch.mjs +4 -0
- package/src/cli/followups/derive.mjs +123 -0
- package/src/cli/followups/schema.mjs +264 -0
- package/src/cli/gate/gates/followups-adjudicated.mjs +158 -0
- package/src/cli/gate/gates/index.mjs +1 -0
- package/src/cli/git/materialize-env.mjs +212 -0
- package/src/cli/help.mjs +9 -1
- package/src/cli/install/legacy-paths.mjs +4 -0
- package/src/cli/roadmap/lifecycle.mjs +4 -1
- package/src/cli/task/ids.mjs +9 -0
- package/src/cli/task/index-md.mjs +12 -4
- package/src/cli/util/io.mjs +15 -1
- package/src/cli/util/project.mjs +11 -14
- package/src/cli/util/state-paths.mjs +45 -0
- package/prompts/followup_planner.md +0 -399
|
@@ -71,13 +71,15 @@ export function buildAperantGitignoreTemplate(policy = DEFAULT_SHARE_POLICY) {
|
|
|
71
71
|
'# framework scaffolding that ships with the repo. Runtime workflow run',
|
|
72
72
|
'# files (workflows/*.json + workflows/archive/) are ephemeral.',
|
|
73
73
|
'# shared-source (committed iff matching share.* toggle is on):',
|
|
74
|
-
'# team.json, roadmap/**, decisions/*.md, digests/*.md, tasks
|
|
75
|
-
'#
|
|
74
|
+
'# team.json, roadmap/**, decisions/*.md, digests/*.md, tasks/*/**,',
|
|
75
|
+
'# quick/*/** (quick shares the tasks toggle) — the allow rules below',
|
|
76
|
+
'# are emitted only when the policy opts in.',
|
|
76
77
|
'# ephemeral/coordination (always ignored): state.json, active.json (legacy, removed in C34 — rule kept as defense in depth), events/,',
|
|
77
78
|
'# locks/, team-status/, paused/, pending-effects.jsonl — per-machine',
|
|
78
79
|
'# lifecycle data; committing leaks PIDs + hostnames.',
|
|
79
|
-
'# derived/regenerated (always ignored): tasks/INDEX.md,
|
|
80
|
-
'# decisions/INDEX.md — humans read the Markdown,
|
|
80
|
+
'# derived/regenerated (always ignored): tasks/INDEX.md, quick/INDEX.md,',
|
|
81
|
+
'# roadmap/**/ROADMAP.md, decisions/INDEX.md — humans read the Markdown,',
|
|
82
|
+
'# machines write the JSON.',
|
|
81
83
|
'',
|
|
82
84
|
'# Ignore everything by default',
|
|
83
85
|
'*',
|
|
@@ -139,6 +141,15 @@ export function buildAperantGitignoreTemplate(policy = DEFAULT_SHARE_POLICY) {
|
|
|
139
141
|
lines.push('!tasks/*/ # class: shared-source')
|
|
140
142
|
lines.push('!tasks/*/** # class: shared-source')
|
|
141
143
|
lines.push('')
|
|
144
|
+
// QUICK-task records share the tasks toggle (quick is a task track).
|
|
145
|
+
// Same shape as tasks/: per-record dirs allowed, derived INDEX denied.
|
|
146
|
+
lines.push('# Quick-task records (the per-task directory content, not the INDEX)')
|
|
147
|
+
lines.push('!quick/')
|
|
148
|
+
lines.push('quick/*')
|
|
149
|
+
lines.push('# Re-allow per-quick-task directories; INDEX.md is derived and stays ignored.')
|
|
150
|
+
lines.push('!quick/*/ # class: shared-source')
|
|
151
|
+
lines.push('!quick/*/** # class: shared-source')
|
|
152
|
+
lines.push('')
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
// ── Shared-source: roadmap (share.roadmap) ──────────────────────────────
|
|
@@ -193,6 +204,10 @@ export function buildAperantGitignoreTemplate(policy = DEFAULT_SHARE_POLICY) {
|
|
|
193
204
|
lines.push('')
|
|
194
205
|
lines.push('# ── Derived markdown indexes (regenerated from JSON) ──────────────')
|
|
195
206
|
lines.push('tasks/INDEX.md')
|
|
207
|
+
// quick/INDEX.md is the QUICK-task analogue of tasks/INDEX.md — a derived
|
|
208
|
+
// markdown index regenerated from the per-task records. Always denied,
|
|
209
|
+
// regardless of share.tasks, so it never lingers as a tracked dirty file.
|
|
210
|
+
lines.push('quick/INDEX.md')
|
|
196
211
|
lines.push('')
|
|
197
212
|
|
|
198
213
|
return lines.join('\n')
|
|
@@ -298,7 +313,14 @@ function loadPolicyFromConfig(aperantDir) {
|
|
|
298
313
|
}
|
|
299
314
|
|
|
300
315
|
function findTrackedFilesThatWillBeIgnored(projectRoot) {
|
|
301
|
-
const candidates = [
|
|
316
|
+
const candidates = [
|
|
317
|
+
'.aperant/state.json',
|
|
318
|
+
'.aperant/active.json',
|
|
319
|
+
'.aperant/tasks/INDEX.md',
|
|
320
|
+
// Derived QUICK index — always ignored now; surface the historical
|
|
321
|
+
// orphan (committed before quick/ entered the taxonomy) for untracking.
|
|
322
|
+
'.aperant/quick/INDEX.md',
|
|
323
|
+
]
|
|
302
324
|
// roadmap/**/ROADMAP.md — enumerate via git ls-files below.
|
|
303
325
|
const tracked = new Set()
|
|
304
326
|
try {
|
|
@@ -48,7 +48,7 @@ export function parseQaSignoff(qa) {
|
|
|
48
48
|
findings.push({
|
|
49
49
|
severity: sev,
|
|
50
50
|
title: typeof f.title === 'string' ? f.title : typeof f.message === 'string' ? f.message : '',
|
|
51
|
-
status:
|
|
51
|
+
status: normalizeFindingStatus(f),
|
|
52
52
|
file: typeof f.file === 'string' ? f.file : typeof f.path === 'string' ? f.path : '',
|
|
53
53
|
})
|
|
54
54
|
}
|
|
@@ -100,3 +100,13 @@ function normalizeSeverity(sev) {
|
|
|
100
100
|
const key = sev.toLowerCase().trim()
|
|
101
101
|
return SEVERITY_MAP[key] ?? null
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
// Mirror parse-review's `normalizeStatus` shorthand: a finding marked with the
|
|
105
|
+
// `fixed: true` / `resolved: true` boolean (no string `status`) is `fixed`.
|
|
106
|
+
// Without this, derive.mjs would treat such a finding as a still-open candidate
|
|
107
|
+
// and the ledger would have to cover a phantom expected row.
|
|
108
|
+
function normalizeFindingStatus(f) {
|
|
109
|
+
if (f.fixed === true) return 'fixed'
|
|
110
|
+
if (f.resolved === true) return 'fixed'
|
|
111
|
+
return typeof f.status === 'string' ? f.status.toLowerCase() : 'open'
|
|
112
|
+
}
|
|
@@ -61,6 +61,42 @@ export function parseReview(review) {
|
|
|
61
61
|
return { findings }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Like {@link parseReview} but RETAINS the sub-critical findings that
|
|
66
|
+
* `DROPPED_SEVERITIES` discards (suggestion/nitpick/note/info/minor). The
|
|
67
|
+
* severity is the raw lowercased string (no SEVERITY_MAP collapse), so a
|
|
68
|
+
* `warning` stays `warning` and a `minor` stays `minor`. Status uses the
|
|
69
|
+
* identical `normalizeStatus` rules as parseReview (`fixed:true`,
|
|
70
|
+
* `resolved:true`, `deferred-*` collapse). The id-synthesis (`rf-<i>`) and
|
|
71
|
+
* `{id, severity, status, title, file}` shape match parseReview.
|
|
72
|
+
*
|
|
73
|
+
* Additive export — parseReview's output is byte-stable.
|
|
74
|
+
*
|
|
75
|
+
* @param {object | null | undefined} review
|
|
76
|
+
* @returns {{findings: Array<{id: string, severity: string, status: string, title: string, file: string}>}}
|
|
77
|
+
*/
|
|
78
|
+
export function parseReviewRaw(review) {
|
|
79
|
+
if (!review || typeof review !== 'object') {
|
|
80
|
+
return { findings: [] }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const raw = collectRaw(review)
|
|
84
|
+
const findings = []
|
|
85
|
+
for (let i = 0; i < raw.length; i++) {
|
|
86
|
+
const f = raw[i]
|
|
87
|
+
if (!f || typeof f !== 'object') continue
|
|
88
|
+
if (typeof f.severity !== 'string') continue
|
|
89
|
+
findings.push({
|
|
90
|
+
id: typeof f.id === 'string' ? f.id : typeof f.id === 'number' ? String(f.id) : `rf-${i + 1}`,
|
|
91
|
+
severity: f.severity.toLowerCase().trim(),
|
|
92
|
+
status: normalizeStatus(f),
|
|
93
|
+
title: typeof f.title === 'string' ? f.title : typeof f.message === 'string' ? f.message : '',
|
|
94
|
+
file: typeof f.file === 'string' ? f.file : typeof f.path === 'string' ? f.path : '',
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
return { findings }
|
|
98
|
+
}
|
|
99
|
+
|
|
64
100
|
function collectRaw(review) {
|
|
65
101
|
const out = []
|
|
66
102
|
const findings = review.findings
|
|
@@ -43,6 +43,49 @@ export function parseSpec(md) {
|
|
|
43
43
|
return { acs, dimensions_claimed }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Extract `## Out of Scope` bullets from a spec.md string.
|
|
48
|
+
*
|
|
49
|
+
* Heading match is case-insensitive at any level >= 2. Bullets may be
|
|
50
|
+
* `-`/`*`/numbered; the section body ends at the next heading of
|
|
51
|
+
* equal-or-shallower depth. Returns `[]` on absent section or non-string
|
|
52
|
+
* input. Does NOT touch the filesystem (additive sibling to parseSpec — the
|
|
53
|
+
* existing parseSpec return shape is unchanged).
|
|
54
|
+
*
|
|
55
|
+
* @param {string | null | undefined} md
|
|
56
|
+
* @returns {Array<{text: string, line: number}>}
|
|
57
|
+
*/
|
|
58
|
+
export function parseOutOfScope(md) {
|
|
59
|
+
if (typeof md !== 'string' || md.length === 0) return []
|
|
60
|
+
|
|
61
|
+
const lines = md.split(/\r?\n/)
|
|
62
|
+
let headingIdx = -1
|
|
63
|
+
let headingLevel = 0
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const m = lines[i].match(/^(#{2,})\s*out\s+of\s+scope\s*$/i)
|
|
66
|
+
if (m) {
|
|
67
|
+
headingIdx = i
|
|
68
|
+
headingLevel = m[1].length
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (headingIdx === -1) return []
|
|
73
|
+
|
|
74
|
+
const out = []
|
|
75
|
+
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i]
|
|
77
|
+
const h = line.match(/^(#{1,6})\s+/)
|
|
78
|
+
if (h && h[1].length <= headingLevel) break
|
|
79
|
+
|
|
80
|
+
const bulletMatch = line.match(/^\s*(?:[-*]|\d+\.)\s+(.*)$/)
|
|
81
|
+
if (!bulletMatch) continue
|
|
82
|
+
const text = bulletMatch[1].trim()
|
|
83
|
+
if (text.length === 0) continue
|
|
84
|
+
out.push({ text, line: i + 1 })
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
46
89
|
function collectAcs(lines) {
|
|
47
90
|
// Find the first "## Acceptance Criteria" heading (level >= 2).
|
|
48
91
|
let headingIdx = -1
|
|
@@ -18,20 +18,22 @@
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { hostname as osHostname } from 'node:os'
|
|
20
20
|
import { join } from 'node:path'
|
|
21
|
-
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
22
21
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
23
22
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
23
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
24
24
|
|
|
25
25
|
// Cached identity per project root (rooted at the shared .aperant/ dir).
|
|
26
26
|
const cache = new Map()
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the shared coordination root — always the main repo's root, even
|
|
30
|
-
* when called from a worktree.
|
|
30
|
+
* when called from a worktree. Body re-pointed at the shared resolveMainRepoRoot
|
|
31
|
+
* mechanism (state-fork-fix ID-07) to inherit the null-guard; the name and the
|
|
32
|
+
* @internal CoordinationStore seam ownership stay as-is (it is NOT merged with
|
|
33
|
+
* resolveStateDir — it is a SEPARATE policy wrapper over the same mechanism).
|
|
31
34
|
*/
|
|
32
35
|
export function resolveSharedRoot(projectDir) {
|
|
33
|
-
|
|
34
|
-
return wt.is_worktree && wt.main_repo_root ? wt.main_repo_root : projectDir
|
|
36
|
+
return resolveMainRepoRoot(projectDir)
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -63,6 +63,7 @@ import { cmdEffects } from './commands/effects.mjs'
|
|
|
63
63
|
import { cmdEvent } from './commands/event.mjs'
|
|
64
64
|
import { cmdFanOut } from './commands/fan-out.mjs'
|
|
65
65
|
import { cmdFeaturesAudit } from './commands/features-audit.mjs'
|
|
66
|
+
import { cmdFollowups } from './commands/followups.mjs'
|
|
66
67
|
import { cmdGate } from './commands/gate.mjs'
|
|
67
68
|
import { cmdHealthCheck } from './commands/health-check.mjs'
|
|
68
69
|
import { cmdHostDetect, cmdReconcile } from './commands/host-detect.mjs'
|
|
@@ -70,6 +71,7 @@ import { cmdInit } from './commands/init.mjs'
|
|
|
70
71
|
import { cmdInitState } from './commands/init-state.mjs'
|
|
71
72
|
import { cmdInstallFromSource } from './commands/install-from-source.mjs'
|
|
72
73
|
import { cmdLock } from './commands/lock.mjs'
|
|
74
|
+
import { cmdMerge } from './commands/merge-detect.mjs'
|
|
73
75
|
import { cmdMigratePrReviews } from './commands/migrate-pr-reviews.mjs'
|
|
74
76
|
import { cmdMobilePrepare } from './commands/mobile-prepare.mjs'
|
|
75
77
|
import { cmdPersonas } from './commands/personas.mjs'
|
|
@@ -317,6 +319,7 @@ export const DISPATCH = {
|
|
|
317
319
|
'variant-pick': (args) => wrap(cmdVariantPick)(args[1], args[2], args.slice(3)),
|
|
318
320
|
worktree: (args) => wrap(cmdWorktree)(args[1], args[2], args.slice(3)),
|
|
319
321
|
audit: (args) => wrap(cmdAudit)(args[1], args[2], args.slice(3)),
|
|
322
|
+
merge: (args) => wrap(cmdMerge)(args[1], args[2], args.slice(3)),
|
|
320
323
|
'release-notes': (args) => wrap(cmdReleaseNotes)(args[1], args[2], args.slice(3)),
|
|
321
324
|
team: (args) => wrap(cmdTeam)(args[1], args[2], args.slice(3)),
|
|
322
325
|
active: (args) => wrap(cmdActive)(args[1], args[2], args.slice(3)),
|
|
@@ -336,6 +339,7 @@ export const DISPATCH = {
|
|
|
336
339
|
context: (args) => wrap(cmdContext)(args[1], args[2], args.slice(3)),
|
|
337
340
|
adr: (args) => wrap(cmdAdr)(args[1], args[2], args.slice(3)),
|
|
338
341
|
triage: (args) => wrap(cmdTriage)(args[1], args[2], args.slice(3)),
|
|
342
|
+
followups: (args) => wrap(cmdFollowups)(args[1], args[2], args.slice(3)),
|
|
339
343
|
modes: async (args) => {
|
|
340
344
|
const { cmdModes } = await import('./commands/modes.mjs')
|
|
341
345
|
return wrap(cmdModes)(args[1], args[2], args.slice(3))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* followups/derive.mjs — deterministic expected-candidate derivation (G45, ID-04).
|
|
4
|
+
*
|
|
5
|
+
* Pure: takes already-parsed inputs (no fs), returns the expected row
|
|
6
|
+
* universe + outcome-ref validation. The expected universe is what makes the
|
|
7
|
+
* ledger DERIVED, not voluntary — a writer cannot self-certify completeness.
|
|
8
|
+
*
|
|
9
|
+
* Expected universe =
|
|
10
|
+
* parseOutOfScope(spec.md) bullets → origin 'plan', id `oos-<n>`
|
|
11
|
+
* ∪ parseReviewRaw(review.json) rows status !== 'fixed' → origin 'review', id `rev-<finding.id>`
|
|
12
|
+
* ∪ parseQaSignoff(qa_signoff.json) findings status!=='fixed' → origin 'verify', id `qa-<n>`
|
|
13
|
+
*
|
|
14
|
+
* Each candidate carries a full payload `{id, title, origin, class_hint}` so
|
|
15
|
+
* the adjudicating agent appends ledger rows mechanically. `class_hint` is a
|
|
16
|
+
* non-authoritative suggestion (ID-03): plan/verify default to 'new-task',
|
|
17
|
+
* review findings default to 'in-pr'.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { parseQaSignoff } from '../consistency/parse-qa.mjs'
|
|
21
|
+
import { parseReviewRaw } from '../consistency/parse-review.mjs'
|
|
22
|
+
import { parseOutOfScope } from '../consistency/parse-spec.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{specContent?: string, review?: object, qa?: object}} inputs
|
|
26
|
+
* @returns {Array<{id: string, title: string, origin: string, class_hint: string}>}
|
|
27
|
+
*/
|
|
28
|
+
export function deriveExpectedCandidates({ specContent, review, qa } = {}) {
|
|
29
|
+
/** @type {Array<{id: string, title: string, origin: string, class_hint: string}>} */
|
|
30
|
+
const candidates = []
|
|
31
|
+
|
|
32
|
+
// (1) Out of Scope bullets — origin plan, 1-based ordinal id.
|
|
33
|
+
const oos = parseOutOfScope(specContent)
|
|
34
|
+
for (let i = 0; i < oos.length; i++) {
|
|
35
|
+
candidates.push({
|
|
36
|
+
id: `oos-${i + 1}`,
|
|
37
|
+
title: oos[i].text,
|
|
38
|
+
origin: 'plan',
|
|
39
|
+
class_hint: 'new-task',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// (2) Review raw rows that are not fixed — origin review, id rev-<finding.id>.
|
|
44
|
+
const rawReview = parseReviewRaw(review)
|
|
45
|
+
for (const f of rawReview.findings) {
|
|
46
|
+
if (f.status === 'fixed') continue
|
|
47
|
+
candidates.push({
|
|
48
|
+
id: `rev-${f.id}`,
|
|
49
|
+
title: f.title || f.id,
|
|
50
|
+
origin: 'review',
|
|
51
|
+
class_hint: 'in-pr',
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// (3) QA sign-off findings that are not fixed — origin verify, 1-based
|
|
56
|
+
// ordinal by ABSOLUTE position in the findings list. The id MUST be stable
|
|
57
|
+
// as findings get fixed over the task's life: a counter over the filtered
|
|
58
|
+
// (non-fixed) subset would renumber qa-2 → qa-1 once an earlier finding
|
|
59
|
+
// flips to fixed, so an already-adjudicated ledger row (id qa-2) would
|
|
60
|
+
// resurface as a "missing expected" candidate — the exact evaporation this
|
|
61
|
+
// gate exists to prevent. Position-based ids match the oos path (absolute
|
|
62
|
+
// index) and the rev path (stable finding id).
|
|
63
|
+
const qaParsed = parseQaSignoff(qa)
|
|
64
|
+
for (let i = 0; i < qaParsed.findings.length; i++) {
|
|
65
|
+
const f = qaParsed.findings[i]
|
|
66
|
+
if (f.status === 'fixed') continue
|
|
67
|
+
candidates.push({
|
|
68
|
+
id: `qa-${i + 1}`,
|
|
69
|
+
title: f.title || `qa finding ${i + 1}`,
|
|
70
|
+
origin: 'verify',
|
|
71
|
+
class_hint: 'new-task',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return candidates
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate every `outcome_refs[]` entry + `adjudication.outcome_ref` on the
|
|
80
|
+
* ledger rows against the set of `O<n>` ids from parseUserOutcomes. Unknown
|
|
81
|
+
* refs are returned for `invalid_outcome_refs[]` (never a new id namespace,
|
|
82
|
+
* ID-01). A null/omitted `adjudication.outcome_ref` is valid (refactor-only
|
|
83
|
+
* specs have no outcomes).
|
|
84
|
+
*
|
|
85
|
+
* @param {Array<{id: string, outcome_refs?: string[], adjudication?: {outcome_ref?: string|null}}>} rows — validated ledger rows (only id / outcome_refs / adjudication.outcome_ref are read)
|
|
86
|
+
* @param {{outcomes?: Array<{id: string}>}} parsedOutcomes — parseUserOutcomes(spec.md)
|
|
87
|
+
* @returns {Array<{row_id: string, ref: string, field: string}>}
|
|
88
|
+
*/
|
|
89
|
+
export function validateOutcomeRefs(rows, parsedOutcomes) {
|
|
90
|
+
const valid = new Set(
|
|
91
|
+
(parsedOutcomes && Array.isArray(parsedOutcomes.outcomes) ? parsedOutcomes.outcomes : []).map(
|
|
92
|
+
(o) => o.id,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
/** @type {Array<{row_id: string, ref: string, field: string}>} */
|
|
96
|
+
const invalid = []
|
|
97
|
+
const list = Array.isArray(rows) ? rows : []
|
|
98
|
+
for (const r of list) {
|
|
99
|
+
const refs = Array.isArray(r.outcome_refs) ? r.outcome_refs : []
|
|
100
|
+
for (const ref of refs) {
|
|
101
|
+
if (!valid.has(ref)) invalid.push({ row_id: r.id, ref, field: 'outcome_refs' })
|
|
102
|
+
}
|
|
103
|
+
const adjRef = r.adjudication?.outcome_ref
|
|
104
|
+
if (typeof adjRef === 'string' && adjRef.length > 0 && !valid.has(adjRef)) {
|
|
105
|
+
invalid.push({ row_id: r.id, ref: adjRef, field: 'adjudication.outcome_ref' })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return invalid
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compute the set of expected candidate ids missing from the ledger rows.
|
|
113
|
+
* Returns the full candidate payloads for each missing id (so the agent can
|
|
114
|
+
* append them mechanically — ID-04).
|
|
115
|
+
*
|
|
116
|
+
* @param {Array<{id: string, title: string, origin: string, class_hint: string}>} candidates
|
|
117
|
+
* @param {Array<{id: string}>} rows — only the `id` is read
|
|
118
|
+
* @returns {Array<{id: string, title: string, origin: string, class_hint: string}>}
|
|
119
|
+
*/
|
|
120
|
+
export function computeMissingExpected(candidates, rows) {
|
|
121
|
+
const present = new Set((Array.isArray(rows) ? rows : []).map((r) => r.id))
|
|
122
|
+
return (Array.isArray(candidates) ? candidates : []).filter((c) => !present.has(c.id))
|
|
123
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* followups/schema.mjs — validate the per-task followups.json ledger (G45).
|
|
4
|
+
*
|
|
5
|
+
* Pure: takes a parsed JSON document, returns a normalized shape with
|
|
6
|
+
* structured `schema_errors[]`. Never throws, never touches the filesystem
|
|
7
|
+
* (mirrors the consistency-parser style).
|
|
8
|
+
*
|
|
9
|
+
* Row shape:
|
|
10
|
+
* {
|
|
11
|
+
* id: string, // free id (e.g. oos-1, rev-rf-3, qa-2, or a voluntary id)
|
|
12
|
+
* title: string,
|
|
13
|
+
* class_proposed: 'in-pr' | 'new-task' | 'polish', // a PROPOSAL — adjudication is authority (ID-03)
|
|
14
|
+
* origin: 'plan' | 'discuss' | 'execute' | 'verify' | 'review' | 'ci',
|
|
15
|
+
* files: string[],
|
|
16
|
+
* outcome_refs: string[], // 'O<n>' ids validated elsewhere (derive.validateOutcomeRefs)
|
|
17
|
+
* status: 'open' | 'fixed' | 'dispositioned',
|
|
18
|
+
* adjudication?: {
|
|
19
|
+
* verdict: 'load-bearing' | 'deferrable',
|
|
20
|
+
* by: 'verify' | 'verify-proof',
|
|
21
|
+
* outcome_ref?: string | null // nullable — refactor-only specs have no outcomes (ID-01)
|
|
22
|
+
* },
|
|
23
|
+
* disposition?: {
|
|
24
|
+
* kind: 'filed_task' | 'executed_commit' | 'parked_note' | 'dropped_by_user',
|
|
25
|
+
* ref: string,
|
|
26
|
+
* reason?: string // required for dropped_by_user (enforced at the CLI seam)
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* Output:
|
|
31
|
+
* { rows: ValidRow[], schema_errors: [{row_index, id?, field, message}] }
|
|
32
|
+
*
|
|
33
|
+
* A row that fails validation is reported in schema_errors and EXCLUDED from
|
|
34
|
+
* rows[] (so downstream gate logic never reasons over a malformed row).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export const CLASS_PROPOSED = new Set(['in-pr', 'new-task', 'polish'])
|
|
38
|
+
export const ORIGINS = new Set(['plan', 'discuss', 'execute', 'verify', 'review', 'ci'])
|
|
39
|
+
export const STATUSES = new Set(['open', 'fixed', 'dispositioned'])
|
|
40
|
+
export const VERDICTS = new Set(['load-bearing', 'deferrable'])
|
|
41
|
+
export const ADJUDICATORS = new Set(['verify', 'verify-proof'])
|
|
42
|
+
export const DISPOSITION_KINDS = new Set([
|
|
43
|
+
'filed_task',
|
|
44
|
+
'executed_commit',
|
|
45
|
+
'parked_note',
|
|
46
|
+
'dropped_by_user',
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} FollowupAdjudication
|
|
51
|
+
* @property {'load-bearing'|'deferrable'} verdict
|
|
52
|
+
* @property {'verify'|'verify-proof'} by
|
|
53
|
+
* @property {string|null} [outcome_ref]
|
|
54
|
+
*
|
|
55
|
+
* @typedef {Object} FollowupDisposition
|
|
56
|
+
* @property {'filed_task'|'executed_commit'|'parked_note'|'dropped_by_user'} kind
|
|
57
|
+
* @property {string} ref
|
|
58
|
+
* @property {string} [reason]
|
|
59
|
+
*
|
|
60
|
+
* @typedef {Object} FollowupRow
|
|
61
|
+
* @property {string} id
|
|
62
|
+
* @property {string} title
|
|
63
|
+
* @property {'in-pr'|'new-task'|'polish'} class_proposed
|
|
64
|
+
* @property {'plan'|'discuss'|'execute'|'verify'|'review'|'ci'} origin
|
|
65
|
+
* @property {string[]} files
|
|
66
|
+
* @property {string[]} outcome_refs
|
|
67
|
+
* @property {'open'|'fixed'|'dispositioned'} status
|
|
68
|
+
* @property {FollowupAdjudication} [adjudication]
|
|
69
|
+
* @property {FollowupDisposition} [disposition]
|
|
70
|
+
*
|
|
71
|
+
* @typedef {Object} ParsedFollowups
|
|
72
|
+
* @property {FollowupRow[]} rows
|
|
73
|
+
* @property {Array<{row_index: number, id?: string, field: string, message: string}>} schema_errors
|
|
74
|
+
*
|
|
75
|
+
* The selector helpers below read only `status` / `adjudication` /
|
|
76
|
+
* `disposition`, so they accept a structural row shape (any caller-built row
|
|
77
|
+
* with those fields fits) and return the same element type they were given.
|
|
78
|
+
* @typedef {{status?: string, adjudication?: {verdict?: string}, disposition?: {kind?: string, ref?: string}}} SelectorRow
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {unknown} doc — a parsed followups.json document. Accepts either
|
|
83
|
+
* `{ rows: [...] }` or a bare `[...]` array of rows.
|
|
84
|
+
* @returns {ParsedFollowups}
|
|
85
|
+
*/
|
|
86
|
+
export function parseFollowups(doc) {
|
|
87
|
+
/** @type {Array<{row_index: number, id?: string, field: string, message: string}>} */
|
|
88
|
+
const schema_errors = []
|
|
89
|
+
/** @type {Array<object>} */
|
|
90
|
+
const rows = []
|
|
91
|
+
|
|
92
|
+
const rawRows = extractRows(doc)
|
|
93
|
+
if (rawRows === null) {
|
|
94
|
+
schema_errors.push({
|
|
95
|
+
row_index: -1,
|
|
96
|
+
field: 'rows',
|
|
97
|
+
message: 'followups.json must be an array of rows or an object with a `rows` array',
|
|
98
|
+
})
|
|
99
|
+
return { rows, schema_errors }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < rawRows.length; i++) {
|
|
103
|
+
const r = rawRows[i]
|
|
104
|
+
const errsBefore = schema_errors.length
|
|
105
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
106
|
+
schema_errors.push({ row_index: i, field: 'row', message: 'row must be an object' })
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const id = typeof r.id === 'string' && r.id.trim().length > 0 ? r.id : undefined
|
|
110
|
+
const push = (field, message) => schema_errors.push({ row_index: i, id, field, message })
|
|
111
|
+
|
|
112
|
+
if (!id) push('id', 'missing or empty `id`')
|
|
113
|
+
if (typeof r.title !== 'string' || r.title.trim().length === 0)
|
|
114
|
+
push('title', 'missing or empty `title`')
|
|
115
|
+
if (!CLASS_PROPOSED.has(r.class_proposed))
|
|
116
|
+
push('class_proposed', `must be one of: ${[...CLASS_PROPOSED].join(', ')}`)
|
|
117
|
+
if (!ORIGINS.has(r.origin)) push('origin', `must be one of: ${[...ORIGINS].join(', ')}`)
|
|
118
|
+
if (!STATUSES.has(r.status)) push('status', `must be one of: ${[...STATUSES].join(', ')}`)
|
|
119
|
+
if (r.files !== undefined && !isStringArray(r.files)) push('files', 'must be a string array')
|
|
120
|
+
if (r.outcome_refs !== undefined && !isStringArray(r.outcome_refs))
|
|
121
|
+
push('outcome_refs', 'must be a string array of O<n> ids')
|
|
122
|
+
|
|
123
|
+
if (r.adjudication !== undefined) {
|
|
124
|
+
const a = r.adjudication
|
|
125
|
+
if (!a || typeof a !== 'object' || Array.isArray(a)) {
|
|
126
|
+
push('adjudication', 'must be an object when present')
|
|
127
|
+
} else {
|
|
128
|
+
if (!VERDICTS.has(a.verdict))
|
|
129
|
+
push('adjudication.verdict', `must be one of: ${[...VERDICTS].join(', ')}`)
|
|
130
|
+
if (!ADJUDICATORS.has(a.by))
|
|
131
|
+
push('adjudication.by', `must be one of: ${[...ADJUDICATORS].join(', ')}`)
|
|
132
|
+
if (
|
|
133
|
+
a.outcome_ref !== undefined &&
|
|
134
|
+
a.outcome_ref !== null &&
|
|
135
|
+
typeof a.outcome_ref !== 'string'
|
|
136
|
+
)
|
|
137
|
+
push('adjudication.outcome_ref', 'must be a string, null, or omitted')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (r.disposition !== undefined) {
|
|
142
|
+
const d = r.disposition
|
|
143
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) {
|
|
144
|
+
push('disposition', 'must be an object when present')
|
|
145
|
+
} else {
|
|
146
|
+
if (!DISPOSITION_KINDS.has(d.kind))
|
|
147
|
+
push('disposition.kind', `must be one of: ${[...DISPOSITION_KINDS].join(', ')}`)
|
|
148
|
+
if (typeof d.ref !== 'string' || d.ref.trim().length === 0)
|
|
149
|
+
push('disposition.ref', 'missing or empty `ref`')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (schema_errors.length === errsBefore) {
|
|
154
|
+
rows.push({
|
|
155
|
+
id,
|
|
156
|
+
title: r.title,
|
|
157
|
+
class_proposed: r.class_proposed,
|
|
158
|
+
origin: r.origin,
|
|
159
|
+
files: Array.isArray(r.files) ? r.files : [],
|
|
160
|
+
outcome_refs: Array.isArray(r.outcome_refs) ? r.outcome_refs : [],
|
|
161
|
+
status: r.status,
|
|
162
|
+
...(r.adjudication !== undefined
|
|
163
|
+
? { adjudication: normalizeAdjudication(r.adjudication) }
|
|
164
|
+
: {}),
|
|
165
|
+
...(r.disposition !== undefined
|
|
166
|
+
? { disposition: normalizeDisposition(r.disposition) }
|
|
167
|
+
: {}),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { rows, schema_errors }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Rows that block `task close`: any `open` row, or any `dispositioned` row
|
|
177
|
+
* without a valid `disposition.kind` + `ref`. (A `fixed` row is settled.)
|
|
178
|
+
*
|
|
179
|
+
* @template {SelectorRow} T
|
|
180
|
+
* @param {{rows: T[]}} parsed
|
|
181
|
+
* @returns {T[]}
|
|
182
|
+
*/
|
|
183
|
+
export function listBlockingRowsForClose(parsed) {
|
|
184
|
+
const rows = parsed && Array.isArray(parsed.rows) ? parsed.rows : []
|
|
185
|
+
return rows.filter((r) => {
|
|
186
|
+
if (r.status === 'open') return true
|
|
187
|
+
if (r.status === 'dispositioned') {
|
|
188
|
+
const d = r.disposition
|
|
189
|
+
return (
|
|
190
|
+
!d ||
|
|
191
|
+
!DISPOSITION_KINDS.has(d.kind) ||
|
|
192
|
+
typeof d.ref !== 'string' ||
|
|
193
|
+
d.ref.trim().length === 0
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
return false
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Open rows that have no `adjudication` (ship-gate input).
|
|
202
|
+
*
|
|
203
|
+
* @template {SelectorRow} T
|
|
204
|
+
* @param {{rows: T[]}} parsed
|
|
205
|
+
* @returns {T[]}
|
|
206
|
+
*/
|
|
207
|
+
export function listUnadjudicatedOpen(parsed) {
|
|
208
|
+
const rows = parsed && Array.isArray(parsed.rows) ? parsed.rows : []
|
|
209
|
+
return rows.filter((r) => r.status === 'open' && !r.adjudication)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Load-bearing rows that are not yet fixed (ship-gate input).
|
|
214
|
+
*
|
|
215
|
+
* @template {SelectorRow} T
|
|
216
|
+
* @param {{rows: T[]}} parsed
|
|
217
|
+
* @returns {T[]}
|
|
218
|
+
*/
|
|
219
|
+
export function listLoadBearingUnfixed(parsed) {
|
|
220
|
+
const rows = parsed && Array.isArray(parsed.rows) ? parsed.rows : []
|
|
221
|
+
return rows.filter(
|
|
222
|
+
(r) => r.adjudication && r.adjudication.verdict === 'load-bearing' && r.status !== 'fixed',
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Rows whose `status === 'dispositioned'` lack a valid disposition record.
|
|
228
|
+
*
|
|
229
|
+
* @template {SelectorRow} T
|
|
230
|
+
* @param {{rows: T[]}} parsed
|
|
231
|
+
* @returns {T[]}
|
|
232
|
+
*/
|
|
233
|
+
export function listUndispositioned(parsed) {
|
|
234
|
+
const rows = parsed && Array.isArray(parsed.rows) ? parsed.rows : []
|
|
235
|
+
return rows.filter((r) => {
|
|
236
|
+
if (r.status !== 'dispositioned') return false
|
|
237
|
+
const d = r.disposition
|
|
238
|
+
return (
|
|
239
|
+
!d || !DISPOSITION_KINDS.has(d.kind) || typeof d.ref !== 'string' || d.ref.trim().length === 0
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function extractRows(doc) {
|
|
245
|
+
if (Array.isArray(doc)) return doc
|
|
246
|
+
if (doc && typeof doc === 'object' && Array.isArray(doc.rows)) return doc.rows
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isStringArray(v) {
|
|
251
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeAdjudication(a) {
|
|
255
|
+
const out = { verdict: a.verdict, by: a.by }
|
|
256
|
+
if (a.outcome_ref !== undefined) out.outcome_ref = a.outcome_ref
|
|
257
|
+
return out
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeDisposition(d) {
|
|
261
|
+
const out = { kind: d.kind, ref: d.ref }
|
|
262
|
+
if (typeof d.reason === 'string') out.reason = d.reason
|
|
263
|
+
return out
|
|
264
|
+
}
|