wicked-crew 0.7.20 → 0.7.21

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.
@@ -4,6 +4,7 @@ import { CoreAdapter } from '../core/adapter.js';
4
4
  import { type GateCache } from './gate-cache.js';
5
5
  import type { ElicitationCache } from './elicitation-cache.js';
6
6
  import { QeGateCache } from '../qe/gate-events.js';
7
+ import type { ConformanceRule, SteeringType } from '../core/types.js';
7
8
  import { SeatHealthTracker } from './seat-health.js';
8
9
  import { type ProjectRoutesDeps } from '../projects/routes.js';
9
10
  import { InteractiveBridgePool } from '../interactive/bridge-pool.js';
@@ -18,6 +19,30 @@ import { DeliveryDerivationCache } from './delivery-cache.js';
18
19
  import { type DeliverExec, type WorktreeReprovisioner } from './post-hoc-deliver.js';
19
20
  import { type AuthMode } from './auth.js';
20
21
  export { API_PREFIX } from './api-prefix.js';
22
+ /**
23
+ * Map an APPROVED policy proposal (estate `proposal.approve` → `handed_off`) into a steering
24
+ * ConformanceRule (DES-MEM-FACETED-001 §5.2). A policy proposal's `kind_type` is
25
+ * `policy:<steering_type>` and its `payload` is `{ rule, severity }`; estate writes NOTHING for it
26
+ * (the AW-11 "no rules.write on estate" invariant) and hands the payload back for crew — the ONE
27
+ * governed rules-write path — to land. Returns the rule to upsert plus the resolved steering type,
28
+ * or a loud `error` string when the proposal cannot be shaped into a valid rule (a malformed
29
+ * `kind_type` / missing `rule` / out-of-enum `severity`) — the caller reports it as a failed
30
+ * landing, never a silent drop (the crew#388 anti-silent-loss doctrine).
31
+ *
32
+ * The rule id is DETERMINISTIC (`proposal:<id>`) so a re-driven landing UPSERTS the same rule
33
+ * (idempotent) instead of minting a duplicate; it sits OUTSIDE the reserved `PAT-/POL-` namespace,
34
+ * which UI/chat/proposal-authored rules are free to do (INV-C1). The rule carries NO `effect`
35
+ * (recall-only — exactly what `{rule, severity}` supports: an enforcement rule would need a
36
+ * non-blank `applies_to`, which the payload does not carry, and INV-S3 fails such a rule closed).
37
+ * `steering_type` is validated against the vocabulary here so the engine's INV-S1 never rejects it
38
+ * as an unknown page.
39
+ */
40
+ export declare function policyProposalToRule(proposalId: string, kindType: string, payload: unknown, facets: Record<string, string> | undefined): {
41
+ rule: ConformanceRule;
42
+ steeringType: SteeringType;
43
+ } | {
44
+ error: string;
45
+ };
21
46
  /**
22
47
  * The launch body. Every field but `problem` is optional, which is what made stripping dangerous:
23
48
  * omitting one is a legitimate request that gets an engine default, so a MISSPELLED one was
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
@@ -100,6 +100,70 @@ function isStringRecord(v) {
100
100
  !Array.isArray(v) &&
101
101
  Object.values(v).every((x) => typeof x === 'string'));
102
102
  }
103
+ /**
104
+ * Map an APPROVED policy proposal (estate `proposal.approve` → `handed_off`) into a steering
105
+ * ConformanceRule (DES-MEM-FACETED-001 §5.2). A policy proposal's `kind_type` is
106
+ * `policy:<steering_type>` and its `payload` is `{ rule, severity }`; estate writes NOTHING for it
107
+ * (the AW-11 "no rules.write on estate" invariant) and hands the payload back for crew — the ONE
108
+ * governed rules-write path — to land. Returns the rule to upsert plus the resolved steering type,
109
+ * or a loud `error` string when the proposal cannot be shaped into a valid rule (a malformed
110
+ * `kind_type` / missing `rule` / out-of-enum `severity`) — the caller reports it as a failed
111
+ * landing, never a silent drop (the crew#388 anti-silent-loss doctrine).
112
+ *
113
+ * The rule id is DETERMINISTIC (`proposal:<id>`) so a re-driven landing UPSERTS the same rule
114
+ * (idempotent) instead of minting a duplicate; it sits OUTSIDE the reserved `PAT-/POL-` namespace,
115
+ * which UI/chat/proposal-authored rules are free to do (INV-C1). The rule carries NO `effect`
116
+ * (recall-only — exactly what `{rule, severity}` supports: an enforcement rule would need a
117
+ * non-blank `applies_to`, which the payload does not carry, and INV-S3 fails such a rule closed).
118
+ * `steering_type` is validated against the vocabulary here so the engine's INV-S1 never rejects it
119
+ * as an unknown page.
120
+ */
121
+ export function policyProposalToRule(proposalId, kindType, payload, facets) {
122
+ const steeringType = kindType.startsWith('policy:') ? kindType.slice('policy:'.length) : '';
123
+ if (!STEERING_TYPES.has(steeringType)) {
124
+ return {
125
+ error: `the approved proposal's kind_type ${JSON.stringify(kindType)} does not name a steering ` +
126
+ `type — expected \`policy:<${STEERING_TYPE_VALUES.join('|')}>\``,
127
+ };
128
+ }
129
+ const body = typeof payload === 'object' && payload !== null && !Array.isArray(payload)
130
+ ? payload
131
+ : {};
132
+ const statement = typeof body['rule'] === 'string' ? body['rule'].trim() : '';
133
+ if (statement === '') {
134
+ return {
135
+ error: 'the approved policy proposal payload has no `rule` string to make a rule statement from',
136
+ };
137
+ }
138
+ const rawSeverity = body['severity'];
139
+ const severity = rawSeverity === 'info' || rawSeverity === 'warn' || rawSeverity === 'error' || rawSeverity === 'critical'
140
+ ? rawSeverity
141
+ : undefined;
142
+ if (rawSeverity !== undefined && severity === undefined) {
143
+ return {
144
+ error: `the approved policy proposal payload has an invalid severity ${JSON.stringify(rawSeverity)} — ` +
145
+ 'expected info|warn|error|critical',
146
+ };
147
+ }
148
+ const language = facets?.['language'];
149
+ const rule = {
150
+ id: `proposal:${proposalId}`,
151
+ rule_type: 'policy',
152
+ statement,
153
+ // A policy proposal SHOULD carry severity; a missing one defaults to `warn` (the middle band)
154
+ // rather than failing the landing, but a present-but-garbage one fails loud above.
155
+ severity: severity ?? 'warn',
156
+ // REQUIRED engine-side (f32, no serde default, INV-C2 `[0,1]`); the payload carries none, so a
157
+ // fixed authority — the same 0.8 the steering-author landing defaults to.
158
+ confidence: 0.8,
159
+ // Only the `language` facet maps onto the engine's Targets facet object; `repo`/`project`
160
+ // facets have no ConformanceRule slot and are dropped (see the route's open question).
161
+ targets: typeof language === 'string' && language !== '' ? { language } : {},
162
+ provenance: { source: 'proposal', source_kinds: [] },
163
+ steering_type: steeringType,
164
+ };
165
+ return { rule, steeringType: steeringType };
166
+ }
103
167
  /** A JSON object whose every value is a number (a coverage breakdown map). */
104
168
  function isNumberRecord(v) {
105
169
  return (typeof v === 'object' &&
@@ -2744,23 +2808,95 @@ runtime = {}) {
2744
2808
  }
2745
2809
  });
2746
2810
  // POST /proposals/:id/approve → proposal.approve →
2747
- // { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
2748
- // { outcome:'handed_off', payload } — a POLICY proposal, returned VERBATIM.
2811
+ // { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
2812
+ // { outcome:'handed_off', payload, landing } — a POLICY proposal, LANDED here as a steering rule.
2813
+ //
2814
+ // The policy→steering landing (DES-MEM-FACETED-001 §5.2): estate performs NO rules-write (the
2815
+ // AW-11 invariant) — it marks the proposal approved and hands its `{ rule, severity }` payload
2816
+ // back, and CREW is the one governed rules-write path. On a `handed_off` outcome we recover the
2817
+ // proposal's `kind_type` (which carries the `policy:<steering_type>` the approve response does
2818
+ // NOT echo), shape a `ConformanceRule` from it, and upsert it via the single-writer actor. The
2819
+ // posture mirrors the steering-author landing (crew#388): the estate decision (approve) already
2820
+ // stands, so a landing failure is reported LOUD in-band as `landing.outcome:"failed"` — never a
2821
+ // 500, never the silent no-op that crew#388 exists to end — and `payload` is still returned for
2822
+ // backward compatibility. The MEMORY path is UNTOUCHED: a `promoted` outcome returns immediately
2823
+ // with no extra estate call and no landing.
2749
2824
  app.post(`${V}/proposals/:id/approve`, { config: { manifest: { responseType: 'ApproveProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
2750
- const { id } = req.params;
2751
- if (id.trim() === '') {
2825
+ // Normalize ONCE and use the trimmed value throughout — an id like `%20pol1%20`
2826
+ // decodes to a padded, non-empty string that would otherwise ride upstream as-is
2827
+ // and into `proposal:<id>` derivations.
2828
+ const id = req.params.id.trim();
2829
+ if (id === '') {
2752
2830
  return reply.code(400).send({ error: '`id` is required' });
2753
2831
  }
2832
+ let approved;
2754
2833
  try {
2755
- // TODO: route handed_off policy payload to steering (crew steering-write, DES-MEM-FACETED-001
2756
- // §5.2). Policy→steering routing is OUT OF SCOPE here: a `handed_off` outcome is passed
2757
- // through as-is for a later steering-write to consume; memory proposals return `promoted` and
2758
- // need nothing more.
2759
- return (await estateTool('proposal.approve', { id }));
2834
+ approved = (await estateTool('proposal.approve', { id }));
2760
2835
  }
2761
2836
  catch (err) {
2762
2837
  return estateUpstreamError(reply, err);
2763
2838
  }
2839
+ // MEMORY proposal (or any future non-policy outcome): estate already promoted it — pass through
2840
+ // verbatim, exactly as before. No extra estate round-trip on the memory path.
2841
+ if (approved.outcome !== 'handed_off') {
2842
+ return approved;
2843
+ }
2844
+ // POLICY proposal — land the handed-off payload as a steering rule.
2845
+ const landFailed = (error) => {
2846
+ audit.record('governance.steering.landing_failed', actorOf(req), {
2847
+ detail: { proposalId: id, error },
2848
+ });
2849
+ return { outcome: 'handed_off', payload: approved.payload, landing: { outcome: 'failed', error } };
2850
+ };
2851
+ // A pre-steering engine would SILENTLY DROP `steering_type` and the other steering fields
2852
+ // (ConformanceRule has no deny_unknown_fields), persisting a rule that enforces differently than
2853
+ // authored — the same guard POST /governance/rules applies. Fail loud (before any further estate
2854
+ // round-trip), persist nothing.
2855
+ if (!adapter.steeringSupported()) {
2856
+ return landFailed(`${new SteeringUnsupportedError('Landing an approved policy proposal').message} — the ` +
2857
+ `installed engine would silently drop the rule's steering fields; upgrade wicked-core-ts (>= 0.7.5)`);
2858
+ }
2859
+ // Recover the proposal's `kind_type` + `facets`: `proposal.approve` does not echo them, and the
2860
+ // steering type lives ONLY in `kind_type` (`policy:<type>`). The proposal is `approved` now, so
2861
+ // read it back from the approved queue.
2862
+ let proposal;
2863
+ try {
2864
+ const listed = (await estateTool('proposal.list', { state: 'approved' }));
2865
+ proposal = listed.proposals.find((p) => p.id === id);
2866
+ }
2867
+ catch (err) {
2868
+ return landFailed(`the policy proposal was approved but its record could not be re-read to land a steering ` +
2869
+ `rule (${message(err)}); land it by hand via POST ${V}/governance/rules`);
2870
+ }
2871
+ if (proposal === undefined || typeof proposal.kind_type !== 'string') {
2872
+ return landFailed(`the policy proposal was approved but could not be found in the approved queue to recover ` +
2873
+ `its steering type; land it by hand via POST ${V}/governance/rules`);
2874
+ }
2875
+ const built = policyProposalToRule(id, proposal.kind_type, approved.payload, isStringRecord(proposal.facets) ? proposal.facets : undefined);
2876
+ if ('error' in built) {
2877
+ return landFailed(built.error);
2878
+ }
2879
+ try {
2880
+ await adapter.upsertConformanceRule(built.rule);
2881
+ }
2882
+ catch (err) {
2883
+ return landFailed(`the store refused the steering rule derived from the approved policy proposal: ${message(err)}`);
2884
+ }
2885
+ audit.record('governance.rule.upserted', actorOf(req), {
2886
+ detail: {
2887
+ id: built.rule.id,
2888
+ source: 'proposal',
2889
+ via: 'proposal-approve',
2890
+ steeringType: built.steeringType,
2891
+ proposalId: id,
2892
+ },
2893
+ });
2894
+ const landing = {
2895
+ outcome: 'landed',
2896
+ ruleId: built.rule.id,
2897
+ steering_type: built.steeringType,
2898
+ };
2899
+ return { outcome: 'handed_off', payload: approved.payload, landing };
2764
2900
  });
2765
2901
  // POST /proposals/:id/reject → proposal.reject → { ok: true }.
2766
2902
  app.post(`${V}/proposals/:id/reject`, { config: { manifest: { responseType: 'RejectProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {