Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions bindings/nodejs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ prepareLocalPublish:
yarn createNpmDirs

publish:
cd npm/wasm32-wasi && npm publish --registry $(REGISTRY) --ignore-scripts
cd npm/darwin-arm64 && npm publish --registry $(REGISTRY) --ignore-scripts
npm publish --registry $(REGISTRY) --ignore-scripts
cd npm/wasm32-wasi && npm publish --registry $(REGISTRY) --ignore-scripts --tag latest
cd npm/darwin-arm64 && npm publish --registry $(REGISTRY) --ignore-scripts --tag latest
npm publish --registry $(REGISTRY) --ignore-scripts --tag latest

publishLocal: prepareLocalPublish wasm darwinArm publish
4 changes: 4 additions & 0 deletions bindings/nodejs/dts-header.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export interface NlResult {
diagnostics: NlDiagnostic[];
/** Resolved `$` type for unary requests (decision-table input cells); present even for empty text. */
subjectType?: PolicyVariableType;
/** Labeled options when the unary subject is an enum (dictionary labels applied); present even for empty text. */
subjectOptions?: NlEnumOption[];
}

/**
Expand All @@ -212,6 +214,8 @@ export interface PolicyNlExpression {
diagnostics: NlDiagnostic[];
/** Resolved `$` type for unary cells. */
subjectType?: PolicyVariableType;
/** Labeled options when the unary subject is an enum (dictionary labels applied). */
subjectOptions?: NlEnumOption[];
}

/**
Expand Down
82 changes: 17 additions & 65 deletions bindings/nodejs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export interface NlResult {
diagnostics: NlDiagnostic[];
/** Resolved `$` type for unary requests (decision-table input cells); present even for empty text. */
subjectType?: PolicyVariableType;
/** Labeled options when the unary subject is an enum (dictionary labels applied); present even for empty text. */
subjectOptions?: NlEnumOption[];
}

/**
Expand All @@ -212,6 +214,8 @@ export interface PolicyNlExpression {
diagnostics: NlDiagnostic[];
/** Resolved `$` type for unary cells. */
subjectType?: PolicyVariableType;
/** Labeled options when the unary subject is an enum (dictionary labels applied). */
subjectOptions?: NlEnumOption[];
}

/**
Expand Down Expand Up @@ -362,30 +366,14 @@ export declare class PolicyWorkspace {
constructor()
setPolicy(path: string, document: any): void
removePolicy(path: string): boolean
/**
* Upsert a single block in an existing policy (replace-by-id or append).
* Errors when the policy does not exist — call `setPolicy` first to
* create one.
*/
updateBlock(req: PolicyUpdateBlockRequest): void
/**
* Remove a block from an existing policy by id. Returns `true` when a
* block was removed, `false` when the policy or block didn't exist.
*/
removeBlock(req: PolicyRemoveBlockRequest): boolean
policyPaths(): Array<string>
/**
* `max_diagnostics` caps the returned list (default: 100). Pass `0` for
* no cap.
*/
diagnostics(policyPath: string, maxDiagnostics?: number | undefined | null): Array<PolicyDiagnostic>
/**
* `max_diagnostics` caps the returned list (default: 100). Pass `0` for
* no cap.
*/
allDiagnostics(maxDiagnostics?: number | undefined | null): Array<PolicyDiagnostic>
entities(req: PolicyScopeRequest): Array<PolicyEntityInfo>
globals(req: PolicyScopeRequest): Array<PolicyGlobalInfo>
dictionaries(req: PolicyScopeRequest): Array<PolicyDictionaryInfo>
inputs(req: PolicyScopeRequest): Array<PolicyInputProperty>
outputs(req: PolicyScopeRequest): Array<PolicyOutputProperty>
conditionalSchema(req: PolicyScopeRequest): PolicyConditionalSchema
Expand All @@ -394,32 +382,9 @@ export declare class PolicyWorkspace {
nlTokenize(cursor: PolicyExpressionCursor, text: string): NlResult | null
completions(cursor: PolicyExpressionCursor): Array<PolicyCompletion>
prepareRename(cursor: PolicyExpressionCursor): PolicyPrepareRenameResult | null
/**
* Returns block-level edits the host applies via id-keyed swap (same
* path as `update_block`). Each edit's `kind` field discriminates the
* variant; `replaceBlock` carries a `newBlock` payload that is the
* rewritten wire-format `BlockDoc`.
*/
rename(req: PolicyRenameRequest): PolicyEngineEdit[]
/**
* Returns every site in the workspace where `target` is used. Same
* visitor as `rename`; carries policy/block/expression/source/span/kind
* for each site so hosts can render a "find references" panel or drive
* navigation.
*/
references(target: any): PolicyReferenceSite[]
/**
* Default-valued JSON object that matches the workspace's input shape
* for `req.policy_path` (and optionally `req.goals`). Hosts use it as
* the initial value of a "Run simulation" panel so `evaluate` can be
* called immediately without first authoring an input by hand.
*/
inputSkeleton(req: PolicyScopeRequest): unknown
/**
* Returns the transitive dependency tree rooted at `target`. Inverse
* of `references()`. Per-write granularity — multi-output blocks
* don't conflate sibling outputs' deps.
*/
dependencies(target: string): PolicyDependencyNode
evaluate(req: PolicyEvaluateRequest): PolicyEvaluationResult
enhanceTrace(req: PolicyEvaluateRequest): PolicyEvaluationResult
Expand All @@ -446,7 +411,7 @@ export declare class ZenEngine {
getDecision(key: string): Promise<ZenDecision>
safeEvaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise<{ success: true, data: ZenEngineResponse } | { success: false; error: any; }>
safeGetDecision(key: string): Promise<{ success: true, data: ZenDecision } | { success: false; error: any; }>
evaluateBatch(requests: Array<EvaluateBatchRequest>, opts?: ZenEvaluateOptions | undefined | null): Promise<Array<EvaluateBatchResult>>
evaluateBatch(requests: Array<EvaluateBatchRequest>, opts?: ZenEvaluateOptions | undefined | null): Promise<Array<{ success: true; data: ZenEngineResponse } | { success: false; error: any }>>
reload(): Promise<void>
compileFailures(): Array<{ key: string; kind: string; diagnostics?: Array<{ code: string; message: string; severity: string }>; error?: string }>
dispose(): void
Expand All @@ -472,12 +437,6 @@ export interface EvaluateBatchRequest {
context: any
}

export interface EvaluateBatchResult {
success: boolean
data?: any
error?: any
}

export declare function evaluateExpression(expression: string, context?: any | undefined | null): Promise<any>

export declare function evaluateExpressionSync(expression: string, context?: any | undefined | null): any
Expand Down Expand Up @@ -524,6 +483,17 @@ export interface PolicyDiagnostic {
target?: PolicyCursorTarget
}

export interface PolicyDictionaryEntryInfo {
value: string
label: string
}

export interface PolicyDictionaryInfo {
name: string
source: string
entries: Array<PolicyDictionaryEntryInfo>
}

export interface PolicyDiscriminantVariant {
value?: string
arm: string
Expand All @@ -550,7 +520,6 @@ export interface PolicyEntityInfo {
export interface PolicyEvaluateRequest {
policyPath: string
input: unknown
/** Goals to evaluate. Omit or pass empty for full evaluation. */
goals?: Array<string>
trace?: boolean
}
Expand All @@ -559,18 +528,9 @@ export interface PolicyExpressionCursor {
policyPath: string
blockId: string
pos: number
/**
* Tagged `{ kind, ...payload }` discriminating what kind of span the
* cursor sits in. See `PolicyCursorTarget` in the TypeScript types.
*/
target: PolicyCursorTarget
}

/**
* Kept as a `#[napi(object)]` struct purely so NAPI-RS emits the TS type
* used by `PolicyFieldOrigin.schema.fieldKind`. The runtime shape is
* hand-built in [`field_kind_to_json`].
*/
export interface PolicyFieldKindInfo {
kind: PolicyFieldKind
target?: string
Expand Down Expand Up @@ -640,19 +600,11 @@ export interface PolicySchemaGroup {

export interface PolicyScopeRequest {
policyPath: string
/**
* Goals to constrain schema introspection to. Omit or pass empty
* for everything reachable from the policy.
*/
goals?: Array<string>
}

export interface PolicyUpdateBlockRequest {
policyPath: string
/**
* A single wire block (same shape as one entry of `PolicyDocument.blocks`).
* Upserted by `block.id`: replaces in place if present, appends otherwise.
*/
block: unknown
}

Expand Down
2 changes: 2 additions & 0 deletions bindings/nodejs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@ module.exports.evaluateExpression = nativeBinding.evaluateExpression
module.exports.evaluateExpressionSync = nativeBinding.evaluateExpressionSync
module.exports.evaluateUnaryExpression = nativeBinding.evaluateUnaryExpression
module.exports.evaluateUnaryExpressionSync = nativeBinding.evaluateUnaryExpressionSync
module.exports.nlEncodeString = nativeBinding.nlEncodeString
module.exports.nlTokenizeBatch = nativeBinding.nlTokenizeBatch
module.exports.overrideConfig = nativeBinding.overrideConfig
module.exports.renderTemplate = nativeBinding.renderTemplate
module.exports.renderTemplateSync = nativeBinding.renderTemplateSync
33 changes: 33 additions & 0 deletions bindings/nodejs/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ pub struct PolicyGlobalInfo {
pub origin: Value,
}

#[napi(object)]
pub struct PolicyDictionaryInfo {
pub name: String,
pub source: String,
pub entries: Vec<PolicyDictionaryEntryInfo>,
}

#[napi(object)]
pub struct PolicyDictionaryEntryInfo {
pub value: String,
pub label: String,
}

#[napi(object)]
pub struct PolicyInputProperty {
pub path: String,
Expand Down Expand Up @@ -540,6 +553,26 @@ impl PolicyWorkspace {
.collect()
}

#[napi]
pub fn dictionaries(&self, req: PolicyScopeRequest) -> Vec<PolicyDictionaryInfo> {
self.inner
.dictionaries(&req.into())
.into_iter()
.map(|d| PolicyDictionaryInfo {
name: d.name.to_string(),
source: d.source.to_string(),
entries: d
.entries
.iter()
.map(|e| PolicyDictionaryEntryInfo {
value: e.value.to_string(),
label: e.label.to_string(),
})
.collect(),
})
.collect()
}

#[napi]
pub fn inputs(&self, req: PolicyScopeRequest) -> Vec<PolicyInputProperty> {
self.inner
Expand Down
34 changes: 33 additions & 1 deletion core/engine/src/policy/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use crate::policy::blocks::{
SharedIntelliSense,
};
use crate::policy::evaluator::EvalArtifact;
use crate::policy::ir::{DataModelIr, ParsedPolicy, Policy, Property, PropertyPath, Scope};
use crate::policy::ir::{
DataModelIr, DictionaryIr, ParsedPolicy, Policy, Property, PropertyPath, Scope,
};
use crate::policy::queries::dependency::{
DataModelPaths, DependencyGraph, EnrichedState, EvalGraph, RuleShallowAnalysis, ShallowAnalyses,
};
Expand Down Expand Up @@ -141,6 +143,14 @@ pub struct Unit {
opcode_cache: OnceCell<Arc<OpcodeCache>>,
pub data_models: Vec<DataModelEntry>,
pub entities: HashMap<Arc<str>, Arc<DataModelIr>>,
pub dictionaries: HashMap<Arc<str>, Arc<DictionaryIr>>,
pub dictionary_blocks: Vec<DictionaryUnitEntry>,
}

pub struct DictionaryUnitEntry {
pub policy_path: Arc<str>,
pub block_id: Arc<str>,
pub ir: Arc<DictionaryIr>,
}

pub struct Db {
Expand Down Expand Up @@ -624,6 +634,26 @@ impl Snapshot {
});

let entities = Self::compute_unit_entities(&subset);
let dictionaries = Self::compute_dictionary_map(&subset);

let mut dictionary_blocks: Vec<DictionaryUnitEntry> = subset
.iter()
.flat_map(|(path, p)| {
p.policy
.dictionaries
.iter()
.map(move |block| DictionaryUnitEntry {
policy_path: path.clone(),
block_id: block.id.clone(),
ir: block.ir.clone(),
})
})
.collect();
dictionary_blocks.sort_by(|a, b| {
a.ir.name
.cmp(&b.ir.name)
.then_with(|| a.policy_path.cmp(&b.policy_path))
});

Unit {
members: member_set,
Expand All @@ -639,6 +669,8 @@ impl Snapshot {
opcode_cache: OnceCell::new(),
data_models,
entities,
dictionaries,
dictionary_blocks,
}
}

Expand Down
44 changes: 39 additions & 5 deletions core/engine/src/policy/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,63 @@ impl Db {
return Vec::new();
};
let scope = self.enriched(policy).scope.shallow_clone();
let labels = self.nl_label_resolver(policy);
let intellisense = self.intellisense();
let mut is = intellisense.borrow_mut();
is.set_nl_labels(labels);
let mut out = Vec::new();
for rule in parsed.policy.rules() {
out.extend(rule.nl(&policy_arc, &scope, &mut is));
}
is.set_nl_labels(None);
out
}

pub fn nl_tokenize(&self, cursor: &Cursor, text: &str) -> Option<NlResult> {
let (kind, scope) = self.nl_scope(cursor)?;
let unary = matches!(kind, ExpressionKind::Unary);
let labels = self.nl_label_resolver(&cursor.policy_path);
let intellisense = self.intellisense();
let mut result =
intellisense
.borrow_mut()
.nl_tokenize_scoped(&cursor.block_id, text, unary, &scope);
let mut is = intellisense.borrow_mut();
is.set_nl_labels(labels);
let mut result = is.nl_tokenize_scoped(&cursor.block_id, text, unary, &scope);
if unary {
result.subject_type = Some(scope.get("$"));
let subject = scope.get("$");
result.subject_options = is.nl_subject_options(&subject);
result.subject_type = Some(subject);
}
is.set_nl_labels(None);
Some(result)
}

fn nl_label_resolver(
&self,
policy: &str,
) -> Option<zen_expression::intellisense::NlLabelResolver> {
let unit = self.unit(policy);
if unit.dictionary_blocks.is_empty() {
return None;
}
let mut labels: HashMap<Arc<str>, HashMap<Arc<str>, Arc<str>>> = HashMap::new();
for (name, dict) in &unit.dictionaries {
let entries: HashMap<Arc<str>, Arc<str>> = dict
.entries
.iter()
.filter(|e| !e.label.is_empty())
.map(|e| (e.value.clone(), e.label.clone()))
.collect();
if !entries.is_empty() {
labels.insert(name.clone(), entries);
}
}
if labels.is_empty() {
return None;
}
Some(std::rc::Rc::new(move |name: &str, value: &str| {
labels.get(name)?.get(value).map(|l| l.to_string())
}))
}

fn nl_scope(&self, cursor: &Cursor) -> Option<(ExpressionKind, VariableType)> {
let block = self.block_ir(&BlockRef {
policy_path: cursor.policy_path.clone(),
Expand Down
Loading
Loading