@attalabs/vinaya 0.22.0 → 0.23.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/README.md +13 -3
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +26 -20
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +10 -7
- package/aeg-root/tranche-model.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +552 -139
- package/dist/checks/bin/check-branch-topology.js +603 -144
- package/dist/checks/bin/check-brief-shape.js +593 -142
- package/dist/checks/bin/check-changeset-coverage.js +935 -161
- package/dist/checks/bin/check-closes-n.js +603 -144
- package/dist/checks/bin/check-coherence.js +614 -147
- package/dist/checks/bin/check-dead-branch-push.js +552 -139
- package/dist/checks/bin/check-dispatch-readiness.js +612 -145
- package/dist/checks/bin/check-doc-coverage-push.js +689 -218
- package/dist/checks/bin/check-doc-coverage.js +689 -218
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +684 -146
- package/dist/checks/bin/check-evidence-fresh.js +994 -174
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +603 -144
- package/dist/checks/bin/check-issue-assignment.js +603 -144
- package/dist/checks/bin/check-main-branch-refusal.js +552 -139
- package/dist/checks/bin/check-no-disk-state.js +552 -139
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +621 -155
- package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
- package/dist/checks/bin/check-registry-gates.js +595 -144
- package/dist/checks/bin/check-retired-vocabulary.js +617 -154
- package/dist/checks/bin/check-review-gate.js +670 -141
- package/dist/checks/bin/check-single-plan-pr.js +552 -139
- package/dist/checks/bin/check-test-plan.js +553 -140
- package/dist/checks/bin/check-token-collection-wired.js +552 -139
- package/dist/checks/bin/check-token-report.js +552 -139
- package/dist/checks/bin/check-workspace-escape.js +985 -145
- package/dist/index.js +8500 -5518
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-code-transcript — the Claude Code **collection adapter** for the
|
|
3
|
+
* token-report obligation (`aeg-root/tranche-model.md` §12, layer 2).
|
|
4
|
+
*
|
|
5
|
+
* This file is host-coupled on purpose: it is the only `src/` module whose
|
|
6
|
+
* LOGIC parses a host's own data format. Others name host paths as
|
|
7
|
+
* classification patterns — `file-classify.ts` matches `.claude/skills/` and
|
|
8
|
+
* `CLAUDE.md` — which is a different and weaker kind of coupling.
|
|
9
|
+
* It knows one harness's session-transcript format: JSONL, one object per
|
|
10
|
+
* line, assistant turns carrying a `message.usage` object whose fields are
|
|
11
|
+
* named `input_tokens` / `output_tokens` / `cache_creation_input_tokens` /
|
|
12
|
+
* `cache_read_input_tokens`. None of that is doctrine — it is one vendor's
|
|
13
|
+
* on-disk shape, and an adopter on another harness replaces this module
|
|
14
|
+
* wholesale rather than configuring it.
|
|
15
|
+
*
|
|
16
|
+
* **The seam is `TranscriptSummary`, not the file tree.** An adapter's entire
|
|
17
|
+
* contract is to produce that shape (four integers plus a model id) from
|
|
18
|
+
* whatever its own host exposes. Everything downstream of it — the
|
|
19
|
+
* `formatTokensLine` renderer in `report-tokens.ts`, the `Tokens: …` grammar
|
|
20
|
+
* in `parse-token-report.ts` — is portable and shared by every host. The
|
|
21
|
+
* `bin/` vs `src/` split does NOT mark this boundary and never did:
|
|
22
|
+
* `bin/report-tokens.ts` is equally host-coupled (it knows how Claude Code
|
|
23
|
+
* points a session at its own transcript), while `src/parse-token-report.ts`
|
|
24
|
+
* is fully portable. Splitting this module out of `report-tokens.ts` is what
|
|
25
|
+
* makes the boundary legible in the tree rather than only in prose — the
|
|
26
|
+
* package ships inside the public tarball, where a reader has no other way to
|
|
27
|
+
* tell which half they may reuse.
|
|
28
|
+
*
|
|
29
|
+
* Pure — no `fs`, no `process.env`, per this package's purity charter. The
|
|
30
|
+
* CLI shim reads the file; this takes its text.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { createHash } from 'node:crypto'
|
|
34
|
+
import type { TranscriptSummary, UsageComponents } from './report-tokens'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sum usage across every unique assistant message in a session transcript
|
|
38
|
+
* (JSONL — one object per line). A single API turn is frequently split
|
|
39
|
+
* across several JSONL entries (a thinking block, a tool_use block, a text
|
|
40
|
+
* block, …) that each carry an identical copy of that turn's `usage` object
|
|
41
|
+
* under the same `message.id` — confirmed against a live transcript, where
|
|
42
|
+
* 59 assistant-typed lines held only 22 unique message ids. Summing every
|
|
43
|
+
* line naively over-counts by as much as 3x. Dedup by `message.id` first; a
|
|
44
|
+
* line missing an id or a usage object is skipped, not guessed at.
|
|
45
|
+
*/
|
|
46
|
+
export function summarizeTranscript(jsonl: string): TranscriptSummary {
|
|
47
|
+
const seen = new Set<string>()
|
|
48
|
+
const components: UsageComponents = {
|
|
49
|
+
inputTokens: 0,
|
|
50
|
+
outputTokens: 0,
|
|
51
|
+
cacheCreationInputTokens: 0,
|
|
52
|
+
cacheReadInputTokens: 0
|
|
53
|
+
}
|
|
54
|
+
let model: string | null = null
|
|
55
|
+
let messageCount = 0
|
|
56
|
+
|
|
57
|
+
for (const rawLine of jsonl.split(/\r?\n/)) {
|
|
58
|
+
const line = rawLine.trim()
|
|
59
|
+
if (!line) continue
|
|
60
|
+
|
|
61
|
+
let entry: unknown
|
|
62
|
+
try {
|
|
63
|
+
entry = JSON.parse(line)
|
|
64
|
+
} catch {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (!entry || typeof entry !== 'object') continue
|
|
68
|
+
const obj = entry as Record<string, unknown>
|
|
69
|
+
if (obj.type !== 'assistant') continue
|
|
70
|
+
|
|
71
|
+
const message = obj.message as Record<string, unknown> | undefined
|
|
72
|
+
const id = message?.id
|
|
73
|
+
const usage = message?.usage as Record<string, unknown> | undefined
|
|
74
|
+
if (typeof id !== 'string' || !id || !usage || seen.has(id)) continue
|
|
75
|
+
seen.add(id)
|
|
76
|
+
messageCount++
|
|
77
|
+
|
|
78
|
+
components.inputTokens += numberOr(usage.input_tokens, 0)
|
|
79
|
+
components.outputTokens += numberOr(usage.output_tokens, 0)
|
|
80
|
+
components.cacheCreationInputTokens += numberOr(usage.cache_creation_input_tokens, 0)
|
|
81
|
+
components.cacheReadInputTokens += numberOr(usage.cache_read_input_tokens, 0)
|
|
82
|
+
|
|
83
|
+
const messageModel = message?.model
|
|
84
|
+
if (typeof messageModel === 'string' && messageModel) model = messageModel
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { components, model, messageCount }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function numberOr(value: unknown, fallback: number): number {
|
|
91
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `resolveMeteringCapability`'s I/O — injected, never imported directly, per
|
|
96
|
+
* this package's purity charter (`fs`/`process.env` stay out of `src/`). The
|
|
97
|
+
* real caller (`apps/cli`'s `tokens`/`doctor` commands) supplies
|
|
98
|
+
* `existsSync`/`readFileSync`/`process.env`/`process.cwd()`; tests supply
|
|
99
|
+
* fakes.
|
|
100
|
+
*/
|
|
101
|
+
export type MeteringCapabilityDeps = {
|
|
102
|
+
env: Record<string, string | undefined>
|
|
103
|
+
cwd: string
|
|
104
|
+
exists: (path: string) => boolean
|
|
105
|
+
readFile: (path: string) => string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type MeteringIncapableReason =
|
|
109
|
+
| 'no-transcript-resolved'
|
|
110
|
+
| 'pointer-unusable'
|
|
111
|
+
| 'transcript-unreadable'
|
|
112
|
+
| 'transcript-empty'
|
|
113
|
+
|
|
114
|
+
export type MeteringCapability =
|
|
115
|
+
| { capable: true; transcriptPath: string; summary: TranscriptSummary }
|
|
116
|
+
| { capable: false; reason: MeteringIncapableReason; detail: string }
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Collapses every run of non-alphanumeric characters to a single `-`. Kept
|
|
120
|
+
* (unchanged) as the READABLE prefix of a pointer key and as the sole
|
|
121
|
+
* derivation for a pre-migration (legacy) pointer filename — see
|
|
122
|
+
* `legacyTranscriptPointerPath`. On its own it is not collision-resistant:
|
|
123
|
+
* `/a/b` and `/a-b` both collapse to `-a-b` (`#315`).
|
|
124
|
+
*/
|
|
125
|
+
function sanitizeKey(value: string): string {
|
|
126
|
+
return value.replace(/[^A-Za-z0-9]+/g, '-')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Collision-resistant (`#315`). `sanitizeKey` alone collapses distinct paths
|
|
131
|
+
* that differ only in which non-alphanumeric characters they use — a full
|
|
132
|
+
* SHA-256 digest of the UNCOLLAPSED, UNTRUNCATED original `value` restores
|
|
133
|
+
* distinctness: two different `value`s can share a `sanitizeKey` prefix, but
|
|
134
|
+
* cannot share this digest without an actual SHA-256 collision, which is
|
|
135
|
+
* cryptographically negligible regardless of how many paths ever run through
|
|
136
|
+
* this on one machine. The digest is not truncated — truncating to, say, 64
|
|
137
|
+
* bits would trade that guarantee for a birthday-bound one; the two extra
|
|
138
|
+
* lines of filename length buy a strictly stronger property, and a pointer
|
|
139
|
+
* filename is never displayed to a person who needs it short. `sanitizeKey`'s
|
|
140
|
+
* output is kept as a prefix purely so the filename still hints at which
|
|
141
|
+
* project it belongs to when a human is looking at a `/tmp` listing — the
|
|
142
|
+
* digest suffix, not the prefix, is what the uniqueness guarantee rests on.
|
|
143
|
+
*/
|
|
144
|
+
function collisionResistantKey(value: string): string {
|
|
145
|
+
const digest = createHash('sha256').update(value).digest('hex')
|
|
146
|
+
return `${sanitizeKey(value)}-${digest}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Mirrors `bin/report-tokens.ts`'s own pointer-file convention (a
|
|
151
|
+
* `track-transcript.sh` Stop hook, keyed by `CLAUDE_PROJECT_DIR`) so a repo
|
|
152
|
+
* that already wires that hook gets probe support for free. Deliberately
|
|
153
|
+
* duplicated rather than imported from `bin/`: `bin/` is I/O-shim code that
|
|
154
|
+
* imports `src/`, never the reverse, and is not part of this package's
|
|
155
|
+
* published `exports` map — the whole reason this probe exists is to work
|
|
156
|
+
* where that path is unreachable.
|
|
157
|
+
*
|
|
158
|
+
* This is the PRIMARY (post-migration, `#315`) pointer path — collision-
|
|
159
|
+
* resistant. `resolvePointer` also consults `legacyTranscriptPointerPath`
|
|
160
|
+
* as a fallback, so a pointer the shipped Stop hook already wrote under the
|
|
161
|
+
* old, collision-prone name is still found rather than orphaned.
|
|
162
|
+
*/
|
|
163
|
+
function transcriptPointerPath(projectDir: string, tmpDir: string): string {
|
|
164
|
+
return `${tmpDir}/claude-transcript-${collisionResistantKey(projectDir)}.txt`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The pre-`#315` pointer filename — `sanitizeKey` alone, collision-prone.
|
|
169
|
+
* Never the primary read or write target going forward; consulted only when
|
|
170
|
+
* `transcriptPointerPath` is absent, so a pointer a not-yet-upgraded
|
|
171
|
+
* `track-transcript.sh` (or one written before this fix shipped) already has
|
|
172
|
+
* on disk is still readable. Removing this fallback would orphan every such
|
|
173
|
+
* pointer the moment this fix ships, which `#315`'s Acceptance forbids.
|
|
174
|
+
*/
|
|
175
|
+
function legacyTranscriptPointerPath(projectDir: string, tmpDir: string): string {
|
|
176
|
+
return `${tmpDir}/claude-transcript-${sanitizeKey(projectDir)}.txt`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolves a transcript path to probe — `explicitTranscriptPath` (the
|
|
181
|
+
* caller's own `--transcript`) wins outright, exactly like
|
|
182
|
+
* `bin/report-tokens.ts`'s `--transcript`. Otherwise consults the Stop-hook
|
|
183
|
+
* pointer file, applying the same `CLAUDE_CODE_SESSION_ID` staleness
|
|
184
|
+
* cross-check that adapter uses — a stale pointer left by a previous session
|
|
185
|
+
* sharing this worktree must not be silently trusted.
|
|
186
|
+
*
|
|
187
|
+
* Returns an error string rather than throwing: this is the
|
|
188
|
+
* "nothing to even try" case (Stop-and-escalate's "no transcript exists"),
|
|
189
|
+
* kept distinct in `resolveMeteringCapability` from a resolved-but-unreadable
|
|
190
|
+
* path.
|
|
191
|
+
*/
|
|
192
|
+
type PointerResolution =
|
|
193
|
+
| { path: string; corroborated: boolean }
|
|
194
|
+
/**
|
|
195
|
+
* `corroborated` — we read the pointer's session id and it is ours.
|
|
196
|
+
* `oursByLocation` — we could NOT read an id, but the file sits at this
|
|
197
|
+
* project's own pointer path and is owned by this user. Two different
|
|
198
|
+
* grounds, deliberately not one field: an earlier revision set
|
|
199
|
+
* `corroborated: Boolean(currentSessionId)` on branches where the id was
|
|
200
|
+
* never read, which asserted a match that had not been established and put
|
|
201
|
+
* three shipped docs at odds with the code.
|
|
202
|
+
*/
|
|
203
|
+
| { error: string; pointerExisted: boolean; corroborated?: boolean; oursByLocation?: boolean }
|
|
204
|
+
|
|
205
|
+
function resolvePointer(explicitTranscriptPath: string | undefined, deps: MeteringCapabilityDeps): PointerResolution {
|
|
206
|
+
// A caller-named transcript is self-corroborating: they told us which file
|
|
207
|
+
// is theirs, so there is no session to cross-check it against.
|
|
208
|
+
if (explicitTranscriptPath) return { path: explicitTranscriptPath, corroborated: true }
|
|
209
|
+
|
|
210
|
+
const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd
|
|
211
|
+
const tmpDir = deps.env.TMPDIR ?? '/tmp'
|
|
212
|
+
const primaryPointerPath = transcriptPointerPath(projectDir, tmpDir)
|
|
213
|
+
const legacyPointerPath = legacyTranscriptPointerPath(projectDir, tmpDir)
|
|
214
|
+
|
|
215
|
+
// Corroboration = we can tell this pointer belongs to THIS session. Without
|
|
216
|
+
// `CLAUDE_CODE_SESSION_ID` there is nothing to cross-check against, so a
|
|
217
|
+
// pointer left by an earlier session is indistinguishable from our own. A
|
|
218
|
+
// plain human terminal is exactly that case, and gating its commits on
|
|
219
|
+
// another session's leftovers is the false positive `#272` names as the
|
|
220
|
+
// expensive failure mode.
|
|
221
|
+
const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID
|
|
222
|
+
|
|
223
|
+
// `#315` migration: the primary (collision-resistant) path wins when both
|
|
224
|
+
// exist — it is what any Stop hook upgraded past this fix writes. The
|
|
225
|
+
// legacy path is consulted ONLY when the primary is absent, so a pointer
|
|
226
|
+
// written before this fix shipped (or by a not-yet-upgraded hook script)
|
|
227
|
+
// is still found rather than orphaned.
|
|
228
|
+
const pointerPath = deps.exists(primaryPointerPath)
|
|
229
|
+
? primaryPointerPath
|
|
230
|
+
: deps.exists(legacyPointerPath)
|
|
231
|
+
? legacyPointerPath
|
|
232
|
+
: undefined
|
|
233
|
+
|
|
234
|
+
if (!pointerPath) {
|
|
235
|
+
return {
|
|
236
|
+
pointerExisted: false,
|
|
237
|
+
corroborated: false,
|
|
238
|
+
error:
|
|
239
|
+
`No transcript pointer at ${primaryPointerPath} (nor its pre-migration name ${legacyPointerPath}) ` +
|
|
240
|
+
'and no --transcript given. Either this repo installs no track-transcript.sh Stop hook (lacking one ' +
|
|
241
|
+
'is not a defect — name the transcript directly instead), or no session has completed a turn yet.'
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let contents: string
|
|
246
|
+
try {
|
|
247
|
+
// Trailing newline only — NEVER `.trim()`. The shipped Stop hook writes
|
|
248
|
+
// `(hook.session_id || "") + "\t" + transcript_path`, so a Stop payload
|
|
249
|
+
// carrying no `session_id` produces a pointer beginning with a TAB.
|
|
250
|
+
// `.trim()` ate that leading tab, `split('\t')` then found no separator,
|
|
251
|
+
// and a pointer naming a present, readable, summarizable transcript was
|
|
252
|
+
// classified malformed — refusing every commit on a host that meters
|
|
253
|
+
// perfectly. Proven end to end against the real hook body.
|
|
254
|
+
contents = deps.readFile(pointerPath).replace(/\r?\n+$/, '')
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return {
|
|
257
|
+
pointerExisted: true,
|
|
258
|
+
// NOT a corroboration claim: an unreadable pointer's session id is never
|
|
259
|
+
// read, so nothing here can show whose it is. The refusal rests on a
|
|
260
|
+
// different and sufficient ground — this is OUR project's pointer path,
|
|
261
|
+
// holding a file WE own, which we cannot use. That is broken wiring
|
|
262
|
+
// whoever wrote it, and it is repaired by removing the file.
|
|
263
|
+
oursByLocation: true,
|
|
264
|
+
error:
|
|
265
|
+
`Transcript pointer at ${pointerPath} could not be read: ${(err as Error).message}. ` +
|
|
266
|
+
'Remove that file to clear this — the Stop hook rewrites it on the next turn.'
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const [pointerSessionId, transcriptPath] = contents.split('\t')
|
|
271
|
+
if (!transcriptPath) {
|
|
272
|
+
return {
|
|
273
|
+
pointerExisted: true,
|
|
274
|
+
// Same ground as the unreadable branch, and NOT a corroboration claim: a
|
|
275
|
+
// malformed pointer carries no parseable session id, so whose it is
|
|
276
|
+
// cannot be established. Ours by location and ownership, and unusable.
|
|
277
|
+
oursByLocation: true,
|
|
278
|
+
// Bounded: this echoes an on-disk file's contents into check output,
|
|
279
|
+
// which in the hook path reaches commit output and CI logs. The guard
|
|
280
|
+
// upstream proves the file is owned by this user, so it is not
|
|
281
|
+
// attacker-controlled — but an unbounded echo is still wrong.
|
|
282
|
+
error:
|
|
283
|
+
`Transcript pointer file ${pointerPath} is malformed: "${contents.slice(0, 120)}${contents.length > 120 ? '…' : ''}". ` +
|
|
284
|
+
'Remove that file to clear this — the Stop hook rewrites it on the next turn.'
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (currentSessionId && pointerSessionId && currentSessionId !== pointerSessionId) {
|
|
289
|
+
// `corroborated: false`, and the distinction is the whole point: the ids
|
|
290
|
+
// DISAGREE, so this pointer is provably NOT this session's. Two reviewers
|
|
291
|
+
// reached opposite conclusions here and the second is right. Treating a
|
|
292
|
+
// stale pointer as a wiring defect refuses a correctly wired host: a second
|
|
293
|
+
// session in the same project directory sees the first session's pointer
|
|
294
|
+
// until its own Stop hook fires, which by construction is only after its
|
|
295
|
+
// first turn completes — so its very first commit is blocked, and the
|
|
296
|
+
// remedy the message names (`--transcript`) is a flag `vinaya check` does
|
|
297
|
+
// not accept, leaving no action that clears the refusal.
|
|
298
|
+
//
|
|
299
|
+
// It also makes `corroborated` mean one thing rather than two. Everywhere
|
|
300
|
+
// else it answers "can we show this pointer is ours"; setting it true here
|
|
301
|
+
// made it mean "we could tell, and it wasn't", which is what put the three
|
|
302
|
+
// shipped docs at odds with the code.
|
|
303
|
+
return {
|
|
304
|
+
pointerExisted: true,
|
|
305
|
+
corroborated: false,
|
|
306
|
+
error:
|
|
307
|
+
`Transcript pointer at ${pointerPath} is stale: written for session ${pointerSessionId}, ` +
|
|
308
|
+
`but this session is ${currentSessionId}. Name your own transcript with --transcript instead of ` +
|
|
309
|
+
"reporting another session's figures as yours."
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { path: transcriptPath, corroborated: Boolean(currentSessionId && pointerSessionId) }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Probes whether this host can currently produce real token figures —
|
|
318
|
+
* capable/incapable, never declared by host identity (`vinaya doctor`'s
|
|
319
|
+
* whole reason for calling this rather than checking `process.env` itself).
|
|
320
|
+
* Distinguishes the two failure classes the Stop-and-escalate condition
|
|
321
|
+
* names: `no-transcript-resolved` (nothing to even try — no `--transcript`,
|
|
322
|
+
* no pointer file) is a different fact from `transcript-unreadable` (a path
|
|
323
|
+
* was resolved but the file can't be read) or `transcript-empty` (the file
|
|
324
|
+
* reads but summarizes to zero assistant messages — empty, unparseable, or
|
|
325
|
+
* not yet flushed). Conflating any of these into one "incapable" bit would
|
|
326
|
+
* reproduce the false "host has no usage" claim this tranche removes.
|
|
327
|
+
*
|
|
328
|
+
* `explicitTranscriptPath`, when given, wins outright over pointer-file
|
|
329
|
+
* discovery — the caller's own `--transcript` is always better evidence than
|
|
330
|
+
* any inference this function could make.
|
|
331
|
+
*/
|
|
332
|
+
export function resolveMeteringCapability(
|
|
333
|
+
deps: MeteringCapabilityDeps,
|
|
334
|
+
explicitTranscriptPath?: string
|
|
335
|
+
): MeteringCapability {
|
|
336
|
+
const resolved = resolvePointer(explicitTranscriptPath, deps)
|
|
337
|
+
if ('error' in resolved) {
|
|
338
|
+
// A pointer that EXISTS but cannot be used is a wiring defect, not an
|
|
339
|
+
// absence of wiring — but only when we can corroborate it is ours.
|
|
340
|
+
// Uncorroborated, it is indistinguishable from another session's leftover
|
|
341
|
+
// and degrades to the sanctioned operator-metered case.
|
|
342
|
+
// Refuse when the pointer is ours on EITHER ground: its id matches, or it
|
|
343
|
+
// is a file we own at our own pointer path that we cannot use. Pass when
|
|
344
|
+
// it is provably another session's (the stale branch sets neither).
|
|
345
|
+
const ours = resolved.pointerExisted && (resolved.corroborated || resolved.oursByLocation)
|
|
346
|
+
if (ours) return { capable: false, reason: 'pointer-unusable', detail: resolved.error }
|
|
347
|
+
// Degraded, so the detail is rewritten to match. An earlier revision
|
|
348
|
+
// degraded only the reason on this path, leaving a `no-transcript-resolved`
|
|
349
|
+
// verdict whose detail described a pointer that HAD resolved — the
|
|
350
|
+
// contradictory pair that reaches `vinaya doctor` and `pr report`'s token
|
|
351
|
+
// cell. The original condition is kept, marked as diagnostic.
|
|
352
|
+
return {
|
|
353
|
+
capable: false,
|
|
354
|
+
reason: 'no-transcript-resolved',
|
|
355
|
+
detail: resolved.pointerExisted
|
|
356
|
+
? `A transcript pointer exists but belongs to another session, so this session has no wiring of its own to use. (Underlying condition, for diagnosis only: ${resolved.error})`
|
|
357
|
+
: resolved.error
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Downstream transcript failures gate only on a corroborated pointer, for the
|
|
362
|
+
// same reason: a stale pointer naming a since-pruned transcript must not
|
|
363
|
+
// refuse a human's commit.
|
|
364
|
+
// Returns the reason AND a detail consistent with it. An earlier revision
|
|
365
|
+
// degraded only the reason and kept the original detail, so an incapable
|
|
366
|
+
// verdict could read `no-transcript-resolved` while its detail said a
|
|
367
|
+
// transcript HAD been resolved and was unreadable. That pair leaks past this
|
|
368
|
+
// check into `vinaya doctor` and into `pr report`'s token cell, which is the
|
|
369
|
+
// false-provenance class this whole tranche exists to remove.
|
|
370
|
+
const downstream = (
|
|
371
|
+
r: 'transcript-unreadable' | 'transcript-empty',
|
|
372
|
+
detail: string
|
|
373
|
+
): { reason: MeteringIncapableReason; detail: string } =>
|
|
374
|
+
resolved.corroborated
|
|
375
|
+
? { reason: r, detail }
|
|
376
|
+
: {
|
|
377
|
+
reason: 'no-transcript-resolved',
|
|
378
|
+
detail: `A transcript pointer was found but could not be shown to belong to this session, so it is not treated as this session's wiring. (Underlying condition, for diagnosis only: ${detail})`
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!deps.exists(resolved.path)) {
|
|
382
|
+
return {
|
|
383
|
+
capable: false,
|
|
384
|
+
// "not readable as a regular file owned by this user" rather than "does
|
|
385
|
+
// not exist": the shipped check's `exists` dep is `lstat`-hardened, so a
|
|
386
|
+
// symlinked, foreign-owned, or non-regular transcript reports false here
|
|
387
|
+
// for a path that does exist. Saying "does not exist" of such a file is
|
|
388
|
+
// both wrong and unactionable.
|
|
389
|
+
...downstream(
|
|
390
|
+
'transcript-unreadable',
|
|
391
|
+
`Resolved transcript path ${resolved.path} is not readable as a regular file owned by this user.`
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let jsonl: string
|
|
397
|
+
try {
|
|
398
|
+
jsonl = deps.readFile(resolved.path)
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return {
|
|
401
|
+
capable: false,
|
|
402
|
+
...downstream(
|
|
403
|
+
'transcript-unreadable',
|
|
404
|
+
`Transcript at ${resolved.path} could not be read: ${(err as Error).message}`
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const summary = summarizeTranscript(jsonl)
|
|
410
|
+
if (summary.messageCount === 0) {
|
|
411
|
+
return {
|
|
412
|
+
capable: false,
|
|
413
|
+
...downstream(
|
|
414
|
+
'transcript-empty',
|
|
415
|
+
`Transcript at ${resolved.path} yielded zero assistant messages with usage data — ` +
|
|
416
|
+
"it's empty, unparseable, or not yet flushed to disk."
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { capable: true, transcriptPath: resolved.path, summary }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The one fact `token-collection-wired` (task 5, #272) gates a commit on,
|
|
426
|
+
* factored out here — contract-agnostic (a plain boolean, no `CheckError`
|
|
427
|
+
* shape) — so both the shipped `apps/cli` check and this repo's own
|
|
428
|
+
* self-hosting `bin/check-token-collection-wired.ts` gate consume the SAME
|
|
429
|
+
* predicate, never two copies (the `isNewDiskStateFile` precedent).
|
|
430
|
+
*
|
|
431
|
+
* `no-transcript-resolved` means this session has no corroborated wiring to
|
|
432
|
+
* try — no pointer file at all, or a pointer it cannot show is its own. That
|
|
433
|
+
* is the sanctioned operator-metered case, never a defect.
|
|
434
|
+
*
|
|
435
|
+
* Every other reason means a pointer BOTH existed AND was corroborated as this
|
|
436
|
+
* session's, and reaching the figures still failed: `pointer-unusable` (the
|
|
437
|
+
* pointer itself is unreadable, malformed, or stale) or `transcript-unreadable`
|
|
438
|
+
* / `transcript-empty` (the path it named could not be read or held nothing).
|
|
439
|
+
* Those are the wiring defect this predicate flags.
|
|
440
|
+
*
|
|
441
|
+
* The corroboration condition is load-bearing in BOTH directions, and an
|
|
442
|
+
* earlier revision got both wrong. Without it, a pointer left in a shared
|
|
443
|
+
* `TMPDIR` by an unrelated session refuses a plain human's commit (a false
|
|
444
|
+
* positive `#272` names as the expensive failure mode); and folding every
|
|
445
|
+
* pointer failure into `no-transcript-resolved` let an unreadable, malformed,
|
|
446
|
+
* or stale pointer pass silently — the exact wired-but-unreachable state this
|
|
447
|
+
* check exists to refuse.
|
|
448
|
+
*/
|
|
449
|
+
/*
|
|
450
|
+
* Returns a plain boolean, deliberately NOT a type predicate. As a predicate it
|
|
451
|
+
* was unsound: `false` also covers `{capable: false, reason:
|
|
452
|
+
* 'no-transcript-resolved'}`, so the negative branch narrowed to `capable:
|
|
453
|
+
* true` and `cap.summary` compiled clean while throwing at runtime. That is a
|
|
454
|
+
* false capability guarantee handed to every adopter of a published package,
|
|
455
|
+
* and no caller needs the narrowing badly enough to be worth it.
|
|
456
|
+
*/
|
|
457
|
+
export function isTokenCollectionWiringBroken(capability: MeteringCapability): boolean {
|
|
458
|
+
if (capability.capable) return false
|
|
459
|
+
return capability.reason !== 'no-transcript-resolved'
|
|
460
|
+
}
|