@gitroll/tacitt-cli 0.6.9 → 0.6.11

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 (57) hide show
  1. package/README.md +43 -4
  2. package/dist/{agent-setup-CODlN6zl.mjs → agent-setup-DdfF3zch.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +35 -12
  5. package/dist/codex-title-worker.mjs +1 -1
  6. package/dist/configured-preview-worker.mjs +5 -5
  7. package/dist/configured-session-worker.mjs +9 -7
  8. package/dist/{frame-Dj5Zs6UW.mjs → frame-Bq2zRh-i.mjs} +1 -1
  9. package/dist/{heap-Dt3-d27v.mjs → heap-CV8Eca8V.mjs} +217 -13
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-BQXssW46.mjs → job-BKsifhyv.mjs} +38 -18
  12. package/dist/{locate-BWkblijS.mjs → locate-Cx52Zqxv.mjs} +204 -20
  13. package/dist/{model-CSx2dP_P.mjs → model-C1UXkHh9.mjs} +1 -1
  14. package/dist/operation-executor.mjs +3 -3
  15. package/dist/preview-worker.mjs +1 -1
  16. package/dist/privacy-validator.mjs +29 -2
  17. package/dist/{query-CfhcS4gd.mjs → query-AMZerUh8.mjs} +712 -215
  18. package/dist/{run-xlOaKCk8.mjs → run-CaPktPhH.mjs} +4 -4
  19. package/dist/{src-DzKuzGVH.mjs → src-D1iONla3.mjs} +2215 -1613
  20. package/dist/{src-V9GHLZQs.mjs → src-DlVbGB32.mjs} +74 -37
  21. package/dist/{src-BgsEnMsy.mjs → src-wZx_v0Ih.mjs} +297 -207
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-CYl7eDUu.js +172 -0
  24. package/dist/web/assets/index-DzOozqms.css +1 -0
  25. package/dist/web/index.html +2 -2
  26. package/package.json +1 -1
  27. package/skills/tacitt/INDEX.md +6 -6
  28. package/skills/tacitt/SKILL.md +11 -11
  29. package/skills/tacitt/references/INDEX.md +3 -3
  30. package/skills/tacitt/references/b-method.md +2 -2
  31. package/skills/tacitt/references/bootstrap.md +2 -2
  32. package/skills/tacitt/references/classification.md +15 -5
  33. package/skills/tacitt/references/commands.md +37 -30
  34. package/skills/tacitt/references/journey.md +14 -6
  35. package/skills/tacitt/references/parallel-review.md +27 -33
  36. package/skills/tacitt/references/send.md +35 -9
  37. package/skills/tacitt/scripts/INDEX.md +37 -37
  38. package/skills/tacitt/scripts/answer.mjs +73 -15
  39. package/skills/tacitt/scripts/journey-classification-metadata.mjs +75 -26
  40. package/skills/tacitt/scripts/journey-classification-progress.mjs +1 -2
  41. package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
  42. package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
  43. package/skills/tacitt/scripts/journey-classification.mjs +6 -1
  44. package/skills/tacitt/scripts/journey-consolidation.mjs +1 -0
  45. package/skills/tacitt/scripts/journey-scan-timing.d.mts +1 -0
  46. package/skills/tacitt/scripts/journey-scan-timing.mjs +19 -4
  47. package/skills/tacitt/scripts/journey-state.d.mts +15 -1
  48. package/skills/tacitt/scripts/journey-state.mjs +104 -5
  49. package/skills/tacitt/scripts/journey-tasks.d.mts +1 -0
  50. package/skills/tacitt/scripts/journey-tasks.mjs +13 -5
  51. package/skills/tacitt/scripts/journey.d.mts +46 -28
  52. package/skills/tacitt/scripts/journey.mjs +57 -8
  53. package/skills/tacitt/scripts/privacy-validator.mjs +178 -12
  54. package/skills/tacitt/scripts/review.d.mts +7 -6
  55. package/skills/tacitt/scripts/review.mjs +576 -147
  56. package/dist/web/assets/index-9UfCT_2C.js +0 -174
  57. package/dist/web/assets/index-CYJBq1px.css +0 -1
@@ -3,43 +3,43 @@
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.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 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 | 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 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 and target-preserving assignment expansion. |
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. |
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, native reader and turn receipts, bounded reissue and visible interruption 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), the left-out session count (issue #59), 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. Reads the host Public source projection with older-view choice fallback. |
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, reduces oversized Works from all evidence slices 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. Declares the optional projected Public source for compatibility. |
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. |
43
43
 
44
44
  `review.mjs` also owns bounded ingest-and-refill stream reuse and slot-limited recovery of reserved packs.
45
45
 
@@ -1,8 +1,14 @@
1
- import { readFile } from 'node:fs/promises'
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
2
  import { dirname, join, resolve } from 'node:path'
3
- import { hash, optional, save } from './journey-state.mjs'
3
+ import { hash, openTaskDraft, optional, save, taskDraftPath } from './journey-state.mjs'
4
4
  import { pathToFileURL } from 'node:url'
5
- import { readAnswer, validatePrivacyAnswer, withUnlockedJourneyCli } from './review.mjs'
5
+ import {
6
+ draftTermIndices,
7
+ readAnswer,
8
+ validatePrivacyAnswer,
9
+ verifyPackCoverage,
10
+ withUnlockedJourneyCli,
11
+ } from './review.mjs'
6
12
  import { publishClassificationProgress } from './journey-classification-progress.mjs'
7
13
  import { sameCardPolicy, validateCardConsolidation } from './journey-card-consolidation.mjs'
8
14
  import {
@@ -45,11 +51,43 @@ async function validator() {
45
51
  }
46
52
 
47
53
  async function publish(args) {
48
- if (args.length !== 4 || args[0] !== '--task' || args[2] !== '--input') throw new Error('USAGE')
54
+ const readPage = args[2] === '--read' && args.length === 5
55
+ if (args[0] !== '--task' || (!readPage && (args.length !== 4 || args[2] !== '--input')))
56
+ throw new Error('USAGE')
49
57
  const taskPath = resolve(args[1]),
50
58
  draftPath = resolve(args[3]),
51
59
  descriptorPath = taskPath.replace(/\.md$/u, '.publish.json')
52
60
  if (descriptorPath === taskPath) throw new Error('TASK_PATH')
61
+ if (readPage) {
62
+ const ref = args[3]
63
+ const ordinal = args[4]
64
+ if (!/^U[1-9]\d*$/u.test(ref) || !/^[1-9]\d*$/u.test(ordinal)) throw new Error('PAGE_RANGE')
65
+ const manifest = await readInput(taskPath, 'TASK', 'utf8')
66
+ const lines = manifest.split('\n')
67
+ const prefix = 'Page digest ' + ref + '/' + ordinal + ' '
68
+ const start = lines.findLastIndex((line) => line.startsWith('Coverage attempt: '))
69
+ const line = start < 0 ? null : lines.slice(start + 1).find((item) => item.startsWith(prefix))
70
+ if (!line) throw new Error('PAGE_RANGE')
71
+ const output = await readInput(
72
+ join(taskPath.replace(/\.md$/u, '.pages'), ref + '-' + ordinal),
73
+ 'PAGE',
74
+ 'utf8',
75
+ )
76
+ const { createHash } = await import('node:crypto')
77
+ const receipt = /\nREAD_RECEIPT ([a-f0-9]{64})\n$/u.exec(output)?.[1]
78
+ if (!receipt || createHash('sha256').update(output).digest('hex') !== line.slice(prefix.length))
79
+ throw new Error('SOURCE_BINDING')
80
+ await new Promise((done, reject) =>
81
+ process.stdout.write(output, (error) => (error ? reject(error) : done())),
82
+ )
83
+ // A new page is a heartbeat; repeated reads of the same page cannot renew a lost reader.
84
+ const directory = taskPath.replace(/\.md$/u, '.reads')
85
+ await mkdir(directory, { recursive: true, mode: 0o700 })
86
+ await writeFile(join(directory, receipt), '', { flag: 'wx', mode: 0o600 }).catch((error) => {
87
+ if (error.code !== 'EEXIST') throw error
88
+ })
89
+ return
90
+ }
53
91
  const descriptor = JSON.parse(await readInput(descriptorPath, 'DESCRIPTOR', 'utf8'))
54
92
  if (
55
93
  descriptor.version !== 1 ||
@@ -77,7 +115,26 @@ async function publish(args) {
77
115
  throw new Error('TASK_INPUT')
78
116
  if (hash(taskInput) !== descriptor.inputHash) throw new Error('INPUT_HASH')
79
117
  }
80
- const draft = await readInput(draftPath, 'DRAFT')
118
+ if (descriptor.kind === 'privacy') {
119
+ const sourceHash = createHash('sha256').update(JSON.stringify(descriptor.source)).digest('hex')
120
+ if (
121
+ sourceHash !== descriptor.sourceHash ||
122
+ !taskText.includes(`\nSOURCE_HASH\n${sourceHash}\n`)
123
+ )
124
+ throw new Error('SOURCE_BINDING')
125
+ }
126
+ if (draftPath !== taskDraftPath(taskPath)) throw new Error('DRAFT_PATH')
127
+ const file = await openTaskDraft(taskPath).catch((error) => {
128
+ if (error.code === 'ENOENT') throw new Error('DRAFT_NOT_FOUND')
129
+ if (error.code === 'ELOOP') throw new Error('DRAFT_PATH')
130
+ throw error
131
+ })
132
+ let draft
133
+ try {
134
+ draft = await file.readFile()
135
+ } finally {
136
+ await file.close()
137
+ }
81
138
  const maxDraftBytes = descriptor.kind === 'privacy' ? MAX_PRIVACY_DRAFT_BYTES : MAX_DRAFT_BYTES
82
139
  if (draft.length === 0 || draft.length > maxDraftBytes) throw new Error('DRAFT_SIZE')
83
140
  let answer
@@ -94,25 +151,22 @@ async function publish(args) {
94
151
  if (at < 0) throw new Error('TASK_INPUT')
95
152
  const refs = JSON.parse(taskText.slice(at + marker.length))
96
153
  if (JSON.stringify(refs) !== JSON.stringify(descriptor.unitRefs)) throw new Error('BINDING')
154
+ const { resolvePrivacySources } = await validator()
155
+ answer.terms = resolvePrivacySources(answer.terms, descriptor.source)
97
156
  validatePrivacyAnswer(answer, refs)
98
- // The canonical answer covers every assigned unit (ADR 0178).
99
- answer = { ...answer, units: refs }
100
- const sourceHash = createHash('sha256').update(JSON.stringify(descriptor.source)).digest('hex')
101
- if (
102
- sourceHash !== descriptor.sourceHash ||
103
- !taskText.includes(`\nSOURCE_HASH\n${sourceHash}\n`)
104
- )
105
- throw new Error('SOURCE_BINDING')
157
+ const coverage = await verifyPackCoverage(taskPath, answer, true)
158
+ answer = { ...answer, units: coverage.refs, attemptId: coverage.attemptId }
106
159
  const { locateTerms } = await validator()
107
160
  const terms = readAnswer([answer]).terms
108
161
  const { unlocatedIndices } = locateTerms(descriptor.source, terms)
109
162
  if (unlocatedIndices.length) {
110
163
  const diagnosticPath = taskPath.replace(/\.md$/u, '.publish-rejected.json')
111
- await save(diagnosticPath, { taskHash, terms, unlocatedIndices })
164
+ const draftIndices = draftTermIndices(answer.terms, terms, unlocatedIndices)
165
+ await save(diagnosticPath, { taskHash, draftIndices, unlocatedIndices })
112
166
  throw new Error('INVALID_REVIEW_SPAN', {
113
167
  cause: [
114
168
  `normalized terms[${unlocatedIndices.slice(0, 32).join(',')}] are absent from review fields or are not admitted literals`,
115
- `read the private normalized terms and complete failing indices at ${diagnosticPath}; repair the draft before publishing`,
169
+ `read the original draft indices and normalized indices at ${diagnosticPath}; repair the draft before publishing`,
116
170
  ],
117
171
  })
118
172
  }
@@ -249,6 +303,10 @@ async function publish(args) {
249
303
  answer.evidenceRootKeys.some((key) => !keys.includes(key))
250
304
  )
251
305
  diagnostics.push('evidenceRootKeys must be unique assigned roots')
306
+ else if (taskInput.evidenceRound === 1 && answer.evidenceRootKeys.length)
307
+ diagnostics.push(
308
+ 'This is the final evidence round. Set evidenceRootKeys to [] and record remaining uncertainty in groupingBasis.',
309
+ )
252
310
  if (diagnostics.length) throw new Error('INVALID_CLASSIFICATION_ANSWER', { cause: diagnostics })
253
311
  } else if (Array.isArray(taskInput.projects)) {
254
312
  const diagnostics = []
@@ -6,6 +6,7 @@ import { task, taskTokens } from './journey-tasks.mjs'
6
6
  import {
7
7
  validateConsolidationAnswer,
8
8
  compileAssignments,
9
+ validateClassificationProject,
9
10
  } from './journey-classification-validation.mjs'
10
11
 
11
12
  const metadataBudget = 12000
@@ -37,36 +38,32 @@ export async function createMetadataTasks(directory, manifest) {
37
38
  if (group.targetProjectId) continue
38
39
  if (group.groupKind === 'unsorted' || (group.groupKind === 'topic' && counts.get(id) < 3))
39
40
  unsortedRootKeys.push(row[0])
40
- else byGroup.set(id, [...(byGroup.get(id) ?? []), row[0]])
41
+ else {
42
+ if (!byGroup.has(id)) byGroup.set(id, [])
43
+ byGroup.get(id).push(row[0])
44
+ }
41
45
  }
42
46
  const metadataTasks = consolidation.metadataTasks ?? []
43
47
  const made = new Set(metadataTasks.flatMap((item) => item.projectIds ?? []))
44
48
  const pendingProjects = []
45
- for (const group of answer.groups) {
46
- if (group.groupKind === 'unsorted' || group.targetProjectId) continue
47
- const rootKeys = byGroup.get(group.id) ?? []
48
- if (!rootKeys.length || made.has(group.id)) continue
49
- const roots = rootKeys.map((rootKey) => {
50
- const row = consolidation.candidates
51
- .flatMap((candidate) => candidate.roots)
52
- .find((entry) => entry[0] === rootKey)
53
- if (!row) throw new Error('CLASSIFICATION_JUDGMENT_COVERAGE_INVALID')
54
- return { rootKey, workIntent: row[1], groupingBasis: row[2] }
55
- })
56
- pendingProjects.push({ projectId: group.id, projectName: group.projectName, roots })
57
- }
58
- if (unsortedRootKeys.length && !made.has('unsorted')) {
59
- const roots = unsortedRootKeys.map((rootKey) => {
60
- const row = consolidation.candidates
61
- .flatMap((candidate) => candidate.roots)
62
- .find((entry) => entry[0] === rootKey)
63
- if (!row) throw new Error('CLASSIFICATION_JUDGMENT_COVERAGE_INVALID')
64
- return { rootKey, workIntent: row[1], groupingBasis: row[2] }
49
+ const rows = new Map(
50
+ consolidation.candidates.flatMap(({ roots }) => roots.map((row) => [row[0], row])),
51
+ )
52
+ const add = (projectId, projectName, keys) => {
53
+ if (!keys?.length || made.has(projectId)) return
54
+ const roots = keys.map((rootKey) => {
55
+ const [, workIntent, groupingBasis] = rows.get(rootKey)
56
+ return { rootKey, workIntent, groupingBasis }
65
57
  })
66
- pendingProjects.push({ projectId: 'unsorted', projectName: 'Unsorted', roots })
58
+ pendingProjects.push({ projectId, projectName, roots })
67
59
  }
60
+ for (const group of answer.groups)
61
+ if (group.groupKind !== 'unsorted' && !group.targetProjectId)
62
+ add(group.id, group.projectName, byGroup.get(group.id))
63
+ add('unsorted', 'Unsorted', unsortedRootKeys)
68
64
  const prompt =
69
- 'Describe each retained new Work using concrete actions and objects from member workIntent. Return {items:[{projectId,description,projectType,recommendation:{access,reason}}]}; include every supplied projectId exactly once. Write one concise synthesized description (<=1000 chars), not copied titles or grouping boilerplate. Reasons (<=200 chars) explain the visibility recommendation using supplied grouping evidence. Membership and projectName are fixed. Recommend drop only when evidence shows the Work has no useful retained purpose; heterogeneous membership, uncertainty or incomplete evidence alone is not enough. Public means visible on their Tacitt profile after privacy masking; Private stays hidden but still counts. Recommend public for showcase work that demonstrates the person’s own skills, such as side projects, open-source or public repositories, coursework, hackathons, personal sites and tools, unless evidence ties it to employer or client confidentiality, internal systems or sensitive personal matters; recommend private for those. Missing privacy evidence alone is not a reason for private. Explain uncertainty in the reason. Types: work, side, coursework, personal, scratch, other; access: public, private, drop. This proposal is not approval.'
65
+ 'Describe each retained new Work using concrete actions and objects from member workIntent. Return {items:[{projectId,description,projectType,recommendation:{access,reason}}]}; include every supplied projectId exactly once. Write one concise synthesized description (<=1000 chars), not copied titles or grouping boilerplate. Reasons (<=200 chars) explain the visibility recommendation using supplied grouping evidence. When partialSummaries are supplied, synthesize across all slices and weigh conflicting visibility evidence; do not vote by slice count. Membership and projectName are fixed. Recommend drop only when evidence shows the Work has no useful retained purpose; heterogeneous membership, uncertainty or incomplete evidence alone is not enough. Public means visible on their Tacitt profile after privacy masking; Private stays hidden but still counts. Recommend public for showcase work that demonstrates the person’s own skills, such as side projects, open-source or public repositories, coursework, hackathons, personal sites and tools, unless evidence ties it to employer or client confidentiality, internal systems or sensitive personal matters; recommend private for those. Missing privacy evidence alone is not a reason for private. Explain uncertainty in the reason. Types: work, side, coursework, personal, scratch, other; access: public, private, drop. This proposal is not approval.'
66
+ const partials = []
70
67
  const batches = []
71
68
  const taskOffset = metadataTasks.length
72
69
  let batch = []
@@ -74,8 +71,11 @@ export async function createMetadataTasks(directory, manifest) {
74
71
  taskTokens(directory, `metadata-${digest}-${taskOffset + batches.length + 1}`, prompt, {
75
72
  projects,
76
73
  }) <= metadataBudget
77
- for (const project of pendingProjects) {
78
- if (!batchFits([project])) throw new Error('CLASSIFICATION_METADATA_TOO_LARGE')
74
+ for (let project of pendingProjects) {
75
+ if (!batchFits([project])) {
76
+ project = await reduceMetadata(directory, digest, prompt, project, partials)
77
+ if (!project) continue
78
+ }
79
79
  if (batch.length && !batchFits([...batch, project])) {
80
80
  batches.push(batch)
81
81
  batch = []
@@ -100,7 +100,7 @@ export async function createMetadataTasks(directory, manifest) {
100
100
  }
101
101
  consolidation.metadataTasks = metadataTasks
102
102
  await save(join(directory, 'manifest.json'), manifest)
103
- const pending = []
103
+ const pending = [...partials]
104
104
  for (const item of metadataTasks) if (!(await optional(item.answer))) pending.push(item)
105
105
  return {
106
106
  stage: 'classification',
@@ -109,3 +109,52 @@ export async function createMetadataTasks(directory, manifest) {
109
109
  tasks: pending,
110
110
  }
111
111
  }
112
+
113
+ // Reduce only when a single Work exceeds the assignment budget. Each task sees every
114
+ // row in its slice; only the final judgment is ingested, with membership still fixed.
115
+ async function reduceMetadata(directory, digest, prompt, project, pending) {
116
+ const { projectId, projectName } = project
117
+ const partialPrompt =
118
+ prompt +
119
+ ' This is one partial evidence slice of the same fixed Work. Summarize concrete work and any privacy-relevant evidence for the final reviewer, including uncertainty and conflicts. Do not infer whole-Work visibility from an unrepresentative slice.'
120
+ const partialId = (data) =>
121
+ 'metadata-slice-' +
122
+ createHash('sha256').update(digest).update(JSON.stringify(data)).digest('hex')
123
+ let entries = project.roots
124
+ let field = 'roots'
125
+ const data = (slice) => ({ projects: [{ projectId, projectName, [field]: slice }] })
126
+ const fits = (slice) =>
127
+ taskTokens(directory, partialId(data(slice)), partialPrompt, data(slice)) <= metadataBudget
128
+ for (;;) {
129
+ const slices = []
130
+ let slice = []
131
+ for (const entry of entries) {
132
+ if (!fits([entry])) throw new Error('CLASSIFICATION_METADATA_TOO_LARGE')
133
+ if (slice.length && !fits([...slice, entry])) {
134
+ slices.push(slice)
135
+ slice = []
136
+ }
137
+ slice.push(entry)
138
+ }
139
+ slices.push(slice)
140
+ const summaries = []
141
+ for (const slice of slices) {
142
+ const input = data(slice)
143
+ const item = await task(directory, partialId(input), partialPrompt, input)
144
+ const answer = await optional(item.answer)
145
+ if (!answer) {
146
+ pending.push(item)
147
+ continue
148
+ }
149
+ if (answer.items?.length !== 1) throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
150
+ const summary = answer.items[0]
151
+ validateClassificationProject(summary, projectId)
152
+ summaries.push(summary)
153
+ }
154
+ if (summaries.length !== slices.length) return null
155
+ field = 'partialSummaries'
156
+ if (fits(summaries)) return data(summaries).projects[0]
157
+ if (summaries.length >= entries.length) throw new Error('CLASSIFICATION_METADATA_TOO_LARGE')
158
+ entries = summaries
159
+ }
160
+ }
@@ -103,8 +103,7 @@ export async function publishClassificationProgress(
103
103
  requestedEvidence.some((key) => !item.rootKeys.includes(key))
104
104
  )
105
105
  throw new Error('INVALID_EVIDENCE_ROOTS')
106
- if (item.evidenceRound > 0 && requestedEvidence.length)
107
- throw new Error('EVIDENCE_ESCALATION_LIMIT')
106
+ if (item.evidenceRound > 0 && requestedEvidence.length) continue
108
107
  const keys = judgments.map((judgment) => judgment.rootKey)
109
108
  if (
110
109
  keys.length !== item.rootKeys.length ||
@@ -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
+ }
@@ -15,6 +15,7 @@ export async function classificationEvidence(directory, manifest, view, call, li
15
15
  const evidenceInstruction = `${classifyInstruction} This is the one bounded evidence review for each supplied family; replace each prior judgment. Structural evidence and selected excerpts are partial and do not establish privacy coverage. Return no further evidence requests. If ownership remains unclear, retain the best supported identity/work intent and state the unresolved question in groupingBasis; do not invent project identity. Final reconciliation can retain this family in Unsorted.`
16
16
  const fits = (families) =>
17
17
  taskTokens(directory, `evidence-${families[0].rootKey}`, evidenceInstruction, {
18
+ evidenceRound: 1,
18
19
  knownProjects: manifest.classification.projects.map(({ projectId, projectName }) => ({
19
20
  projectId,
20
21
  projectName,
@@ -28,6 +29,7 @@ export async function classificationEvidence(directory, manifest, view, call, li
28
29
  `evidence-${batch[0].rootKey}`,
29
30
  evidenceInstruction,
30
31
  {
32
+ evidenceRound: 1,
31
33
  knownProjects: manifest.classification.projects.map(({ projectId, projectName }) => ({
32
34
  projectId,
33
35
  projectName,
@@ -132,7 +134,10 @@ export async function classificationEvidence(directory, manifest, view, call, li
132
134
  await flush()
133
135
  const pendingEvidence = []
134
136
  for (const item of manifest.tasks) {
135
- if (item.evidenceRound > 0 && !(await optional(item.answer))) pendingEvidence.push(item)
137
+ if (item.evidenceRound !== 1) continue
138
+ const response = await optional(item.answer)
139
+ // Keep an invalid prior publication available for repair; never fetch another round.
140
+ if (!response || response.evidenceRootKeys?.length) pendingEvidence.push(item)
136
141
  }
137
142
  let allAnswered = true
138
143
  for (const item of manifest.tasks)
@@ -18,6 +18,7 @@ function dispatchDescriptor(item) {
18
18
  id: item.id,
19
19
  path: item.path,
20
20
  answer: item.answer,
21
+ draft: item.draft,
21
22
  descriptor: item.descriptor,
22
23
  prompt: item.prompt,
23
24
  inputHash: item.inputHash,
@@ -4,6 +4,7 @@ export interface ScanTimingObservation {
4
4
  id: string
5
5
  dispatchedAt: number | string
6
6
  publishedAt: number | null
7
+ confirmedClosedAt?: number | null
7
8
  }[]
8
9
  activeCardChildren: number
9
10
  queuedBatches: number
@@ -201,7 +201,7 @@ export async function observeScanTiming(run, observation) {
201
201
  observationBasis: {
202
202
  dispatch: 'helper-reservation',
203
203
  publication: 'answer-file-mtime',
204
- slotRelease: 'helper-observed-publication-or-retirement',
204
+ slotRelease: 'confirmed-native-close-when-bound-otherwise-helper-observation',
205
205
  cards: 'parent-reported-count-at-helper-boundary',
206
206
  preparation: 'helper-observed-interval',
207
207
  lockWait: 'unavailable-in-rpc',
@@ -215,6 +215,7 @@ export async function observeScanTiming(run, observation) {
215
215
  dispatchedAt:
216
216
  typeof pack.dispatchedAt === 'number' ? pack.dispatchedAt : Date.parse(pack.dispatchedAt),
217
217
  publishedAt: pack.publishedAt ?? null,
218
+ ...(pack.confirmedClosedAt !== undefined ? { confirmedClosedAt: pack.confirmedClosedAt } : {}),
218
219
  }))
219
220
  for (const pack of packs) {
220
221
  let child = slots.children.find((item) => item.id === pack.id)
@@ -234,7 +235,11 @@ export async function observeScanTiming(run, observation) {
234
235
  }
235
236
  if (pack.publishedAt !== null) {
236
237
  child.publishedAt = pack.publishedAt
237
- child.freedAt ??= at
238
+ if (pack.confirmedClosedAt === undefined) child.freedAt ??= at
239
+ }
240
+ if (pack.confirmedClosedAt !== undefined) {
241
+ child.confirmedClosedAt = pack.confirmedClosedAt
242
+ if (pack.confirmedClosedAt !== null) child.freedAt ??= pack.confirmedClosedAt
238
243
  }
239
244
  }
240
245
  // A retired/cancelled child also frees a slot, without claiming it published an answer.
@@ -246,13 +251,23 @@ export async function observeScanTiming(run, observation) {
246
251
  slots.busyChildMs += elapsed * last.activeCardChildren
247
252
  for (const id of last.activePackIds) {
248
253
  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)
254
+ if (child)
255
+ slots.busyChildMs += Math.max(
256
+ 0,
257
+ Math.min(
258
+ at,
259
+ (child.confirmedClosedAt !== undefined ? child.freedAt : child.publishedAt) ?? at,
260
+ ) - last.at,
261
+ )
250
262
  }
251
263
  }
252
264
  slots.occupancy = slots.waitingBatchWallMs
253
265
  ? slots.busyChildMs / (slots.capacity * slots.waitingBatchWallMs)
254
266
  : null
255
- const activePacks = packs.filter((pack) => pack.publishedAt === null)
267
+ const activePacks = packs.filter(
268
+ (pack) =>
269
+ (pack.confirmedClosedAt !== undefined ? pack.confirmedClosedAt : pack.publishedAt) === null,
270
+ )
256
271
  const batchesWaiting = observation.queuedBatches > 0 || observation.unreadBatches > 0
257
272
  measurements.seenBatch ||= batchesWaiting || packs.length > 0
258
273
  const phase = !measurements.seenBatch
@@ -35,11 +35,25 @@ type SkipRuleWork = {
35
35
  access: string
36
36
  rootSessionIds: readonly string[]
37
37
  placement?: string
38
+ inputRevision?: number
39
+ reviewInterruptions?: readonly { inputRevision: number }[] | undefined
38
40
  status?: string
39
41
  blockedCodes?: readonly string[]
40
42
  }
41
- export function isSkipped(work: Pick<SkipRuleWork, 'status' | 'blockedCodes'>): boolean
43
+ export function isSkipped(
44
+ work: Pick<SkipRuleWork, 'status' | 'blockedCodes' | 'inputRevision' | 'reviewInterruptions'>,
45
+ ): boolean
42
46
  export function isIncluded(work: SkipRuleWork): boolean
47
+ export function leftOutField(view: { works: readonly SkipRuleWork[] }): {
48
+ leftOutSessions?: number
49
+ }
50
+ export function hasPublicChoice(work: {
51
+ access: string
52
+ publicSource?: 'account' | 'choice' | 'suggestion' | null
53
+ accessConfirmed?: boolean
54
+ accessOrigin?: string
55
+ accessChoice?: unknown
56
+ }): boolean
43
57
  export function definiteWriteRejection(error: unknown): boolean
44
58
  export function stageFingerprint(view: unknown, stage: string): string
45
59
  export function planResolutions(