@steve31415/baselib 3.29.0 → 3.31.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 +3 -1
- package/dist/deploy/rollback.js +26 -1
- package/dist/deploy/types.d.ts +7 -0
- package/dist/llm/providers/anthropic.js +29 -21
- package/dist/llm/providers/openai.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,9 @@ ERROR log (`~/plasticine-way/docs/SECURITY.md`, "Resend API key").
|
|
|
29
29
|
|
|
30
30
|
Bins: `check-test-owners` — the fleet's structural test-coverage gate; every
|
|
31
31
|
app runs it from `npm run verify`. `pw-deploy` / `pw-rollback` — the fleet
|
|
32
|
-
deploy (`~/plasticine-way/docs/OPERATIONS.md`, "Deployment safety")
|
|
32
|
+
deploy (`~/plasticine-way/docs/OPERATIONS.md`, "Deployment safety"); an
|
|
33
|
+
app's hooks module (`DeployHooks` in `src/deploy/types.ts`) extends both,
|
|
34
|
+
`postRollback` being the one hook `pw-rollback` runs.
|
|
33
35
|
`pw-sql` / `pw-logs` — read-only production queries (database, Axiom), the
|
|
34
36
|
first step of any production investigation (same doc, "Investigating
|
|
35
37
|
production state").
|
package/dist/deploy/rollback.js
CHANGED
|
@@ -7,7 +7,7 @@ import { hostname } from 'node:os';
|
|
|
7
7
|
import { must, mustJson } from './exec.js';
|
|
8
8
|
import { openEvidence, PhaseTimer } from './evidence.js';
|
|
9
9
|
import { acquireLock, listReleaseRecords, placeHold, releaseLock } from './gcs.js';
|
|
10
|
-
import { DEFAULT_ACCOUNT, loadConfig } from './config.js';
|
|
10
|
+
import { DEFAULT_ACCOUNT, loadConfig, loadHooks } from './config.js';
|
|
11
11
|
import { pickRevisionForSha, releaseEntriesFrom, selectRollbackTarget, readServingState, SHA40, } from './plan.js';
|
|
12
12
|
export async function runRollback(options) {
|
|
13
13
|
const log = options.log ?? ((m) => console.error(`[pw-rollback] ${m}`));
|
|
@@ -19,6 +19,7 @@ export async function runRollback(options) {
|
|
|
19
19
|
const requested = argv.find((a) => SHA40.test(a));
|
|
20
20
|
const timer = new PhaseTimer(log);
|
|
21
21
|
let evidenceDir = '';
|
|
22
|
+
let rolledBackTo = '';
|
|
22
23
|
let lock = null;
|
|
23
24
|
try {
|
|
24
25
|
const config = await loadConfig(options.repoRoot);
|
|
@@ -151,6 +152,26 @@ export async function runRollback(options) {
|
|
|
151
152
|
throw new Error(`${service.name}: post-rollback smoke failed (${lastDetail})`);
|
|
152
153
|
log(`${service.name}: smoke passed`);
|
|
153
154
|
}
|
|
155
|
+
// The hooks module is loaded only now: a broken one must not stand
|
|
156
|
+
// between an incident and the traffic move.
|
|
157
|
+
timer.enter('hooks');
|
|
158
|
+
rolledBackTo = target;
|
|
159
|
+
const hooks = await loadHooks(options.repoRoot, config);
|
|
160
|
+
if (hooks.postRollback) {
|
|
161
|
+
const ctx = {
|
|
162
|
+
sha: target,
|
|
163
|
+
baseSha: servingShas.length === 1 ? servingShas[0] : null,
|
|
164
|
+
config,
|
|
165
|
+
repoRoot: options.repoRoot,
|
|
166
|
+
contextDir: '',
|
|
167
|
+
evidenceDir,
|
|
168
|
+
imageDigest: targets[0].imageDigest,
|
|
169
|
+
serving,
|
|
170
|
+
exec: runner,
|
|
171
|
+
log,
|
|
172
|
+
};
|
|
173
|
+
await hooks.postRollback(ctx);
|
|
174
|
+
}
|
|
154
175
|
timer.close();
|
|
155
176
|
log(`rolled back to ${target.slice(0, 12)}`);
|
|
156
177
|
log(`hold placed on ${servingShas.map((sha) => sha.slice(0, 12)).join(', ')} — ` +
|
|
@@ -162,6 +183,10 @@ export async function runRollback(options) {
|
|
|
162
183
|
const message = error instanceof Error ? error.message : String(error);
|
|
163
184
|
console.error(message);
|
|
164
185
|
console.error(`rollback failed in phase: ${timer.current}`);
|
|
186
|
+
if (rolledBackTo) {
|
|
187
|
+
console.error(`traffic IS on ${rolledBackTo.slice(0, 12)} and the hold is placed; only the app's ` +
|
|
188
|
+
'postRollback hook failed — finish what it does by hand');
|
|
189
|
+
}
|
|
165
190
|
if (evidenceDir)
|
|
166
191
|
console.error(`evidence: ${evidenceDir}`);
|
|
167
192
|
return 1;
|
package/dist/deploy/types.d.ts
CHANGED
|
@@ -120,4 +120,11 @@ export interface DeployHooks {
|
|
|
120
120
|
smoke?: (ctx: HookContext) => Promise<void>;
|
|
121
121
|
/** Runs after everything else succeeded (e.g. notes's release state). */
|
|
122
122
|
postSuccess?: (ctx: HookContext) => Promise<void>;
|
|
123
|
+
/** pw-rollback only: runs after traffic is back on the target build, the
|
|
124
|
+
* hold is placed and the smoke passed — for what a traffic move does not
|
|
125
|
+
* carry (e.g. podcasts' render Job image). `sha` and `imageDigest` are the
|
|
126
|
+
* rollback target's, `serving` is the state rolled back from, and
|
|
127
|
+
* `contextDir` is empty (nothing is built). A throw fails the command but
|
|
128
|
+
* undoes nothing. */
|
|
129
|
+
postRollback?: (ctx: HookContext) => Promise<void>;
|
|
123
130
|
}
|
|
@@ -13,32 +13,35 @@ function classifyFinish(stopReason) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Keywords
|
|
17
|
-
* Opus 5.5, 2026-09-22)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Keywords Anthropic's constrained decoding rejects outright with a 400 —
|
|
17
|
+
* `output_config.format` (verified on Opus 5.5, 2026-09-22) and strict tools
|
|
18
|
+
* alike (verified on every forced-tool registry model, 2026-10-07). `minItems`
|
|
19
|
+
* is also rejected for values other than 0 or 1, handled separately;
|
|
20
|
+
* `minLength`/`maxLength`, `pattern`, `format` and nullable unions are
|
|
21
|
+
* accepted.
|
|
20
22
|
*/
|
|
21
|
-
const
|
|
23
|
+
const UNSUPPORTED_STRICT_KEYWORDS = [
|
|
22
24
|
'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
|
|
23
25
|
'maxItems', 'uniqueItems',
|
|
24
26
|
];
|
|
25
27
|
/**
|
|
26
|
-
* Transform a portable JSON schema into the dialect Anthropic's
|
|
27
|
-
*
|
|
28
|
-
* `additionalProperties: false`, and the numeric/array
|
|
29
|
-
* dropped. The model then no longer enforces those
|
|
30
|
-
* the parsed result as they already must for
|
|
31
|
-
*
|
|
32
|
-
* caller's schema object is reused across
|
|
28
|
+
* Transform a portable JSON schema into the dialect Anthropic's constrained
|
|
29
|
+
* decoding accepts (`output_config.format` and `strict: true` tools): every
|
|
30
|
+
* object schema must set `additionalProperties: false`, and the numeric/array
|
|
31
|
+
* bounds listed above are dropped. The model then no longer enforces those
|
|
32
|
+
* bounds — callers validate the parsed result as they already must for
|
|
33
|
+
* truncation. Optional properties stay optional (unlike OpenAI's strict
|
|
34
|
+
* dialect). Returns a deep copy — the caller's schema object is reused across
|
|
35
|
+
* failover attempts.
|
|
33
36
|
*/
|
|
34
|
-
function
|
|
37
|
+
function toAnthropicStrictSchema(node) {
|
|
35
38
|
if (Array.isArray(node))
|
|
36
|
-
return node.map(
|
|
39
|
+
return node.map(toAnthropicStrictSchema);
|
|
37
40
|
if (node === null || typeof node !== 'object')
|
|
38
41
|
return node;
|
|
39
42
|
const out = {};
|
|
40
43
|
for (const [key, value] of Object.entries(node)) {
|
|
41
|
-
if (
|
|
44
|
+
if (UNSUPPORTED_STRICT_KEYWORDS.includes(key))
|
|
42
45
|
continue;
|
|
43
46
|
if (key === 'minItems' && typeof value === 'number' && value > 1)
|
|
44
47
|
continue;
|
|
@@ -46,10 +49,10 @@ function toOutputFormatSchema(node) {
|
|
|
46
49
|
// property that happens to be named `minimum` is kept.
|
|
47
50
|
if ((key === 'properties' || key === '$defs' || key === 'definitions')
|
|
48
51
|
&& value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
49
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => [k,
|
|
52
|
+
out[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toAnthropicStrictSchema(v)]));
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
52
|
-
out[key] =
|
|
55
|
+
out[key] = toAnthropicStrictSchema(value);
|
|
53
56
|
}
|
|
54
57
|
const type = out.type;
|
|
55
58
|
if (type === 'object' || (Array.isArray(type) && type.includes('object'))) {
|
|
@@ -87,20 +90,25 @@ export const anthropicProvider = {
|
|
|
87
90
|
? [{ type: 'text', text: request.systemPrompt, cache_control: { type: 'ephemeral' } }]
|
|
88
91
|
: request.systemPrompt;
|
|
89
92
|
}
|
|
90
|
-
// JSON schema: a forced tool_use call where the model allows it,
|
|
91
|
-
// structured outputs (Opus 5.5 rejects forced tool_choice).
|
|
93
|
+
// JSON schema: a forced, strict tool_use call where the model allows it,
|
|
94
|
+
// else structured outputs (Opus 5.5 rejects forced tool_choice). Both
|
|
95
|
+
// constrain decoding to the schema. A tool without `strict: true` is not
|
|
96
|
+
// enforced: Sonnet 5 returned an array property as a JSON string (digest,
|
|
97
|
+
// 2026-10-06), Opus 4.8 one wrapped in leaked tool-call markup (podcasts,
|
|
98
|
+
// 2026-09-15).
|
|
92
99
|
const useOutputFormat = request.jsonSchema !== undefined && !request.supportsForcedToolChoice;
|
|
93
100
|
if (request.jsonSchema && useOutputFormat) {
|
|
94
101
|
body.output_config = {
|
|
95
102
|
...body.output_config,
|
|
96
|
-
format: { type: 'json_schema', schema:
|
|
103
|
+
format: { type: 'json_schema', schema: toAnthropicStrictSchema(request.jsonSchema.schema) },
|
|
97
104
|
};
|
|
98
105
|
}
|
|
99
106
|
else if (request.jsonSchema) {
|
|
100
107
|
body.tools = [{
|
|
101
108
|
name: request.jsonSchema.name,
|
|
102
109
|
description: `Generate structured output matching the ${request.jsonSchema.name} schema`,
|
|
103
|
-
input_schema: request.jsonSchema.schema,
|
|
110
|
+
input_schema: toAnthropicStrictSchema(request.jsonSchema.schema),
|
|
111
|
+
strict: true,
|
|
104
112
|
}];
|
|
105
113
|
body.tool_choice = { type: 'tool', name: request.jsonSchema.name };
|
|
106
114
|
}
|
|
@@ -37,10 +37,10 @@ function makeNullable(propSchema) {
|
|
|
37
37
|
* Transform a portable JSON schema into the shape OpenAI's strict Structured
|
|
38
38
|
* Outputs mode requires: every object must set `additionalProperties: false`
|
|
39
39
|
* and list ALL of its properties in `required`. Callers author one portable
|
|
40
|
-
* schema
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* schema; this keeps the OpenAI-specific dialect local to this adapter,
|
|
41
|
+
* mirroring the Gemini adapter's `stripUnsupportedSchemaFields` and the
|
|
42
|
+
* Anthropic adapter's `toAnthropicStrictSchema` (which closes objects but keeps
|
|
43
|
+
* optional fields optional). Returns a deep copy — the
|
|
44
44
|
* caller's schema object is never mutated (it is reused across failover attempts).
|
|
45
45
|
*/
|
|
46
46
|
function toStrictSchema(node) {
|
package/package.json
CHANGED