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
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@
"devDependencies": {
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
"dependencies": {
"@ar-nelson/foldcase": "^1.0.1"
}
}
12 changes: 4 additions & 8 deletions src/decision.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CanonicalDirective } from './grammar.js';
import { unicodeCaseFold } from './unicode.js';

export const DECISION_ERROR = 'error' as const;
export const DECISION_NO_DIRECTIVE = 'no_directive' as const;
Expand Down Expand Up @@ -89,16 +90,11 @@ function formatFailure(failure: string, directive: CanonicalDirective): string {
}

function normalizeItemForMessage(value: string): string {
return value
const normalized = value
.normalize('NFKC')
.replaceAll('’', "'")
.replaceAll('`', "'")
.toLowerCase()
.replaceAll('ß', 'ss')
.replaceAll('ς', 'σ')
.replaceAll('ſ', 's')
.replace(/\s+/g, ' ')
.trim();
.replaceAll('`', "'");
return unicodeCaseFold(normalized).replace(/\s+/g, ' ').trim();
}

function freezeDirective(directive: CanonicalDirective): CanonicalDirective {
Expand Down
9 changes: 1 addition & 8 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SemanticFailure,
UpdateDecision
} from './decision.js';
import { unicodeCaseFold } from './unicode.js';

export const POLICY_USE = 'use' as const;
export const POLICY_PROHIBIT = 'prohibit' as const;
Expand Down Expand Up @@ -277,14 +278,6 @@ function normalizeItem(value: string): string {
return normalized.trim();
}

function unicodeCaseFold(value: string): string {
return value
.toLowerCase()
.replaceAll('ß', 'ss')
.replaceAll('ς', 'σ')
.replaceAll('ſ', 's');
}

function sortKeysDeep(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map((v) => sortKeysDeep(v));
Expand Down
6 changes: 2 additions & 4 deletions src/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class CanonicalDirective {
const kind = normalizeDirectiveKind(kindInput);
const operands = normalizeCanonicalOperands(kind, operandsInput);
const rendered = serializeCanonicalDirective(kind, operands);
if (kind !== DirectiveKind.SET_PREMISE && containsMultipleCanonicalDirectives(rendered)) {
if (containsMultipleCanonicalDirectives(rendered)) {
throw new Error(`Operands do not produce a canonical ${kind} directive.`);
}
this.kind = kind;
Expand Down Expand Up @@ -243,9 +243,7 @@ function parseReplacement(text: string): CanonicalDirective | null {
export function decompose_directive(text: string): CanonicalDirective | InvalidDirectiveSyntax | null {
const trimmed = trimAsciiWhitespace(text);
if (trimmed === '' || !startsWithDirectiveFamily(trimmed)) return null;
// A set-premise operand is opaque: directive-shaped text inside it is premise
// content, not a second directive.
if (!normalizedForMatching(trimmed).startsWith('set premise ') && containsMultipleCanonicalDirectives(trimmed)) {
if (containsMultipleCanonicalDirectives(trimmed)) {
return invalid(DirectiveSyntaxFailure.COMPOUND_DIRECTIVE);
}

Expand Down
6 changes: 6 additions & 0 deletions src/unicode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import foldcase from '@ar-nelson/foldcase';

/** Apply Unicode Default Case Folding (the full, potentially expanding form). */
export function unicodeCaseFold(value: string): string {
return foldcase.full(value);
}
12 changes: 12 additions & 0 deletions src/vendor.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare module '@ar-nelson/foldcase' {
interface Foldcase {
(value: string): string;
full(value: string): string;
simple(value: string): string;
charFull(value: string): string;
charSimple(value: string): string;
}

const foldcase: Foldcase;
export default foldcase;
}
9 changes: 5 additions & 4 deletions tests/api_parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type EngineMemberSpec = {
probes?: Array<{
args: unknown[];
raises?: { type: string };
rejects?: boolean;
}>;
};

Expand Down Expand Up @@ -401,7 +402,7 @@ describe('public API parity contract (conformance fixture)', () => {
return values.map(materializeProbeValue);
})();
const construct = () => Reflect.construct(cc.SemanticErrorDecision, args);
if (probe.raises != null) {
if (probe.raises != null || probe.rejects === true) {
expect(construct, `SemanticErrorDecision construction probe ${index} should raise`).toThrowError(TypeError);
continue;
}
Expand Down Expand Up @@ -434,8 +435,8 @@ describe('public API parity contract (conformance fixture)', () => {
return Reflect.construct(cc.Engine, constructorArgs);
};

if (probe.raises != null) {
if (probe.raises.type === 'TypeError') {
if (probe.raises != null || probe.rejects === true) {
if (probe.raises?.type === 'TypeError') {
expect(construct, `Engine construction probe ${index} should raise TypeError`).toThrowError(TypeError);
} else {
expect(construct, `Engine construction probe ${index} should raise`).toThrow();
Expand Down Expand Up @@ -529,7 +530,7 @@ describe('public API parity contract (conformance fixture)', () => {
for (const [index, probe] of (memberSpec.probes ?? []).entries()) {
const invoke = () =>
(engine as unknown as Record<string, (...args: unknown[]) => unknown>)[memberName](...probe.args);
if (probe.raises != null) {
if (probe.raises != null || probe.rejects === true) {
expect(invoke, `${memberName} probe ${index} should raise`).toThrow();
} else {
expect(invoke, `${memberName} probe ${index} should not raise`).not.toThrow();
Expand Down
4 changes: 3 additions & 1 deletion tests/apply-directive-fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe('apply-directive fixtures (conformance)', () => {
'function'
);

const directive = decompose_directive(fixture.payload.action.text);
const directive = fixture.payload.action.directive === undefined
? decompose_directive(fixture.payload.action.text as string)
: new CanonicalDirective(fixture.payload.action.directive.kind, fixture.payload.action.directive.operands as Record<string, string>);
expect(directive).toBeInstanceOf(CanonicalDirective);
const decision = (applyDirective as (value: CanonicalDirective) => unknown).call(engine, directive);
expect(decision).toEqual(fixture.payload.expected.decision);
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/.source-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ee49df7fdcd8965392da31d4ead37c7514d53d29
7ffc19c53ef5c41e4e421f4fd2958655e52737c9
13 changes: 0 additions & 13 deletions tests/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,6 @@ Portable serialization contract coverage for `engine.export_json()` and
`engine.import_json(...)`, including canonical export payload shape and
deterministic validation/error boundaries.

## Controller fixtures

For [`conformance/controller/`](conformance/controller/):

Portable controller contract coverage for:

* `step(engine, user_input)` result envelope and state snapshot
* `preview(engine, user_input)` result envelope, `would_mutate`, and non-mutation of live engine state
* `state_diff(state_before, state_after)` deterministic structural diff output

These fixtures keep a minimal, language-neutral contract matrix for controller APIs.

## Source of truth

Fixtures reflect current Python behavior and tests.
Expand Down Expand Up @@ -89,5 +77,4 @@ See the TypeScript fixture runners in this repository for execution details:

* [`step-fixtures.test.ts`](../step-fixtures.test.ts)
* [`state-json-fixtures.test.ts`](../state-json-fixtures.test.ts)
* [`controller-fixtures.test.ts`](../controller-fixtures.test.ts)
* [`structured-regression-fixtures.test.ts`](../structured-regression-fixtures.test.ts)
57 changes: 19 additions & 38 deletions tests/fixtures/conformance/api/public-api-v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"_NO_DIRECTIVE"
],
"forbidden_engine_members": [
"apply_transcript",
"export_checkpoint",
"export_checkpoint_json",
"has_pending_clarification",
Expand Down Expand Up @@ -99,17 +100,13 @@
"args": [
"unexpected"
],
"raises": {
"type": "TypeError"
}
"rejects": true
},
{
"kwargs": {
"value": "unexpected"
},
"raises": {
"type": "TypeError"
}
"rejects": true
}
]
},
Expand Down Expand Up @@ -179,9 +176,7 @@
},
{
"args": [],
"raises": {
"type": "TypeError"
}
"rejects": true
},
{
"kwargs": {
Expand All @@ -193,9 +188,7 @@
},
"unexpected": true
},
"raises": {
"type": "TypeError"
}
"rejects": true
}
]
},
Expand Down Expand Up @@ -252,17 +245,13 @@
},
{
"args": [],
"raises": {
"type": "TypeError"
}
"rejects": true
},
{
"kwargs": {
"value": true
},
"raises": {
"type": "TypeError"
}
"rejects": true
}
]
},
Expand Down Expand Up @@ -293,17 +282,13 @@
"args": [
"unexpected"
],
"raises": {
"type": "TypeError"
}
"rejects": true
},
{
"kwargs": {
"state": {}
},
"raises": {
"type": "TypeError"
}
"rejects": true
}
]
},
Expand Down Expand Up @@ -351,33 +336,25 @@
"args": [
"use docker"
],
"raises": {
"type": "AttributeError"
}
"rejects": true
},
{
"args": [
null
],
"raises": {
"type": "AttributeError"
}
"rejects": true
},
{
"args": [
1
],
"raises": {
"type": "AttributeError"
}
"rejects": true
},
{
"args": [
{}
],
"raises": {
"type": "AttributeError"
}
"rejects": true
}
]
},
Expand All @@ -394,10 +371,14 @@
}
},
"policies": {
"kind": "property"
"kind": "property",
"readable": true,
"writable": false
},
"premise": {
"kind": "property"
"kind": "property",
"readable": true,
"writable": false
},
"step": {
"kind": "method",
Expand Down
Loading
Loading