@particle-academy/fancy-conformance 0.6.0 → 0.9.1

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.
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "../../../schema/suite-manifest.schema.json",
3
+ "suite": "dark-slide/table-cell-model",
4
+ "title": "How a loose agent-authored table element resolves into per-cell decisions",
5
+ "since": "0.7.0",
6
+ "caseFormat": "table",
7
+ "cases": "cases.json",
8
+ "contract": {
9
+ "summary": "DarkSlide's three engines (PHP, Node, Python) turn one JSON table element into a pptx table. Between the two sits a pure resolver that decides, per CELL, every value the writer then serialises: fill, text colour and weight, alignment, vertical anchor, font size, letter spacing, capitalisation, the four insets, the four borders, and the two span directions. This suite pins that RESOLVER, not the XML. It exists because per-cell control was unreachable from the model in every engine — a:tcPr carried a fill and nothing else, header fill and zebra were hardcoded constants, and a declared column width was thrown away and replaced with an equal split. The resolver is where a port silently diverges, because two engines can emit well-formed XML from different decisions and no byte comparison of DIFFERENT inputs will notice.",
10
+ "functions": {
11
+ "resolvedCell": "resolvedCell(element, theme, row, col) -> the fully-resolved cell at that grid position. `row` counts the HEADER as row 0 when the table has one, so it addresses the emitted grid rather than the input's `rows` array.",
12
+ "gridWidthsEmu": "gridWidthsEmu(element, totalEmu) -> the <a:gridCol w> values, in order, as strings."
13
+ },
14
+ "runShape": {
15
+ "everyValueIsAString": "No golden in this file is a number. Fractional quantities are converted to the INTEGER unit the writer emits — font size and letter spacing in hundredths of a point, padding and border width in EMU — and then stringified. That removes float comparison from the suite entirely, which matters because the four fancy-conformance loaders do NOT agree on how a float golden is compared (PHP, Python and Rust use a scaled 1e-12 epsilon; the TypeScript loader uses exact Object.is). A suite whose verdict depends on which loader ran it is not a conformance suite.",
16
+ "resolvedCell": "{ text, bold, italic, underline, color, fill, align, anchor, fontSizeHundredths, letterSpacingHundredths, caps, padding: {left,right,top,bottom}, borders: {left,right,top,bottom}, colSpan, rowSpan, merged }. Booleans are \"1\" / \"0\". `fill` is null when the cell has none. Each border is null when the cell has none on that side, otherwise { widthEmu, color, style }.",
17
+ "gridWidthsEmu": "An ordered array of strings."
18
+ },
19
+ "reference": "php",
20
+ "referenceNote": "particle-academy/dark-slide is the reference: it shipped first and the trio's contract is 'the same document whichever backend runs it'. Every golden here is the output of RUNNING that engine, never what the value obviously ought to be — the width rows in particular are the accumulate-and-difference result rather than an independent division, and those differ.",
21
+ "implementations": [
22
+ { "language": "php", "package": "particle-academy/dark-slide", "symbol": "Table\\TableResolver::resolve" },
23
+ { "language": "node", "package": "@particle-academy/dark-slide", "symbol": "table/table-resolver.ts TableResolver.resolve" },
24
+ { "language": "python", "package": "fancy-dark-slide", "symbol": "dark_slide.table.table_resolver.resolve" }
25
+ ]
26
+ },
27
+ "notes": [
28
+ "PRECEDENCE IS THE POINT, and it is the half a byte comparison cannot reach. The chain is cell > row > column > band (header|stripe|body) > table > theme > default, and the rows here walk it one layer at a time (0009 through 0013) so a port that collapses two layers fails a NAMED case instead of shifting a colour somewhere in a large document. Byte parity across the three engines on a whole reference deck already covers the emission; it covers exactly ONE path through this chain.",
29
+ "AN ABSENT KEY AND A FALSE KEY ARE DIFFERENT, and conflating them is the most likely port bug in the whole model. Absent falls through to the next layer; `false` STOPS the chain and means off. `borders: false` on a table with a header must therefore beat the header band's own borders, and a row that sets `borders: {bottom: false}` must keep the other three sides. Cases 0005, 0006 and 0016 pin all three directions; a port written with `??` / `or` / `.get(k, default)` passes the first and fails the others.",
30
+ "COLUMN WIDTHS ARE TWO MODES CHOSEN BY THE VALUES. Every declared width <= 1 makes them FRACTIONS of the table and undeclared columns share the remainder; any declared width > 1 makes them WEIGHTS and undeclared columns weigh 1. No width anywhere is an equal split. Case 0018 pins the boundary at exactly 1.0, which is the value that decides the mode.",
31
+ "THE WIDTHS MUST SUM TO THE TABLE EXACTLY, so they are accumulated and differenced rather than each rounded independently. Case 0017's goldens are NOT totalEmu * frac rounded per column — 0019 exists specifically because independent rounding gives a different, plausible-looking answer that leaves the grid a few EMU off the frame.",
32
+ "SPANS ARE CLAMPED TO THE GRID, never emitted as written. A colSpan of 99 on a two-column table is a 2 (case 0020). A row with fewer <a:tc> than the grid declares is a corrupt file rather than a narrow table, so the covered cells are still emitted, carrying `merged` and an empty text.",
33
+ "THE SIBLING SUITE IS last-word/docx-constructs, and the two packages fail on the SAME axis — per-cell borders, insets, vertical alignment, merging — because both models were narrower than the XML their writers already emitted. That suite pins WordprocessingML output; this one pins a resolver. They are deliberately not the same shape: the XML cannot be shared (w:tcBorders / w:tcMar / w:vAlign against a:lnL..lnB / marL..marB / @anchor) but the DECISIONS can, and this file is expressed in points-and-hex for that reason. Promoting it to `shared/` is the right move the day last-word resolves through the same model; declaring it shared before then would list an implementation that does not run it, which is the failure this repository exists to prevent."
34
+ ]
35
+ }
@@ -0,0 +1,126 @@
1
+ {
2
+ "$schema": "../../../schema/cases.schema.json",
3
+ "suite": "flow/subflow-registry",
4
+ "cases": [
5
+ {
6
+ "id": "0001-host-kind-survives-nesting",
7
+ "title": "A kind the HOST registered is still resolvable inside the child.",
8
+ "since": "0.8.0",
9
+ "tags": ["inheritance", "regression"],
10
+ "input": {
11
+ "parent": { "@particle-academy/output": "builtin", "host_kind": "host" },
12
+ "injected": null,
13
+ "graphConfig": null
14
+ },
15
+ "expected": { "@particle-academy/output": "builtin", "host_kind": "host" }
16
+ },
17
+ {
18
+ "id": "0002-bare-builtins-are-not-enough",
19
+ "title": "A parent carrying several host kinds passes all of them down, not just the builtins.",
20
+ "since": "0.8.0",
21
+ "tags": ["inheritance", "regression"],
22
+ "input": {
23
+ "parent": {
24
+ "@particle-academy/output": "builtin",
25
+ "@particle-academy/transform": "builtin",
26
+ "agent": "host",
27
+ "invoice_lookup": "host",
28
+ "tenant_notify": "host"
29
+ },
30
+ "injected": null,
31
+ "graphConfig": null
32
+ },
33
+ "expected": {
34
+ "@particle-academy/output": "builtin",
35
+ "@particle-academy/transform": "builtin",
36
+ "agent": "host",
37
+ "invoice_lookup": "host",
38
+ "tenant_notify": "host"
39
+ }
40
+ },
41
+ {
42
+ "id": "0003-host-override-of-a-builtin-reaches-the-child",
43
+ "title": "A host that REPLACED a builtin keeps its own version inside the child.",
44
+ "since": "0.8.0",
45
+ "tags": ["inheritance", "override", "regression"],
46
+ "input": {
47
+ "parent": { "@particle-academy/llm_call": "host", "@particle-academy/output": "builtin" },
48
+ "injected": null,
49
+ "graphConfig": null
50
+ },
51
+ "expected": { "@particle-academy/llm_call": "host", "@particle-academy/output": "builtin" }
52
+ },
53
+ {
54
+ "id": "0004-graph-config-adds-to-the-inherited-set",
55
+ "title": "A registry the graph declares on the node is layered ON TOP of the inherited one.",
56
+ "since": "0.8.0",
57
+ "tags": ["config"],
58
+ "input": {
59
+ "parent": { "@particle-academy/output": "builtin", "host_kind": "host" },
60
+ "injected": null,
61
+ "graphConfig": { "child_only": "child-specific" }
62
+ },
63
+ "expected": {
64
+ "@particle-academy/output": "builtin",
65
+ "host_kind": "host",
66
+ "child_only": "child-specific"
67
+ }
68
+ },
69
+ {
70
+ "id": "0005-graph-config-wins-on-a-conflict",
71
+ "title": "An executor the graph names for its child beats the inherited one of the same kind.",
72
+ "since": "0.8.0",
73
+ "tags": ["config", "precedence"],
74
+ "input": {
75
+ "parent": { "@particle-academy/output": "builtin", "host_kind": "host" },
76
+ "injected": null,
77
+ "graphConfig": { "host_kind": "child-specific" }
78
+ },
79
+ "expected": { "@particle-academy/output": "builtin", "host_kind": "child-specific" }
80
+ },
81
+ {
82
+ "id": "0006-an-injected-registry-replaces-the-inherited-one",
83
+ "title": "A registry handed to the executor at construction is used INSTEAD of the parent's.",
84
+ "since": "0.8.0",
85
+ "tags": ["injection", "precedence"],
86
+ "input": {
87
+ "parent": { "@particle-academy/output": "builtin", "host_kind": "host" },
88
+ "injected": { "@particle-academy/output": "builtin", "sandboxed_kind": "host" },
89
+ "graphConfig": null
90
+ },
91
+ "expected": { "@particle-academy/output": "builtin", "sandboxed_kind": "host" }
92
+ },
93
+ {
94
+ "id": "0007-graph-config-still-layers-over-an-injected-registry",
95
+ "title": "Injection replaces the inherited registry; the graph's own still layers on top of that.",
96
+ "since": "0.8.0",
97
+ "tags": ["injection", "config", "precedence"],
98
+ "input": {
99
+ "parent": { "host_kind": "host" },
100
+ "injected": { "@particle-academy/output": "builtin" },
101
+ "graphConfig": { "child_only": "child-specific" }
102
+ },
103
+ "expected": { "@particle-academy/output": "builtin", "child_only": "child-specific" }
104
+ },
105
+ {
106
+ "id": "0008-an-empty-graph-config-changes-nothing",
107
+ "title": "An empty config registry is not the same as an absent one, and neither empties the child.",
108
+ "since": "0.8.0",
109
+ "tags": ["config", "edge"],
110
+ "input": {
111
+ "parent": { "@particle-academy/output": "builtin", "host_kind": "host" },
112
+ "injected": null,
113
+ "graphConfig": {}
114
+ },
115
+ "expected": { "@particle-academy/output": "builtin", "host_kind": "host" }
116
+ },
117
+ {
118
+ "id": "0009-an-empty-parent-yields-an-empty-child",
119
+ "title": "Inheritance copies what is there; it never invents builtins the parent did not have.",
120
+ "since": "0.8.0",
121
+ "tags": ["edge"],
122
+ "input": { "parent": {}, "injected": null, "graphConfig": null },
123
+ "expected": {}
124
+ }
125
+ ]
126
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../../schema/suite-manifest.schema.json",
3
+ "suite": "flow/subflow-registry",
4
+ "title": "What executors a subflow's child runs against",
5
+ "since": "0.8.0",
6
+ "caseFormat": "table",
7
+ "cases": "cases.json",
8
+ "contract": {
9
+ "function": "childRegistry(parent: object, injected: object|null, graphConfig: object|null) -> object",
10
+ "summary": "Compose the executor registry a `subflow` node runs its CHILD graph against. `parent` is the registry the parent run is executing with, keyed by kind id, whose values stand in for executors. `injected` is a registry handed to the subflow executor at construction, or null. `graphConfig` is the registry the graph itself declares on the subflow node's `config.executors`, or null. The result is the registry the child must resolve against, keyed by kind id.",
11
+ "reference": "node",
12
+ "referenceNote": "Designed here rather than lifted from an implementation, because at the time this suite was written all four runtimes were WRONG in the same way and none could serve as the reference. `node` is recorded as the reference because @particle-academy/fancy-flow's run-flow.ts is the behavioural source of truth for the engine generally.",
13
+ "implementations": [
14
+ { "language": "node", "package": "@particle-academy/fancy-flow", "symbol": "subflowExecutor" },
15
+ { "language": "php", "package": "particle-academy/fancy-flow-php", "symbol": "FancyFlow\\Nodes\\Structural\\SubflowExecutor" },
16
+ { "language": "python", "package": "fancy-flow", "symbol": "fancy_flow.nodes.structural.SubflowExecutor" },
17
+ { "language": "rust", "package": "fancy-flow", "symbol": "fancy_flow::nodes::structural::SubflowExecutor" }
18
+ ]
19
+ },
20
+ "notes": [
21
+ "This table exists because ALL FOUR runtimes had the same defect at once, and each of the three that were fixed carried its own hand-written test for it. Three copies of an assertion agree right up until someone changes one of them, and nothing anywhere reports the divergence — which is the same shape as the bug the table describes.",
22
+ "Reported against the PHP twin as fancy-flow-php#7 by a consumer running a real graph: a child containing a host-registered kind failed with `No executor registered for kind=<host-kind>`, while the identical graph run at top level succeeded. Same workflow, two behaviours depending on nesting depth.",
23
+ "The TypeScript engine had it with a WORSE default. Its child ran against `config.executors ?? {}` — an EMPTY registry unless the graph happened to carry one — so nesting a graph did not merely lose the host's kinds, it lost everything. PHP and Python fell back to the bare builtins. None of the three warned, because an unregistered kind fails closed with no outputs, which is the correct default and exactly what makes this silent.",
24
+ "The costly case is not a MISSING kind but a REPLACED one. A host that overrides `llm_call` to add tenancy, budgeting or token accounting gets its own version in the parent and the package's version in the child: the same graph billing two different ways depending on where it was invoked from, with nothing failing. Case 0003 is that case.",
25
+ "The contract is deliberately a function over REGISTRIES rather than a graph run. `flow/graph-runs` cannot ship a discrimination probe because the implementation under test there is an entire workflow engine, which this package does not carry and should not grow. Registry composition is a pure function over maps, so this suite CAN be probed — and is, in tests/discrimination.test.ts, by the exact mutant that shipped in production: a child that sees only the builtins.",
26
+ "Values in these maps are marker strings, not executors. `builtin`, `host` and `child-specific` stand for whichever callable a runtime would bind; what the table pins is WHICH one survives into the child, not what it does."
27
+ ]
28
+ }