@gitroll/tacitt-cli 0.6.8 → 0.6.10
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/README.md +1 -1
- package/dist/{agent-setup-YlFIJz-h.mjs → agent-setup-LPd8WEsN.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +19 -10
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +5 -5
- package/dist/{frame-Dj5Zs6UW.mjs → frame-ByT1xayJ.mjs} +1 -1
- package/dist/{heap-DsE74_-n.mjs → heap-CM298Yu0.mjs} +8 -6
- package/dist/inspection-worker.mjs +4 -4
- package/dist/{job-CeSf9UCC.mjs → job-BHIwElNF.mjs} +5 -5
- package/dist/{locate-BWkblijS.mjs → locate-Cwv6WH8n.mjs} +25 -2
- package/dist/{model-f_QnDyyb.mjs → model-zvn5GUJ9.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +1 -1
- package/dist/{query-CaBXQlpF.mjs → query-By41gaKm.mjs} +585 -250
- package/dist/{run-RA_8zZsL.mjs → run-ZQx7p4Rw.mjs} +4 -4
- package/dist/{src-p_uJNjV3.mjs → src-3jvMvSQn.mjs} +3 -3
- package/dist/{src-d7n58sbH.mjs → src-BqtdFVq7.mjs} +683 -341
- package/dist/{src-BBjnljUs.mjs → src-D_DfY--2.mjs} +4 -4
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-B14e6_-z.js +174 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/skills/tacitt/INDEX.md +6 -6
- package/skills/tacitt/SKILL.md +4 -4
- package/skills/tacitt/references/INDEX.md +3 -3
- package/skills/tacitt/references/classification.md +12 -2
- package/skills/tacitt/references/commands.md +60 -13
- package/skills/tacitt/references/journey.md +10 -2
- package/skills/tacitt/references/parallel-review.md +21 -0
- package/skills/tacitt/references/send.md +29 -8
- package/skills/tacitt/scripts/INDEX.md +39 -36
- package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
- package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
- package/skills/tacitt/scripts/journey-scan-timing.d.mts +15 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +153 -1
- package/skills/tacitt/scripts/journey-state.d.mts +6 -0
- package/skills/tacitt/scripts/journey-state.mjs +24 -1
- package/skills/tacitt/scripts/journey.d.mts +5 -0
- package/skills/tacitt/scripts/journey.mjs +48 -3
- package/skills/tacitt/scripts/privacy-validator.mjs +11 -1
- package/skills/tacitt/scripts/review.mjs +294 -38
- package/LICENSE +0 -11
- package/dist/web/assets/index-B6kJ6XXF.js +0 -174
|
@@ -3,45 +3,48 @@
|
|
|
3
3
|
> Audience: agents and maintainers
|
|
4
4
|
> Ownership: contributor-maintained
|
|
5
5
|
|
|
6
|
-
| Entry | Kind | Responsibility
|
|
7
|
-
| ---------------------------------------------------------------------------------- | -------------------------- |
|
|
8
|
-
| [INDEX.md](INDEX.md) | Directory index | Lists executable helpers shipped with the exact Skill release.
|
|
9
|
-
| [journey-scan-timing.
|
|
10
|
-
| [
|
|
11
|
-
| [
|
|
12
|
-
| [
|
|
13
|
-
| [review.
|
|
14
|
-
| [
|
|
15
|
-
| [
|
|
16
|
-
| [
|
|
17
|
-
| [journey.
|
|
18
|
-
| [journey
|
|
19
|
-
| [journey-panel-host.
|
|
20
|
-
| [journey-
|
|
21
|
-
| [
|
|
22
|
-
| [
|
|
23
|
-
| [journey-tasks.
|
|
24
|
-
| [journey-
|
|
25
|
-
| [journey-
|
|
26
|
-
| [journey-
|
|
27
|
-
| [journey-classification-metadata.
|
|
28
|
-
| [journey-
|
|
29
|
-
| [journey-consolidation.
|
|
30
|
-
| [journey-
|
|
31
|
-
| [journey-classification-tasks.
|
|
32
|
-
| [journey-classification-
|
|
33
|
-
| [journey-
|
|
34
|
-
| [journey-
|
|
35
|
-
| [journey-classification-validation.
|
|
36
|
-
| [journey-
|
|
37
|
-
| [journey-card-consolidation.
|
|
38
|
-
| [journey-card-
|
|
39
|
-
| [journey-card-values.
|
|
40
|
-
| [
|
|
41
|
-
| [review-stream-client.
|
|
6
|
+
| Entry | Kind | Responsibility |
|
|
7
|
+
| ---------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| [INDEX.md](INDEX.md) | Directory index | Lists executable helpers shipped with the exact Skill release. |
|
|
9
|
+
| [journey-scan-timing.d.mts](journey-scan-timing.d.mts) | Declaration | Content-free scan observation interface for helper measurements and tests. |
|
|
10
|
+
| [journey-scan-timing.mjs](journey-scan-timing.mjs) | Runtime measurement | Scope-bound scan wall clock plus content-free slot occupancy, ingest latency and preparation wait observations. |
|
|
11
|
+
| [describe.mjs](describe.mjs) | Runtime | Binds proposed generated metadata, never Hidden, to the current decision/context. |
|
|
12
|
+
| [local-cli.mjs](local-cli.mjs) | Read-only package verifier | Validates an explicit installed CLI identity and aggregate artifact digest without network or application-state access. |
|
|
13
|
+
| [review.mjs](review.mjs) | Runtime | Session-ready scanning, immediate sixteen-slot refill, draft heartbeat and acknowledged lost-pack recovery, planned card capacity, outcome-reported preparation recovery and read-only status, metadata-gated questions, Track B-gated final preview and hash-bound answer recovery. |
|
|
14
|
+
| [review.d.mts](review.d.mts) | Type declarations | Types the review helper's exported task rendering, answer reading, per-Work progress and entry functions for tests. |
|
|
15
|
+
| [local-cli.d.mts](local-cli.d.mts) | Type declarations | Defines the shared installed-package binding used by the CLI and standalone verifier. |
|
|
16
|
+
| [verify-cli.mjs](verify-cli.mjs) | Verification entry | Checks a managed local receipt and its current package bytes before the Skill starts a journey. |
|
|
17
|
+
| [journey.mjs](journey.mjs) | Runtime | Phase tasks with compact existing-card identity snapshots, durable exact batch writes, the person-decision `await` that hands off at Ready to send, the late placed-Work check before the classification commit (its stderr names only item indexes and Work ids) and shared CLI transport. |
|
|
18
|
+
| [journey.d.mts](journey.d.mts) | Declaration | Phase helper contract for integration tests. |
|
|
19
|
+
| [journey-panel-host.mjs](journey-panel-host.mjs) | Runtime | Runs the paired CLI's `journey.serve --detach` before the handoff so a detached host owns the panel (ADR 0227). |
|
|
20
|
+
| [journey-panel-host.d.mts](journey-panel-host.d.mts) | Declaration | Types the detached-host handoff for helper tests. |
|
|
21
|
+
| [journey-state.mjs](journey-state.mjs) | Runtime state | Pending-Public-choice, confirmed-answer and static-finishing phase selection, skip and included rules (ADR 0236), card batch limits, threshold and wait clock (ADR 0234), input fingerprints and exact pending-write recovery including definite scope rejection, with affected-Pending-only resolution. |
|
|
22
|
+
| [answer.mjs](answer.mjs) | CLI publisher | Validates task-bound classification and privacy drafts, including required privacy unit coverage and reused-card policy with repair diagnostics, reports card issue paths without mutating validator errors, and atomically publishes canonical answers. |
|
|
23
|
+
| [journey-tasks.mjs](journey-tasks.mjs) | Task builder | Exact rendered-token budgets, private drafts, a 200-value cap (ADR 0234), compact existing policies with bounded overhead (ADR 0251), and one-term-per-line card/resolution payloads with complete bounded read ranges. |
|
|
24
|
+
| [journey-tasks.d.mts](journey-tasks.d.mts) | Declaration | Types task creation, rendered-token sizing, canonical answer metadata and batch answer reads. |
|
|
25
|
+
| [journey-classification.mjs](journey-classification.mjs) | Runtime | Batched known-project metadata, unknown-root grouping and bounded evidence, versioned checkpoints and generation progress. |
|
|
26
|
+
| [journey-consolidation.mjs](journey-consolidation.mjs) | Runtime | Builds proposed-name and stable-target candidate buckets, persists complete reconciliation input, and returns compact dispatch descriptors. |
|
|
27
|
+
| [journey-classification-metadata.mjs](journey-classification-metadata.mjs) | Runtime | Validates global reconciliation and creates hash-bound, token-bounded AI metadata tasks for new Works and Unsorted. |
|
|
28
|
+
| [journey-classification-metadata.d.mts](journey-classification-metadata.d.mts) | Declaration | Types generated AI metadata tasks for final project groups and Unsorted. |
|
|
29
|
+
| [journey-consolidation-submit.mjs](journey-consolidation-submit.mjs) | Runtime | Expands candidate assignments, applies per-root overrides, combines small topics into Unsorted, attaches known projects and checks exact root coverage. |
|
|
30
|
+
| [journey-consolidation.d.mts](journey-consolidation.d.mts) | Declaration | Consolidation helper contract for bounded evidence regression tests. |
|
|
31
|
+
| [journey-classification-tasks.mjs](journey-classification-tasks.mjs) | Runtime | Original-root routing, capacity-balanced bounded tasks and unresolved-first ready ordering with stable assignments. |
|
|
32
|
+
| [journey-classification-tasks.d.mts](journey-classification-tasks.d.mts) | Declaration | Types initial classification routing and catalog/evidence task generation for helper consumers and tests. |
|
|
33
|
+
| [journey-classification-progress.mjs](journey-classification-progress.mjs) | Runtime | Accepted-answer counters and fenced progress checkpoints. |
|
|
34
|
+
| [journey-state.d.mts](journey-state.d.mts) | Declaration | Journey state helper contracts used by typed recovery tests. |
|
|
35
|
+
| [journey-classification-validation.mjs](journey-classification-validation.mjs) | Runtime | Shared root judgment, stable target identity, fixed project metadata, consolidation validation, target-preserving assignment expansion and the late check that an item joins only placed Works with one access record (ADR 0274). |
|
|
36
|
+
| [journey-classification-validation.d.mts](journey-classification-validation.d.mts) | Declaration | Shared classification validator contracts. |
|
|
37
|
+
| [journey-card-consolidation.mjs](journey-card-consolidation.mjs) | Runtime | Immutably merges matching existing-policy extensions without a second model task (ADR 0251); policy or identity mismatches fall back to digest-bound global review, with exact key coverage and five/ten question limits. |
|
|
38
|
+
| [journey-card-consolidation.d.mts](journey-card-consolidation.d.mts) | Declaration | Types global card generation, policy comparison, identity boundaries, answer validation and final key expansion. |
|
|
39
|
+
| [journey-card-values.mjs](journey-card-values.mjs) | Runtime | Settles a card publication against the current journey: only still-open values, one card per normalized value, own identity on its own card by the journey-wide own-value rule, distinct short examples. |
|
|
40
|
+
| [journey-card-values.d.mts](journey-card-values.d.mts) | Declaration | Types card value settlement for helper tests. |
|
|
41
|
+
| [review-stream-client.mjs](review-stream-client.mjs) | Runtime | Bounds requests and owned-child shutdown, permits retry only for explicit pre-request startup busy, and preserves unknown lost-write outcomes for receipt replay. |
|
|
42
|
+
| [review-stream-client.d.mts](review-stream-client.d.mts) | Type declarations | Types the fixed request/shutdown limits, private lifecycle callbacks and review-stream client methods. |
|
|
42
43
|
|
|
43
44
|
`review.mjs` also owns bounded ingest-and-refill stream reuse and slot-limited recovery of reserved packs.
|
|
44
45
|
|
|
45
46
|
`journey-tasks.mjs` preserves exact greedy card/resolution boundaries using bounded prefix search rather than serializing every growing prefix.
|
|
46
47
|
|
|
48
|
+
`journey.mjs` uses local immutable drafts for append-card consolidation and incomplete ingest when no request or receipt needs recovery; ready publication retains live-state settlement.
|
|
49
|
+
|
|
47
50
|
`review.mjs` amortizes small-task instructions and fills confirmed free native slots before distributing additional complete units by current pack load.
|
|
@@ -18,3 +18,16 @@ export function validateConsolidationAnswer(
|
|
|
18
18
|
projectCatalog: unknown[]
|
|
19
19
|
},
|
|
20
20
|
): { code: string | null; details: string[] }
|
|
21
|
+
export function validateLatePlacedWorks(
|
|
22
|
+
items: ReadonlyArray<{ projectName: string; rootSessionIds: readonly string[] }>,
|
|
23
|
+
works: ReadonlyArray<{
|
|
24
|
+
id: string
|
|
25
|
+
placement?: string
|
|
26
|
+
rootSessionIds: readonly string[]
|
|
27
|
+
access: string
|
|
28
|
+
accessOrigin?: string
|
|
29
|
+
accessConfirmed?: boolean
|
|
30
|
+
accessChoice?: { channel: string; at: string }
|
|
31
|
+
accountAccessConflict?: boolean
|
|
32
|
+
}>,
|
|
33
|
+
): void
|
|
@@ -238,3 +238,41 @@ export function compileAssignments(candidates, assignments, overrides = []) {
|
|
|
238
238
|
for (const override of overrides) result.set(override.rootKey, override.groupId)
|
|
239
239
|
return result
|
|
240
240
|
}
|
|
241
|
+
|
|
242
|
+
const sameAccessRecord = (work, other) =>
|
|
243
|
+
work.access === other.access &&
|
|
244
|
+
work.accessOrigin === other.accessOrigin &&
|
|
245
|
+
work.accessConfirmed === other.accessConfirmed &&
|
|
246
|
+
work.accessChoice?.channel === other.accessChoice?.channel &&
|
|
247
|
+
work.accessChoice?.at === other.accessChoice?.at &&
|
|
248
|
+
work.accountAccessConflict === other.accountAccessConflict
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* ADR 0274: after scope confirmation one item may hold roots of several placed Works only when
|
|
252
|
+
* they share one access record (an isolated Work and its parent). Mirrors `sameAccessRecord` in
|
|
253
|
+
* `@sess-pilot/application` scope/proposal.ts, which refuses the commit otherwise.
|
|
254
|
+
*/
|
|
255
|
+
export function validateLatePlacedWorks(items, works) {
|
|
256
|
+
const placed = works.filter((work) => work.placement === 'placed')
|
|
257
|
+
const details = []
|
|
258
|
+
const conflicts = []
|
|
259
|
+
items.forEach((item, index) => {
|
|
260
|
+
const owners = placed.filter((work) =>
|
|
261
|
+
work.rootSessionIds.some((root) => item.rootSessionIds.includes(root)),
|
|
262
|
+
)
|
|
263
|
+
if (!owners.some((work) => !sameAccessRecord(work, owners[0]))) return
|
|
264
|
+
conflicts.push({ item: index, workIds: owners.map((work) => work.id) })
|
|
265
|
+
details.push(
|
|
266
|
+
`items[${index}] (${item.projectName}) joins roots of Works ${owners
|
|
267
|
+
.map((work) => work.id)
|
|
268
|
+
.join(', ')}, which have different visibility; keep each Work's roots in its own item`,
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
// `conflicts` carries only item indexes and Work ids, so the helper may print it to stderr.
|
|
272
|
+
if (details.length)
|
|
273
|
+
throw Object.assign(new Error('CLASSIFICATION_LATE_SEVERAL_WORKS'), {
|
|
274
|
+
code: 'CLASSIFICATION_LATE_SEVERAL_WORKS',
|
|
275
|
+
details,
|
|
276
|
+
conflicts,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScanTimingObservation {
|
|
2
|
+
at: number
|
|
3
|
+
packs: readonly {
|
|
4
|
+
id: string
|
|
5
|
+
dispatchedAt: number | string
|
|
6
|
+
publishedAt: number | null
|
|
7
|
+
}[]
|
|
8
|
+
activeCardChildren: number
|
|
9
|
+
queuedBatches: number
|
|
10
|
+
unreadBatches: number
|
|
11
|
+
waiting: string | null | undefined
|
|
12
|
+
pendingWorkIds: readonly string[]
|
|
13
|
+
ingestStartedAt?: number
|
|
14
|
+
}
|
|
15
|
+
export function observeScanTiming(run: string, observation: ScanTimingObservation): Promise<void>
|
|
@@ -119,6 +119,7 @@ export async function scanTiming(run, journeyId, view, start = false, call) {
|
|
|
119
119
|
if (receipt.binding !== identity || (authority !== null && receipt.authority !== authority))
|
|
120
120
|
throw new Error('SCAN_TIMING_SCOPE_CHANGED')
|
|
121
121
|
if (!receipt.completedAt && complete(view)) {
|
|
122
|
+
closePreparationInterval(receipt.measurements?.preparation, Date.now())
|
|
122
123
|
receipt = {
|
|
123
124
|
...receipt,
|
|
124
125
|
completedAt: new Date().toISOString(),
|
|
@@ -130,9 +131,10 @@ export async function scanTiming(run, journeyId, view, start = false, call) {
|
|
|
130
131
|
}
|
|
131
132
|
const elapsedMs =
|
|
132
133
|
Date.parse(receipt.completedAt ?? new Date().toISOString()) - Date.parse(receipt.startedAt)
|
|
133
|
-
const { initialWorks, finalWorks, ...summary } = receipt
|
|
134
|
+
const { initialWorks, finalWorks, measurements, ...summary } = receipt
|
|
134
135
|
return {
|
|
135
136
|
...summary,
|
|
137
|
+
...(measurements ? { measurements: measurementSummary(measurements) } : {}),
|
|
136
138
|
initialWorkCount: initialWorks.length,
|
|
137
139
|
...(finalWorks ? { finalWorkCount: finalWorks.length } : {}),
|
|
138
140
|
receipt: path,
|
|
@@ -142,3 +144,153 @@ export async function scanTiming(run, journeyId, view, start = false, call) {
|
|
|
142
144
|
withinBudget: receipt.completedAt ? elapsedMs < 30 * 60 * 1000 : null,
|
|
143
145
|
}
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
// Keep per-child and per-ingest evidence in the receipt, outside repeated parent replies.
|
|
149
|
+
function measurementSummary(measurements) {
|
|
150
|
+
const { slots, preparation } = measurements
|
|
151
|
+
return {
|
|
152
|
+
observationBasis: measurements.observationBasis,
|
|
153
|
+
slots: {
|
|
154
|
+
capacity: slots.capacity,
|
|
155
|
+
busyChildMs: slots.busyChildMs,
|
|
156
|
+
waitingBatchWallMs: slots.waitingBatchWallMs,
|
|
157
|
+
occupancy: slots.occupancy,
|
|
158
|
+
refillGapPercentilesMs: slots.refillGapPercentilesMs,
|
|
159
|
+
childCount: slots.children.length,
|
|
160
|
+
},
|
|
161
|
+
preparation: {
|
|
162
|
+
milliseconds: preparation.milliseconds,
|
|
163
|
+
intervalCount: preparation.intervals.length,
|
|
164
|
+
},
|
|
165
|
+
ingestCount: measurements.ingests.length,
|
|
166
|
+
ingestPercentilesMs: measurements.ingestPercentilesMs,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function closePreparationInterval(preparation, at) {
|
|
171
|
+
const interval = preparation?.intervals.at(-1)
|
|
172
|
+
if (interval?.endedAt === null) {
|
|
173
|
+
interval.endedAt = at
|
|
174
|
+
preparation.milliseconds[interval.phase] += Math.max(0, at - interval.startedAt)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function percentiles(values) {
|
|
179
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
180
|
+
const percentile = (fraction) =>
|
|
181
|
+
sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)] ?? null
|
|
182
|
+
return { p50: percentile(0.5), p95: percentile(0.95) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Content-free helper observations; publication timestamps come from answer-file metadata.
|
|
186
|
+
* Card occupancy and slot release are observed at helper boundaries, not native scheduler events.
|
|
187
|
+
* The RPC protocol does not expose store lock wait, so it remains explicitly unavailable.
|
|
188
|
+
*/
|
|
189
|
+
export async function observeScanTiming(run, observation) {
|
|
190
|
+
const path = join(run, 'scan-timing.json')
|
|
191
|
+
const receipt = await optional(path)
|
|
192
|
+
if (!receipt || receipt.completedAt) return
|
|
193
|
+
const at = observation.at
|
|
194
|
+
const measurements = receipt.measurements ?? {
|
|
195
|
+
slots: { capacity: 16, busyChildMs: 0, waitingBatchWallMs: 0, refillGapMs: [], children: [] },
|
|
196
|
+
preparation: {
|
|
197
|
+
intervals: [],
|
|
198
|
+
milliseconds: { 'before-first-batch': 0, 'mid-run': 0, tail: 0 },
|
|
199
|
+
},
|
|
200
|
+
ingests: [],
|
|
201
|
+
observationBasis: {
|
|
202
|
+
dispatch: 'helper-reservation',
|
|
203
|
+
publication: 'answer-file-mtime',
|
|
204
|
+
slotRelease: 'helper-observed-publication-or-retirement',
|
|
205
|
+
cards: 'parent-reported-count-at-helper-boundary',
|
|
206
|
+
preparation: 'helper-observed-interval',
|
|
207
|
+
lockWait: 'unavailable-in-rpc',
|
|
208
|
+
},
|
|
209
|
+
seenBatch: false,
|
|
210
|
+
last: null,
|
|
211
|
+
}
|
|
212
|
+
const { slots, preparation, last } = measurements
|
|
213
|
+
const packs = observation.packs.map((pack) => ({
|
|
214
|
+
id: hash(pack.id),
|
|
215
|
+
dispatchedAt:
|
|
216
|
+
typeof pack.dispatchedAt === 'number' ? pack.dispatchedAt : Date.parse(pack.dispatchedAt),
|
|
217
|
+
publishedAt: pack.publishedAt ?? null,
|
|
218
|
+
}))
|
|
219
|
+
for (const pack of packs) {
|
|
220
|
+
let child = slots.children.find((item) => item.id === pack.id)
|
|
221
|
+
if (!child) {
|
|
222
|
+
const freed = slots.children.find(
|
|
223
|
+
(item) =>
|
|
224
|
+
item.freedAt !== null &&
|
|
225
|
+
item.freedAt <= pack.dispatchedAt &&
|
|
226
|
+
item.nextDispatchAt === null,
|
|
227
|
+
)
|
|
228
|
+
if (freed) {
|
|
229
|
+
freed.nextDispatchAt = pack.dispatchedAt
|
|
230
|
+
slots.refillGapMs.push(Math.max(0, pack.dispatchedAt - freed.freedAt))
|
|
231
|
+
}
|
|
232
|
+
child = { ...pack, freedAt: null, nextDispatchAt: null }
|
|
233
|
+
slots.children.push(child)
|
|
234
|
+
}
|
|
235
|
+
if (pack.publishedAt !== null) {
|
|
236
|
+
child.publishedAt = pack.publishedAt
|
|
237
|
+
child.freedAt ??= at
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// A retired/cancelled child also frees a slot, without claiming it published an answer.
|
|
241
|
+
for (const child of slots.children)
|
|
242
|
+
if (child.freedAt === null && !packs.some((pack) => pack.id === child.id)) child.freedAt = at
|
|
243
|
+
if (last && at >= last.at && last.batchesWaiting) {
|
|
244
|
+
const elapsed = at - last.at
|
|
245
|
+
slots.waitingBatchWallMs += elapsed
|
|
246
|
+
slots.busyChildMs += elapsed * last.activeCardChildren
|
|
247
|
+
for (const id of last.activePackIds) {
|
|
248
|
+
const child = slots.children.find((item) => item.id === id)
|
|
249
|
+
if (child) slots.busyChildMs += Math.max(0, Math.min(at, child.publishedAt ?? at) - last.at)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
slots.occupancy = slots.waitingBatchWallMs
|
|
253
|
+
? slots.busyChildMs / (slots.capacity * slots.waitingBatchWallMs)
|
|
254
|
+
: null
|
|
255
|
+
const activePacks = packs.filter((pack) => pack.publishedAt === null)
|
|
256
|
+
const batchesWaiting = observation.queuedBatches > 0 || observation.unreadBatches > 0
|
|
257
|
+
measurements.seenBatch ||= batchesWaiting || packs.length > 0
|
|
258
|
+
const phase = !measurements.seenBatch
|
|
259
|
+
? 'before-first-batch'
|
|
260
|
+
: batchesWaiting || activePacks.length
|
|
261
|
+
? 'mid-run'
|
|
262
|
+
: 'tail'
|
|
263
|
+
const pendingWorkHashes = [...new Set(observation.pendingWorkIds.map((id) => hash(id)))].sort(
|
|
264
|
+
(a, b) => a.localeCompare(b),
|
|
265
|
+
)
|
|
266
|
+
const waiting = observation.waiting === 'preparation'
|
|
267
|
+
const interval = preparation.intervals.at(-1)
|
|
268
|
+
if (
|
|
269
|
+
interval?.endedAt === null &&
|
|
270
|
+
(!waiting ||
|
|
271
|
+
interval.phase !== phase ||
|
|
272
|
+
JSON.stringify(interval.pendingWorkHashes) !== JSON.stringify(pendingWorkHashes))
|
|
273
|
+
) {
|
|
274
|
+
closePreparationInterval(preparation, at)
|
|
275
|
+
}
|
|
276
|
+
if (waiting && (!interval || interval.endedAt !== null))
|
|
277
|
+
preparation.intervals.push({ phase, startedAt: at, endedAt: null, pendingWorkHashes })
|
|
278
|
+
if (observation.ingestStartedAt !== undefined)
|
|
279
|
+
measurements.ingests.push({
|
|
280
|
+
startedAt: observation.ingestStartedAt,
|
|
281
|
+
completedAt: at,
|
|
282
|
+
elapsedMs: Math.max(0, at - observation.ingestStartedAt),
|
|
283
|
+
lockWaitMs: null,
|
|
284
|
+
})
|
|
285
|
+
slots.refillGapPercentilesMs = percentiles(slots.refillGapMs)
|
|
286
|
+
measurements.ingestPercentilesMs = percentiles(
|
|
287
|
+
measurements.ingests.map((ingest) => ingest.elapsedMs),
|
|
288
|
+
)
|
|
289
|
+
measurements.last = {
|
|
290
|
+
at,
|
|
291
|
+
batchesWaiting,
|
|
292
|
+
activeCardChildren: observation.activeCardChildren,
|
|
293
|
+
activePackIds: activePacks.map((pack) => pack.id),
|
|
294
|
+
}
|
|
295
|
+
await save(path, { ...receipt, measurements })
|
|
296
|
+
}
|
|
@@ -40,6 +40,12 @@ type SkipRuleWork = {
|
|
|
40
40
|
}
|
|
41
41
|
export function isSkipped(work: Pick<SkipRuleWork, 'status' | 'blockedCodes'>): boolean
|
|
42
42
|
export function isIncluded(work: SkipRuleWork): boolean
|
|
43
|
+
export function hasPublicChoice(work: {
|
|
44
|
+
access: string
|
|
45
|
+
accessConfirmed?: boolean
|
|
46
|
+
accessOrigin?: string
|
|
47
|
+
accessChoice?: unknown
|
|
48
|
+
}): boolean
|
|
43
49
|
export function definiteWriteRejection(error: unknown): boolean
|
|
44
50
|
export function stageFingerprint(view: unknown, stage: string): string
|
|
45
51
|
export function planResolutions(
|
|
@@ -33,6 +33,17 @@ export const isIncluded = (work) =>
|
|
|
33
33
|
work.rootSessionIds.length > 0 &&
|
|
34
34
|
work.placement !== 'classifying' &&
|
|
35
35
|
!isSkipped(work)
|
|
36
|
+
/**
|
|
37
|
+
* Public needs a recorded choice (ADR 0274): the person's, or the account's when every root was
|
|
38
|
+
* already Public (ADR 0275), as `hasPublicChoice` of `@sess-pilot/application`;
|
|
39
|
+
* journey-helper.test.ts checks the older-format and account cases.
|
|
40
|
+
*/
|
|
41
|
+
export const hasPublicChoice = (work) =>
|
|
42
|
+
work.access !== 'public' ||
|
|
43
|
+
(work.accessConfirmed === true &&
|
|
44
|
+
work.accessChoice !== undefined &&
|
|
45
|
+
work.accessChoice !== null &&
|
|
46
|
+
work.accessOrigin === (work.accessChoice.channel === 'account' ? 'account' : 'explicit'))
|
|
36
47
|
/** A journey-wide dictionary remains relevant while any Work is selected. */
|
|
37
48
|
export function activeTerms(view) {
|
|
38
49
|
return view.works.some((work) => work.access !== 'drop' && work.rootSessionIds.length)
|
|
@@ -62,6 +73,13 @@ export function phaseOf(view) {
|
|
|
62
73
|
const selected = view.works.filter(isIncluded)
|
|
63
74
|
// Every Work failed to read or was Dropped: there is no Send, only Retry or un-Drop in the panel.
|
|
64
75
|
if (!selected.length) return 'nothing-to-send'
|
|
76
|
+
if (
|
|
77
|
+
selected.some(
|
|
78
|
+
(work) =>
|
|
79
|
+
(work.access === 'private' && work.accessConfirmed === false) || !hasPublicChoice(work),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return 'public-choice'
|
|
65
83
|
// Track B builds each Work's private packaging base in the background (ADR 0171).
|
|
66
84
|
if (selected.some((work) => !work.staticReview)) return 'static-finishing'
|
|
67
85
|
// The person's Send click packages and uploads (D5); the agent has nothing left to run.
|
|
@@ -186,9 +204,14 @@ export const procedureFor = (stage) =>
|
|
|
186
204
|
resolution: 'journey.resolve',
|
|
187
205
|
})[stage]
|
|
188
206
|
|
|
189
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Record these explicit refusals as rejected. INVALID_SELECTION alone does not prove an
|
|
209
|
+
* earlier attempt never committed: append replay relies on the service returning its receipt
|
|
210
|
+
* before surfacing a refusal from validation against newer state.
|
|
211
|
+
*/
|
|
190
212
|
export const definiteWriteRejection = (error) =>
|
|
191
213
|
[
|
|
214
|
+
'SCOPE_NOT_READY',
|
|
192
215
|
'INVALID_SELECTION',
|
|
193
216
|
'INVALID_ARGUMENTS',
|
|
194
217
|
'INVALID_REVIEW_SPAN',
|
|
@@ -17,6 +17,9 @@ export function phaseOf(view: {
|
|
|
17
17
|
metadataReviewedDigest?: string
|
|
18
18
|
classificationMetadata?: { digest: string }
|
|
19
19
|
access: string
|
|
20
|
+
accessOrigin?: string
|
|
21
|
+
accessConfirmed?: boolean
|
|
22
|
+
accessChoice?: unknown
|
|
20
23
|
rootSessionIds: readonly string[]
|
|
21
24
|
status?: string
|
|
22
25
|
blockedCodes?: readonly string[]
|
|
@@ -27,6 +30,8 @@ export function phaseOf(view: {
|
|
|
27
30
|
}[]
|
|
28
31
|
}): string
|
|
29
32
|
export function main(argv: string[]): Promise<Record<string, unknown>>
|
|
33
|
+
/** The helper's stderr for a failed command: the code and, for a late join, safe ids only. */
|
|
34
|
+
export function failureOutput(error: unknown): string
|
|
30
35
|
|
|
31
36
|
export function stageFingerprint(view: unknown, stage: string): string
|
|
32
37
|
export function runPhase(
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
uncoveredPending,
|
|
23
23
|
} from './journey-state.mjs'
|
|
24
24
|
import { classificationTasks } from './journey-classification-tasks.mjs'
|
|
25
|
+
import { validateLatePlacedWorks } from './journey-classification-validation.mjs'
|
|
25
26
|
import { classificationEvidence, classificationFormat } from './journey-classification.mjs'
|
|
26
27
|
import {
|
|
27
28
|
consolidateClassification,
|
|
@@ -197,6 +198,21 @@ async function runPhaseCore(
|
|
|
197
198
|
call,
|
|
198
199
|
) {
|
|
199
200
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 16) throw new Error('INVALID_ARGUMENTS')
|
|
201
|
+
// Append-batch consolidation uses immutable local drafts. No current journey view is
|
|
202
|
+
// needed until publication; retain write recovery and live value settlement on that path.
|
|
203
|
+
if (stage === 'cards' && !refresh && ['consolidate', 'ingest'].includes(command)) {
|
|
204
|
+
const active = await optional(join(run, 'cards-active.json'))
|
|
205
|
+
const manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
|
|
206
|
+
if (
|
|
207
|
+
manifest?.format === classificationFormat &&
|
|
208
|
+
manifest.batch &&
|
|
209
|
+
!(await optional(join(active.directory, 'request.json'))) &&
|
|
210
|
+
!(await optional(join(active.directory, 'receipt.json')))
|
|
211
|
+
) {
|
|
212
|
+
const readiness = await consolidateCards(active.directory, manifest)
|
|
213
|
+
if (command === 'consolidate' || !readiness.readyForIngest) return readiness
|
|
214
|
+
}
|
|
215
|
+
}
|
|
200
216
|
if (command === 'await')
|
|
201
217
|
return awaitDecision(
|
|
202
218
|
journeyId,
|
|
@@ -499,7 +515,7 @@ async function runPhaseCore(
|
|
|
499
515
|
? { expectedClassificationRevision: manifest.partial.revision }
|
|
500
516
|
: {}),
|
|
501
517
|
...(stage === 'classification'
|
|
502
|
-
? { [key]: await
|
|
518
|
+
? { [key]: await classificationItems(manifest, view) }
|
|
503
519
|
: stage === 'cards'
|
|
504
520
|
? cards
|
|
505
521
|
: { [key]: await answers(tasks, key) }),
|
|
@@ -534,6 +550,16 @@ async function runPhaseCore(
|
|
|
534
550
|
return { stage, status: result.status, revision: result.state?.revision }
|
|
535
551
|
}
|
|
536
552
|
|
|
553
|
+
/**
|
|
554
|
+
* The complete proposal, checked before commit: after scope confirmation an item never joins
|
|
555
|
+
* placed Works with different visibility (ADR 0274), which CLI would refuse.
|
|
556
|
+
*/
|
|
557
|
+
async function classificationItems(manifest, view) {
|
|
558
|
+
const items = await classificationSubmission(manifest, view)
|
|
559
|
+
if (view.scope?.status === 'confirmed') validateLatePlacedWorks(items, view.works)
|
|
560
|
+
return items
|
|
561
|
+
}
|
|
562
|
+
|
|
537
563
|
/** Classification published known Works and is still grouping the remaining roots (E2). */
|
|
538
564
|
const grouping = (view) => view.works.some((work) => work.placement === 'classifying')
|
|
539
565
|
|
|
@@ -576,6 +602,7 @@ async function ingestKnownWorks(directory, manifest, view, call, partial) {
|
|
|
576
602
|
|
|
577
603
|
const terminalClassificationErrors = new Set([
|
|
578
604
|
'INVALID_SELECTION',
|
|
605
|
+
'CLASSIFICATION_LATE_SEVERAL_WORKS',
|
|
579
606
|
'CLASSIFICATION_PROJECT_NAME_CONFLICT',
|
|
580
607
|
'CLASSIFICATION_ROOT_COVERAGE_INVALID',
|
|
581
608
|
'CLASSIFICATION_THEME_TOO_SMALL',
|
|
@@ -650,13 +677,31 @@ export async function main(argv) {
|
|
|
650
677
|
),
|
|
651
678
|
)
|
|
652
679
|
}
|
|
680
|
+
const safeId = /^[A-Za-z0-9._:-]{1,128}$/u
|
|
681
|
+
/**
|
|
682
|
+
* Stderr for a failed command: the code, and for CLASSIFICATION_LATE_SEVERAL_WORKS one line per
|
|
683
|
+
* refused item with its index and the placed Work ids, never a title or path (ADR 0274).
|
|
684
|
+
*/
|
|
685
|
+
export function failureOutput(error) {
|
|
686
|
+
const message = error?.message ?? ''
|
|
687
|
+
const code = /^[A-Z_]{3,80}$/u.test(message) ? message : 'JOURNEY_HELPER_FAILED'
|
|
688
|
+
const lines = [code]
|
|
689
|
+
if (code === 'CLASSIFICATION_LATE_SEVERAL_WORKS')
|
|
690
|
+
for (const conflict of error.conflicts ?? [])
|
|
691
|
+
if (Number.isInteger(conflict.item))
|
|
692
|
+
lines.push(
|
|
693
|
+
`items[${conflict.item}] joins Works ${conflict.workIds
|
|
694
|
+
.map((id) => (safeId.test(id) ? id : '?'))
|
|
695
|
+
.join(', ')}`,
|
|
696
|
+
)
|
|
697
|
+
return lines.join('\n') + '\n'
|
|
698
|
+
}
|
|
653
699
|
const entry = process.argv[1]
|
|
654
700
|
if (entry && (await realpath(entry)) === fileURLToPath(import.meta.url))
|
|
655
701
|
main(process.argv.slice(2)).then(
|
|
656
702
|
(result) => process.stdout.write(JSON.stringify({ ok: true, ...result }) + '\n'),
|
|
657
703
|
(error) => {
|
|
658
|
-
|
|
659
|
-
process.stderr.write(code + '\n')
|
|
704
|
+
process.stderr.write(failureOutput(error))
|
|
660
705
|
process.exitCode = 1
|
|
661
706
|
},
|
|
662
707
|
)
|
|
@@ -10131,7 +10131,8 @@ const journeyWorkSchema = object({
|
|
|
10131
10131
|
"default",
|
|
10132
10132
|
"model",
|
|
10133
10133
|
"explicit",
|
|
10134
|
-
"delegate"
|
|
10134
|
+
"delegate",
|
|
10135
|
+
"account"
|
|
10135
10136
|
]),
|
|
10136
10137
|
placement: _enum([
|
|
10137
10138
|
"placed",
|
|
@@ -10148,6 +10149,15 @@ const journeyWorkSchema = object({
|
|
|
10148
10149
|
grouping: _enum(["workspace", "regrouped"]),
|
|
10149
10150
|
access: journeyAccessSchema,
|
|
10150
10151
|
accessConfirmed: boolean(),
|
|
10152
|
+
accessChoice: object({
|
|
10153
|
+
channel: _enum([
|
|
10154
|
+
"conversation",
|
|
10155
|
+
"panel",
|
|
10156
|
+
"account"
|
|
10157
|
+
]),
|
|
10158
|
+
at: datetime()
|
|
10159
|
+
}).strict().optional(),
|
|
10160
|
+
accountAccessConflict: boolean().optional(),
|
|
10151
10161
|
reason: string().max(1e3),
|
|
10152
10162
|
revision: count$1,
|
|
10153
10163
|
inputRevision: count$1.min(1),
|