dsh-ptc-plus 0.5.3 → 0.5.4

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 (40) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/client.cjs +3326 -2881
  4. package/compiler-core.cjs +127 -127
  5. package/compiler-core.cjs.map +2 -2
  6. package/docs/README.md +1 -0
  7. package/docs/capability-planning-components.md +6 -6
  8. package/docs/capability-projection.md +1 -1
  9. package/docs/capability-surface.md +133 -0
  10. package/docs/client-ui.md +17 -15
  11. package/docs/client-visual-contract.md +7 -7
  12. package/docs/installation.md +9 -2
  13. package/docs/panel-concept.md +171 -0
  14. package/docs/releases/v0.5.4.md +75 -0
  15. package/docs/runtime-reference.md +14 -0
  16. package/docs/verification.md +1 -1
  17. package/index.js +117 -30
  18. package/internal/binding-adoption.js +103 -11
  19. package/internal/capability-command-receipt.js +99 -15
  20. package/internal/capability-environment.js +1108 -348
  21. package/internal/capability-plan.js +18 -2
  22. package/internal/capability-policy.js +13 -0
  23. package/internal/capability-selection.js +330 -0
  24. package/internal/direct-surface-owner.js +17 -4
  25. package/internal/message-sources.js +21 -0
  26. package/internal/program-bindings.js +13 -4
  27. package/internal/runtime-bridge-owner.js +5 -1
  28. package/internal/runtime-config.js +10 -0
  29. package/internal/session-log-view.js +47 -5
  30. package/internal/settings-compat.js +6 -0
  31. package/internal/user-bindings-owner.js +20 -3
  32. package/package.json +45 -31
  33. package/src/client-authoring-view.js +7 -8
  34. package/src/client-capabilities-view.js +871 -948
  35. package/src/client-capability-sort.js +22 -9
  36. package/src/client-copy.js +116 -294
  37. package/src/client-settings-view.js +90 -33
  38. package/src/client-styles.js +67 -19
  39. package/src/client.js +46 -21
  40. package/internal/capability-delivery-proof.js +0 -70
@@ -1,5 +1,5 @@
1
1
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
2
- import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
2
+ import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND, RETIRED_SURFACE_RECORD } from './message-sources.js'
3
3
 
4
4
  /**
5
5
  * Session binding adoption: which Global User Binding catalog entries this
@@ -23,13 +23,28 @@ import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.j
23
23
  * `automatic`, which means "the host derives this value". A binding has no host
24
24
  * derivation to defer to, so borrowing that word would misdescribe both.
25
25
  *
26
- * The decision is recorded as one PTC message in the session log, so it is
26
+ * The decision is recorded as a plugin record in the session log, so it is
27
27
  * reconstructed by replaying that log after a restart instead of being kept in
28
- * process memory.
28
+ * process memory. The message form is the historical carrier that 0.5.4 and
29
+ * earlier wrote; it is still read (those sessions keep their decision, and a
30
+ * repaired statement answers through it), and no writer produces it.
29
31
  */
30
32
  export const BINDING_ADOPTION_FORM = 'binding-adoption'
31
33
  export const BINDING_ADOPTION_SCHEMA = 'ptc-binding-adoption/v1'
32
34
 
35
+ /**
36
+ * The log record this session's adoption is written as.
37
+ *
38
+ * Adoption is a decision about this session's own environment: the catalog the
39
+ * model reads already reflects it, so the decision needs no message of its own.
40
+ * Writing it as a record makes it durable and immediate — it is taken the moment
41
+ * the control is used, before any step, without entering the model-visible surface
42
+ * (a surface node ahead of the host's protected head would make the log
43
+ * unreadable to every reader). A host that publishes no plugin-record writer
44
+ * refuses the write instead of falling back to that surface node.
45
+ */
46
+ export const BINDING_ADOPTION_RECORD = 'plugin:ptc-plus/binding-adoption'
47
+
33
48
  const MAX_DECLINED = 64
34
49
  const ENTRY_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
35
50
  const PREFIX = 'PTC Plus session binding adoption. This session uses every entry the global catalog supplies except the declined ones. Adoption can only withdraw an entry from this session: it cannot supply, enable or revive one, and the global configuration decides which entries exist at all. Declined entry IDs:'
@@ -65,6 +80,7 @@ export function bindingAdoptionText(adoption) {
65
80
  return `${PREFIX}\n${JSON.stringify(normalizeBindingAdoption(adoption).declined)}`
66
81
  }
67
82
 
83
+ /** The historical message this adoption was written as before the record carrier. */
68
84
  export function bindingAdoptionMessage(adoption) {
69
85
  const text = bindingAdoptionText(adoption)
70
86
  const message = createUserMessage({
@@ -92,24 +108,100 @@ export function readBindingAdoption(value) {
92
108
  }
93
109
 
94
110
  /**
95
- * The newest adoption this session recorded, or `undefined` while it recorded
96
- * none. The newest record replaces earlier ones, because stating a decision
97
- * again is how a session changes its mind.
111
+ * How this session's newest adoption statement reads.
112
+ *
113
+ * `none` is the absence of a decision, `decided` is a statement this build can
114
+ * read, and `unreadable` is a statement it cannot. The third state is not the
115
+ * first: an unreadable newest statement still supersedes every earlier record,
116
+ * so no consumer may answer from the older one in its place. `latestBindingAdoption`
117
+ * collapses the last two into `undefined`, which is why the panel asks this
118
+ * function instead: `undefined` there means "no decision to apply", not "the
119
+ * session declined nothing".
98
120
  */
99
- export function latestBindingAdoption(records) {
100
- if (!Array.isArray(records)) return undefined
121
+ export function classifyBindingAdoption(records) {
122
+ if (!Array.isArray(records)) return Object.freeze({ state: 'none' })
101
123
  for (let index = records.length - 1; index >= 0; index -= 1) {
102
124
  const record = records[index]
103
125
  if (record?.form !== BINDING_ADOPTION_FORM) continue
104
126
  try {
105
- return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: record.declined })
127
+ return Object.freeze({ state: 'decided',
128
+ adoption: normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: record.declined }) })
106
129
  } catch {
107
130
  // A record that cannot be read is not evidence for an earlier record
108
131
  // either: the newest statement is the current one, and it is unreadable.
109
- return undefined
132
+ return Object.freeze({ state: 'unreadable' })
110
133
  }
111
134
  }
112
- return undefined
135
+ return Object.freeze({ state: 'none' })
136
+ }
137
+
138
+ /** Read one committed plugin record back as this session's adoption, or nothing. */
139
+ export function readBindingAdoptionRecord(event) {
140
+ if (event?.type !== BINDING_ADOPTION_RECORD || event.ignorable !== true) return undefined
141
+ try {
142
+ return normalizeBindingAdoption(event.data)
143
+ } catch {
144
+ return undefined
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Whether one event states this session's adoption at all.
150
+ *
151
+ * The record type states it, and so does the historical message form. A repaired statement
152
+ * (`RETIRED_SURFACE_RECORD`) states it only when the payload it kept is this owner's own form:
153
+ * the repair retires every surface node ahead of the protected head, whichever owner wrote it,
154
+ * so the carrier alone says nothing about who the statement belongs to.
155
+ */
156
+ function statesBindingAdoption(event) {
157
+ if (event?.type === BINDING_ADOPTION_RECORD) return true
158
+ const source = event?.data?.source
159
+ return isPtcMessageSource(source) && source.form === BINDING_ADOPTION_FORM
160
+ }
161
+
162
+ /**
163
+ * Read one adoption out of either carrier, so a reader never has to know which release wrote it —
164
+ * and a repaired statement answers for the owner whose form it carries.
165
+ */
166
+ export function readBindingAdoptionStatement(event) {
167
+ if (event?.type === BINDING_ADOPTION_RECORD) return readBindingAdoptionRecord(event)
168
+ if (event?.type === RETIRED_SURFACE_RECORD) {
169
+ return event.ignorable === true && statesBindingAdoption(event) ? readBindingAdoption(event.data) : undefined
170
+ }
171
+ return event?.type === 'user/message' ? readBindingAdoption(event.data) : undefined
172
+ }
173
+
174
+ /**
175
+ * This session's newest adoption statement, read from its own log events.
176
+ *
177
+ * The log is the owner of the decision, so every adoption statement it carries — a record, the
178
+ * message form an older release wrote, or a repaired statement of the same form — is classified,
179
+ * including one this build cannot read. Extracting the statements and classifying them here keeps
180
+ * every reader on the same cut: a reader that filtered to the readable statements first would hand
181
+ * a superseded record the answer, and the panel and the `/binding` command would then disagree
182
+ * about which record governs this session.
183
+ */
184
+ export function bindingAdoptionOf(events) {
185
+ const records = []
186
+ for (const event of Array.isArray(events) ? events : []) {
187
+ if (event?.type !== 'user/message' && event?.type !== BINDING_ADOPTION_RECORD
188
+ && event?.type !== RETIRED_SURFACE_RECORD) continue
189
+ // A repair retires every owner's surface node, so only a statement that kept this owner's
190
+ // form joins the ordering; another owner's retired node is not an adoption at all.
191
+ if (!statesBindingAdoption(event)) continue
192
+ records.push({ form: BINDING_ADOPTION_FORM, declined: readBindingAdoptionStatement(event)?.declined })
193
+ }
194
+ return classifyBindingAdoption(records)
195
+ }
196
+
197
+ /**
198
+ * The newest adoption this session recorded, or `undefined` while it recorded
199
+ * none this build can read. The newest record replaces earlier ones, because
200
+ * stating a decision again is how a session changes its mind.
201
+ */
202
+ export function latestBindingAdoption(records) {
203
+ const statement = classifyBindingAdoption(records)
204
+ return statement.state === 'decided' ? statement.adoption : undefined
113
205
  }
114
206
 
115
207
  /**
@@ -1,6 +1,14 @@
1
- /** Structured command outcome for locale-owned Client receipts; text remains a CLI fallback. */
2
- import { capabilityProofText, normalizeCapabilityProof } from './capability-delivery-proof.js'
3
-
1
+ /**
2
+ * Structured command outcome for locale-owned Client receipts; text remains a CLI fallback.
3
+ *
4
+ * A receipt states the one write a command performed: which entry, which choice,
5
+ * and which owner now records it. Whether a cell later reached that entry is a
6
+ * different question with a different owner — a catalogue row's `evidence` field,
7
+ * where a marker means an accepted execution obtained it, and which the page does
8
+ * not state — so a receipt that restated
9
+ * a degree of proof would answer a question this command never asked and could
10
+ * not answer.
11
+ */
4
12
  const PREFIX = '\n\nPTC Plus capability refresh receipt:\n'
5
13
  const FALLBACKS = Object.freeze({
6
14
  'saved-only': 'Global choice saved, but the session update failed; inspect and retry session',
@@ -12,34 +20,110 @@ const FALLBACKS = Object.freeze({
12
20
  off: 'Capability management is off; the session stays with the Host capabilities for session',
13
21
  })
14
22
  const STATES = new Set(['recorded', 'pending', 'unavailable', 'usage', 'saved-only', 'paused', 'off'])
23
+ const SCOPES = new Set(['session', 'global'])
24
+ const DIMENSIONS = new Set(['availability', 'disclosure'])
25
+ // Every choice `/capabilities set` accepts, so a receipt cannot name a write the
26
+ // command would have refused.
27
+ const SELECTIONS = new Set(['full', 'on-demand', 'closed', 'automatic', 'inherit', 'enabled', 'host', 'adaptive'])
28
+ /**
29
+ * What the receipt's one-line sentence needs from an id: no whitespace, bounded length.
30
+ *
31
+ * Which ids a choice may name is the record vocabulary's question
32
+ * (`capability-selection.js`), answered once where the record is written. Asking it
33
+ * again here would be a second owner of the same vocabulary, and a stricter second
34
+ * owner refuses a record the session already holds: the write lands, the receipt cannot
35
+ * be composed, and the command reports a failure it did not have. This predicate is a
36
+ * strict superset of that vocabulary, so every record a writer accepts is reportable.
37
+ */
38
+ const ENTRY_ID = /^\S{1,256}$/
39
+ /**
40
+ * Releases up to 0.5.4 appended a saved/delivered/usable report to the same
41
+ * receipt. Such a receipt stays readable — the Client renders the outcome it
42
+ * recorded instead of falling back to its raw text — and the report is dropped
43
+ * before any consumer sees it. Its wording is frozen history this build does not
44
+ * reproduce, so its opening segment plus the stored facts are the whole check.
45
+ */
46
+ const LEGACY_REPORT = ' Saved: '
47
+
48
+ function isRecord(value) {
49
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
50
+ }
51
+
52
+ /** One written choice, closed over the vocabulary the writer accepts. */
53
+ function commandRecord(value) {
54
+ if (!isRecord(value) || !SCOPES.has(value.scope) || typeof value.id !== 'string' || !ENTRY_ID.test(value.id)
55
+ || !SELECTIONS.has(value.selection)
56
+ || value.dimension !== undefined && !DIMENSIONS.has(value.dimension)
57
+ || Reflect.ownKeys(value).length !== (value.dimension === undefined ? 3 : 4)) {
58
+ throw new TypeError('invalid capability command record')
59
+ }
60
+ return Object.freeze({
61
+ scope: value.scope,
62
+ id: value.id,
63
+ ...(value.dimension === undefined ? {} : { dimension: value.dimension }),
64
+ selection: value.selection,
65
+ })
66
+ }
67
+
68
+ /** The sentence a non-Client reader gets; the Client states the same facts in its own words. */
69
+ function recordFallback(record) {
70
+ const dimension = record.dimension === undefined ? '' : ` ${record.dimension}`
71
+ return `Recorded ${record.id}${dimension} = ${record.selection}`
72
+ + (record.scope === 'global' ? ' in the global configuration.' : ' for this session.')
73
+ }
15
74
 
16
- export function capabilityCommandReceipt(state, sessionId, error, proposal, proof) {
75
+ export function capabilityCommandReceipt(state, sessionId, error, proposal, record) {
17
76
  if (!STATES.has(state) || typeof sessionId !== 'string' || !sessionId) throw new TypeError('invalid capability command receipt')
18
77
  if (error !== undefined && (state !== 'saved-only' || typeof error !== 'string' || !error)) throw new TypeError('invalid capability command error')
19
78
  if (proposal !== undefined && (state !== 'pending' || !proposal || typeof proposal.commandId !== 'string'
20
79
  || typeof proposal.rawInput !== 'string' || !proposal.message)) throw new TypeError('invalid capability proposal receipt')
21
- // A command that names one capability reports that capability's three layers
22
- // beside the session outcome, so "saved" cannot be read as "delivered" or
23
- // "usable". The schema stays v1 because the field is additive: receipts
24
- // recorded before it existed still reproduce their own text.
25
- const layers = proof === undefined ? undefined : normalizeCapabilityProof(proof)
80
+ // Only a receipt that says the write happened names one: a command whose session
81
+ // write failed reports that failure instead of naming a record it never appended.
82
+ if (record !== undefined && state !== 'recorded') throw new TypeError('invalid capability command record state')
83
+ const written = record === undefined ? undefined : commandRecord(record)
26
84
  return `${FALLBACKS[state]} ${sessionId}.${error ? ` ${error}` : ''}`
27
- + (layers === undefined ? '' : ` ${capabilityProofText(layers)}`)
85
+ + (written === undefined ? '' : ` ${recordFallback(written)}`)
28
86
  + PREFIX
29
87
  + JSON.stringify({
30
88
  schema: 'ptc-capability-command/v1', state, sessionId,
31
89
  ...(error === undefined ? {} : { error }),
32
90
  ...(proposal === undefined ? {} : { proposal }),
33
- ...(layers === undefined ? {} : { proof: layers }),
91
+ ...(written === undefined ? {} : { record: written }),
34
92
  })
35
93
  }
36
94
 
95
+ /**
96
+ * The receipt a command text carries, or `undefined` for any other text.
97
+ *
98
+ * A receipt this build writes is read only when the whole text is exactly what
99
+ * the writer produces from the facts it stores. A 0.5.4 receipt carries the
100
+ * report described above instead, and its visible sentence cannot be reproduced
101
+ * without keeping that wording, so its stored facts decide: the outcome sentence,
102
+ * the field order and the record of what was written must all be the writer's.
103
+ */
37
104
  export function readCapabilityCommandReceipt(text) {
38
105
  if (typeof text !== 'string' || !text.includes(PREFIX)) return undefined
106
+ const cut = text.indexOf(PREFIX)
107
+ let stored
108
+ try { stored = JSON.parse(text.slice(cut + PREFIX.length)) } catch { return undefined }
109
+ if (stored?.schema !== 'ptc-capability-command/v1' || !STATES.has(stored.state)) return undefined
110
+ let current
39
111
  try {
40
- const receipt = JSON.parse(text.slice(text.indexOf(PREFIX) + PREFIX.length))
41
- return receipt?.schema === 'ptc-capability-command/v1' && STATES.has(receipt.state)
42
- && typeof receipt.sessionId === 'string' && receipt.sessionId
43
- && capabilityCommandReceipt(receipt.state, receipt.sessionId, receipt.error, receipt.proposal, receipt.proof) === text ? receipt : undefined
112
+ current = capabilityCommandReceipt(stored.state, stored.sessionId, stored.error, stored.proposal, stored.record)
44
113
  } catch { return undefined }
114
+ if (stored.proof === undefined) return current === text ? stored : undefined
115
+ const head = current.slice(0, current.indexOf(PREFIX))
116
+ if (!text.startsWith(head)) return undefined
117
+ const sentence = text.slice(head.length, cut)
118
+ // One report segment, one line, directly after the outcome sentence: the shape
119
+ // the 0.5.4 writer produced from these stored facts.
120
+ if (stored.record !== undefined || !sentence.startsWith(LEGACY_REPORT)
121
+ || sentence.includes('\n') || sentence.split(LEGACY_REPORT).length !== 2) return undefined
122
+ const fields = Object.keys(JSON.parse(current.slice(current.indexOf(PREFIX) + PREFIX.length)))
123
+ if (Object.keys(stored).join() !== [...fields, 'proof'].join()) return undefined
124
+ return {
125
+ schema: stored.schema, state: stored.state, sessionId: stored.sessionId,
126
+ ...(stored.error === undefined ? {} : { error: stored.error }),
127
+ ...(stored.proposal === undefined ? {} : { proposal: stored.proposal }),
128
+ }
45
129
  }