interaqt 1.5.7 → 1.5.8
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/agent/.claude/agents/bug-fix-handler.md +1 -0
- package/agent/.claude/agents/code-generation-handler.md +1 -0
- package/agent/.claude/agents/computation-generation-handler.md +2 -0
- package/agent/.claude/agents/error-check-handler.md +10 -9
- package/agent/.claude/agents/implement-design-handler.md +1 -0
- package/agent/agentspace/knowledge/generator/api-reference.md +59 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +2 -1
- package/agent/agentspace/knowledge/generator/computation-implementation.md +48 -3
- package/agent/agentspace/knowledge/generator/data-analysis.md +1 -1
- package/agent/agentspace/knowledge/generator/test-implementation.md +7 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +47 -1
- package/agent/agentspace/knowledge/usage/13-testing.md +8 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +57 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +6 -2
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +38 -0
- package/agent/agentspace/knowledge/usage/20-postgresql-concurrency-migration.md +31 -0
- package/agent/agentspace/knowledge/usage/README.md +1 -1
- package/agent/skill/interaqt-migration.md +54 -1
- package/agent/skill/interaqt-patterns.md +47 -2
- package/agent/skill/interaqt-recipes.md +120 -0
- package/agent/skill/interaqt-reference.md +66 -3
- package/dist/core/ScopedSequence.d.ts +100 -0
- package/dist/core/ScopedSequence.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/types.d.ts +3 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/drivers/PGLite.d.ts +9 -0
- package/dist/drivers/PGLite.d.ts.map +1 -1
- package/dist/drivers/PostgreSQL.d.ts +9 -0
- package/dist/drivers/PostgreSQL.d.ts.map +1 -1
- package/dist/drivers/SQLite.d.ts +9 -0
- package/dist/drivers/SQLite.d.ts.map +1 -1
- package/dist/index.js +6954 -6191
- package/dist/index.js.map +1 -1
- package/dist/runtime/Controller.d.ts.map +1 -1
- package/dist/runtime/MonoSystem.d.ts +4 -4
- package/dist/runtime/MonoSystem.d.ts.map +1 -1
- package/dist/runtime/Scheduler.d.ts +2 -1
- package/dist/runtime/Scheduler.d.ts.map +1 -1
- package/dist/runtime/System.d.ts +54 -4
- package/dist/runtime/System.d.ts.map +1 -1
- package/dist/runtime/computations/Computation.d.ts +1 -1
- package/dist/runtime/computations/Computation.d.ts.map +1 -1
- package/dist/runtime/computations/ScopedSequence.d.ts +19 -0
- package/dist/runtime/computations/ScopedSequence.d.ts.map +1 -0
- package/dist/runtime/computations/index.d.ts +2 -0
- package/dist/runtime/computations/index.d.ts.map +1 -1
- package/dist/runtime/migration.d.ts +88 -1
- package/dist/runtime/migration.d.ts.map +1 -1
- package/dist/runtime/scopedSequenceManifest.d.ts +31 -0
- package/dist/runtime/scopedSequenceManifest.d.ts.map +1 -0
- package/dist/runtime/scopedSequenceScope.d.ts +8 -0
- package/dist/runtime/scopedSequenceScope.d.ts.map +1 -0
- package/package.json +2 -1
|
@@ -27,6 +27,7 @@ This agent assumes the following project structure (configurable):
|
|
|
27
27
|
- Use Transform computations with `eventDeps` for creating entities/relations from interactions
|
|
28
28
|
- Always use `.name` property when querying entities/relations (never hardcode strings)
|
|
29
29
|
- Properties can have `defaultValue` OR `computation`, but not both
|
|
30
|
+
- Scoped serial numbers must use `ScopedSequence` on a number property plus a matching `UniqueConstraint`; never fix them with `StateMachine`, raw SQL, or `max + 1`
|
|
30
31
|
- Timestamp properties must use seconds: `Math.floor(Date.now()/1000)`
|
|
31
32
|
- Always specify `attributeQuery` parameter in storage queries
|
|
32
33
|
- For relations: use dot notation in matchExpression (`source.id`) and nested queries in attributeQuery
|
|
@@ -118,6 +118,7 @@ git commit -m "feat: Task 3.1.1 - Setup module file and register in index"
|
|
|
118
118
|
- **IMPORTANT: If a property will have `computed` or `computation`, do NOT set `defaultValue`**
|
|
119
119
|
- The computation will provide the value, defaultValue would conflict
|
|
120
120
|
- Either use defaultValue OR computation, never both
|
|
121
|
+
- For future scoped serial numbers, create the primitive scope fields now, but leave the serial property without `defaultValue`; `ScopedSequence` will be attached in the computation phase
|
|
121
122
|
- [ ] Generate all relations with proper cardinality
|
|
122
123
|
- Relations define how entities connect
|
|
123
124
|
- Relations create the property names for accessing related entities
|
|
@@ -180,6 +180,8 @@ color: blue
|
|
|
180
180
|
})
|
|
181
181
|
- Remove any `defaultValue` if adding computation to that property
|
|
182
182
|
- Never use Transform in Property computation
|
|
183
|
+
- Use `ScopedSequence` for scoped serial numbers (for example project + prefix + serialNumber). Do not generate `StateMachine`, `Custom`, raw SQL, or `max + 1` logic for this case.
|
|
184
|
+
- When generating `ScopedSequence`, ensure the property is `type: 'number'`, scope fields are already present on the created record, and a `UniqueConstraint` covers scope fields plus the sequence property.
|
|
183
185
|
- For `_owner` properties, always set them in the owner's creation/derivation logic
|
|
184
186
|
|
|
185
187
|
2. **Type Check**
|
|
@@ -263,15 +263,16 @@ Before starting any checks, create a comprehensive checklist document in `docs/{
|
|
|
263
263
|
- [ ] ERROR_CI_015: Test not checking all `ownerProperties` after entity creation
|
|
264
264
|
- [ ] ERROR_CI_016: Test not verifying all `createdWithRelations` were created
|
|
265
265
|
- [ ] ERROR_CI_017: StateMachine test not covering all StateTransfer transitions
|
|
266
|
-
- [ ] ERROR_CI_018:
|
|
267
|
-
- [ ] ERROR_CI_019:
|
|
268
|
-
- [ ] ERROR_CI_020: Tests
|
|
269
|
-
- [ ] ERROR_CI_021:
|
|
270
|
-
- [ ] ERROR_CI_022:
|
|
271
|
-
- [ ] ERROR_CI_023:
|
|
272
|
-
- [ ] ERROR_CI_024:
|
|
273
|
-
- [ ] ERROR_CI_025:
|
|
274
|
-
- [ ] ERROR_CI_026: **CRITICAL**: Computation
|
|
266
|
+
- [ ] ERROR_CI_018: Scoped serial number implemented with StateMachine/Custom/raw SQL/max+1 instead of ScopedSequence
|
|
267
|
+
- [ ] ERROR_CI_019: Type check not run before running tests
|
|
268
|
+
- [ ] ERROR_CI_020: Tests marked as completed but actually failing
|
|
269
|
+
- [ ] ERROR_CI_021: Tests skipped with `.skip()` or `.todo()`
|
|
270
|
+
- [ ] ERROR_CI_022: More than 10 fix attempts made without stopping
|
|
271
|
+
- [ ] ERROR_CI_023: Error document not created in `docs/errors/` after repeated failures
|
|
272
|
+
- [ ] ERROR_CI_024: `lastError` field not updated in implementation plan after failure
|
|
273
|
+
- [ ] ERROR_CI_025: Item marked `completed: true` but tests still failing
|
|
274
|
+
- [ ] ERROR_CI_026: **CRITICAL**: Computation uses mock/placeholder data instead of complete implementation
|
|
275
|
+
- [ ] ERROR_CI_027: **CRITICAL**: Computation contains side effects (email, AI calls, etc.) that should be in integration
|
|
275
276
|
|
|
276
277
|
**Check Results**: [To be filled]
|
|
277
278
|
|
|
@@ -138,6 +138,7 @@ When analyzing properties in `docs/{module}.data-design.json`:
|
|
|
138
138
|
- [ ] Create analysis document at `docs/{module}.computation-analysis.json`
|
|
139
139
|
- [ ] Analyze each entity systematically (creation source, update requirements, deletion strategy)
|
|
140
140
|
- [ ] Analyze each property individually (type, purpose, data source, update frequency)
|
|
141
|
+
- [ ] Use `ScopedSequence` for scoped serial/order-number properties; do not model them as StateMachine counters or Custom computations
|
|
141
142
|
- [ ] Analyze each relation's complete lifecycle (creation, updates, deletion)
|
|
142
143
|
- [ ] Select appropriate computation type based on decision trees
|
|
143
144
|
- [ ] Document reasoning for each computation decision
|
|
@@ -1560,6 +1560,65 @@ const lastRecomputeTime = await system.storage.dict.get(lastRecomputeTimeKey);
|
|
|
1560
1560
|
const nextRecomputeTime = await system.storage.dict.get(nextRecomputeTimeKey);
|
|
1561
1561
|
```
|
|
1562
1562
|
|
|
1563
|
+
### ScopedSequence.create()
|
|
1564
|
+
|
|
1565
|
+
Create a transactional scoped sequence allocator for a number property. Use it for serial numbers that must be unique inside a declared business scope.
|
|
1566
|
+
|
|
1567
|
+
**Syntax**
|
|
1568
|
+
```typescript
|
|
1569
|
+
ScopedSequence.create(config: ScopedSequenceConfig): ScopedSequenceInstance
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
**Parameters**
|
|
1573
|
+
- `config.name` (string, required): Sequence name, must match `/^[a-zA-Z0-9_]+$/`
|
|
1574
|
+
- `config.scope` (array, required): Ordered scope items:
|
|
1575
|
+
- Primitive: `{ name, type: 'string' | 'number' | 'boolean', path }`
|
|
1576
|
+
- Reference: `{ name, type: 'ref', base: EntityInstance, path }`
|
|
1577
|
+
- `config.initialValue` (number, optional): Defaults to `0`; first automatic allocation returns `initialValue + step`
|
|
1578
|
+
- `config.step` (positive integer, optional): Defaults to `1`
|
|
1579
|
+
- `config.allowManualValue` (boolean, optional): Defaults to `false`; if true, provided values are preserved and do not advance the counter
|
|
1580
|
+
- `config.initializeFrom` (optional): Migration seed declaration `{ record, valuePath, scope, aggregate: 'max' }`
|
|
1581
|
+
|
|
1582
|
+
**Example**
|
|
1583
|
+
```typescript
|
|
1584
|
+
const assetSerial = ScopedSequence.create({
|
|
1585
|
+
name: 'projectAssetSerial',
|
|
1586
|
+
scope: [
|
|
1587
|
+
{ name: 'project', type: 'ref', base: Project, path: 'project' },
|
|
1588
|
+
{ name: 'prefix', type: 'string', path: 'prefix' }
|
|
1589
|
+
]
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
const Media = Entity.create({
|
|
1593
|
+
name: 'Media',
|
|
1594
|
+
properties: [
|
|
1595
|
+
Property.create({ name: 'project', type: 'id' }),
|
|
1596
|
+
Property.create({ name: 'prefix', type: 'string' }),
|
|
1597
|
+
Property.create({
|
|
1598
|
+
name: 'serialNumber',
|
|
1599
|
+
type: 'number',
|
|
1600
|
+
computation: assetSerial
|
|
1601
|
+
})
|
|
1602
|
+
],
|
|
1603
|
+
constraints: [
|
|
1604
|
+
UniqueConstraint.create({
|
|
1605
|
+
name: 'uniqMediaProjectPrefixSerial',
|
|
1606
|
+
properties: ['project', 'prefix', 'serialNumber']
|
|
1607
|
+
})
|
|
1608
|
+
]
|
|
1609
|
+
})
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
**Generation rules**
|
|
1613
|
+
- Only place `ScopedSequence` on `Property.computation`, and only when the property type is `number`.
|
|
1614
|
+
- Do not add `defaultValue` to the same property.
|
|
1615
|
+
- Do not use a non-null insert-time constraint for the sequence property; allocation is post-create/pre-commit.
|
|
1616
|
+
- Scope paths must read stable primitive/ref values already present on the created host record.
|
|
1617
|
+
- Do not use relation traversal, database queries, or another post-create computation as scope input.
|
|
1618
|
+
- Always add a `UniqueConstraint` over the scope fields plus the allocated property.
|
|
1619
|
+
- Do not generate `StateMachine`, `Custom`, raw SQL, or `max + 1` logic for scoped serial numbers.
|
|
1620
|
+
- For existing data, use `initializeFrom` to seed every existing scope with `MAX(valuePath)`. Do not partial-seed with `initializeFrom.match` unless the sequence will only ever allocate for that exact subset.
|
|
1621
|
+
|
|
1563
1622
|
### Custom.create()
|
|
1564
1623
|
|
|
1565
1624
|
Create custom computation with completely user-defined calculation logic.
|
|
@@ -73,7 +73,7 @@ Check `lifecycle.creation` and `lifecycle.deletion`:
|
|
|
73
73
|
|
|
74
74
|
**🔴 CRITICAL RULE: Properties can NEVER use Transform computation**
|
|
75
75
|
- Transform is ONLY for Entity/Relation creation
|
|
76
|
-
- Properties must use: _owner, StateMachine, computed, aggregations (Count/Sum/etc.), or Custom
|
|
76
|
+
- Properties must use: _owner, StateMachine, ScopedSequence, computed, aggregations (Count/Sum/etc.), or Custom
|
|
77
77
|
- Even if a property needs to respond to external events (like Integration Events), use StateMachine with appropriate triggers
|
|
78
78
|
|
|
79
79
|
First check the property's `controlType`, then analyze dependencies if needed:
|
|
@@ -92,6 +92,7 @@ Analyze the property's `dataDependencies`, `interactionDependencies`, and `compu
|
|
|
92
92
|
| Condition | Computation Decision |
|
|
93
93
|
|-----------|---------------------|
|
|
94
94
|
| `calculationMethod` contains "sum of", "count of", "aggregate", or involves Record entities | `Custom` or aggregation (e.g., balance = sum(deposits) - sum(withdrawals)) |
|
|
95
|
+
| Needs a serial/order number scoped by other record fields (e.g. project + prefix) | `ScopedSequence` |
|
|
95
96
|
| Has `interactionDependencies` that can modify it | `StateMachine` for state transitions or value updates (even for external events) |
|
|
96
97
|
| Has `dataDependencies` with relations/entities (including Integration Events) | `StateMachine` if triggered by events, otherwise aggregation computation |
|
|
97
98
|
| `dataDependencies` = self properties only | `computed` function |
|
|
@@ -570,9 +570,10 @@ import { Custom, Dictionary, GlobalBoundState, Entity, Property, Relation } from
|
|
|
570
570
|
Before using Custom computation, ask yourself:
|
|
571
571
|
1. Can I use Transform for entity/relation creation? → Use Transform
|
|
572
572
|
2. Can I use StateMachine for updates? → Use StateMachine
|
|
573
|
-
3.
|
|
574
|
-
4. Can I use
|
|
575
|
-
5. Can I
|
|
573
|
+
3. Do I need a scoped serial number allocated on create? → Use ScopedSequence
|
|
574
|
+
4. Can I use Count/Summation/Every/Any for aggregations? → Use those
|
|
575
|
+
5. Can I use computed/getValue for simple calculations? → Use those
|
|
576
|
+
6. Can I combine existing computations? → Combine them
|
|
576
577
|
|
|
577
578
|
**Only use Custom when:**
|
|
578
579
|
- You need complex business logic that doesn't fit ANY existing computation type
|
|
@@ -741,6 +742,50 @@ When using `type: 'property'` with Custom computation:
|
|
|
741
742
|
|
|
742
743
|
**Remember:** The power of interaqt comes from its declarative computations. Custom computation breaks this paradigm and should only be used when absolutely necessary. Always try to express your logic using the standard computation types first!
|
|
743
744
|
|
|
745
|
+
### 4. ScopedSequence - Scoped Atomic Serial Numbers
|
|
746
|
+
|
|
747
|
+
Use `ScopedSequence` when a property should receive the next number within a declared business scope during record creation.
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
import { Entity, Property, ScopedSequence, UniqueConstraint } from 'interaqt';
|
|
751
|
+
|
|
752
|
+
const Asset = Entity.create({
|
|
753
|
+
name: 'Asset',
|
|
754
|
+
properties: [
|
|
755
|
+
Property.create({ name: 'project', type: 'id' }),
|
|
756
|
+
Property.create({ name: 'prefix', type: 'string' }),
|
|
757
|
+
Property.create({
|
|
758
|
+
name: 'serialNumber',
|
|
759
|
+
type: 'number',
|
|
760
|
+
computation: ScopedSequence.create({
|
|
761
|
+
name: 'projectAssetSerial',
|
|
762
|
+
scope: [
|
|
763
|
+
{ name: 'project', type: 'ref', base: Project, path: 'project' },
|
|
764
|
+
{ name: 'prefix', type: 'string', path: 'prefix' },
|
|
765
|
+
],
|
|
766
|
+
}),
|
|
767
|
+
}),
|
|
768
|
+
],
|
|
769
|
+
constraints: [
|
|
770
|
+
UniqueConstraint.create({
|
|
771
|
+
name: 'uniqAssetProjectPrefixSerial',
|
|
772
|
+
properties: ['project', 'prefix', 'serialNumber'],
|
|
773
|
+
}),
|
|
774
|
+
],
|
|
775
|
+
});
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
Generation rules:
|
|
779
|
+
|
|
780
|
+
- Only generate `ScopedSequence` on a `number` property.
|
|
781
|
+
- Do not add `defaultValue` to the same property.
|
|
782
|
+
- Ensure every scope path is present on the new host record before allocation. Populate scope inputs in the Transform/interaction data that creates the host record.
|
|
783
|
+
- Do not derive the scope from relation traversal or a query.
|
|
784
|
+
- Do not generate `StateMachine`, `Custom`, raw SQL, or `max + 1` allocation for scoped serials.
|
|
785
|
+
- Always generate a `UniqueConstraint` covering scope fields plus the allocated property.
|
|
786
|
+
- If existing rows already have serial values, add `initializeFrom` so migration can seed `MAX(serialNumber)` per scope.
|
|
787
|
+
- Do not use `initializeFrom.match` for partial seeding unless the sequence property will only allocate for that exact subset forever.
|
|
788
|
+
|
|
744
789
|
## Implementation Steps
|
|
745
790
|
|
|
746
791
|
### Step 1: Entity Creation Pattern
|
|
@@ -374,7 +374,7 @@ Search `requirements/{module}.interactions-design.json` (using the module name f
|
|
|
374
374
|
Transform the computation description using semantic best practices:
|
|
375
375
|
- **Don't copy directly** from `requirements/{module}.data-concepts.json`
|
|
376
376
|
- Apply the "Best Practices for Computation Design" principles
|
|
377
|
-
- Use semantic computations (Count, Every, Any, Summation, etc.) where possible
|
|
377
|
+
- Use semantic computations (Count, Every, Any, Summation, ScopedSequence, etc.) where possible
|
|
378
378
|
- Decompose complex calculations into intermediate properties
|
|
379
379
|
- Make the computation intent clear and implementation-ready
|
|
380
380
|
|
|
@@ -182,6 +182,13 @@ type RecordMutationEvent = {
|
|
|
182
182
|
- **Effects are useful for**: Getting auto-generated IDs, tracking all mutations in complex interactions, debugging what changed
|
|
183
183
|
- **Storage queries are better for**: Verifying computed properties, checking related data, confirming final state
|
|
184
184
|
|
|
185
|
+
**ScopedSequence testing:**
|
|
186
|
+
- Create records through `controller.dispatch` / `controller.callInteraction`, not direct `storage.create`, so the post-create/pre-commit allocator runs.
|
|
187
|
+
- Verify first value semantics (`initialValue + step`), independent scopes, manual value rejection, and uniqueness.
|
|
188
|
+
- For rollback/delete behavior, assert the next successful allocation follows the documented transactional counter semantics.
|
|
189
|
+
- For existing data migration, assert every existing scope is seeded to `MAX(serialNumber)` and the next allocation returns `max + step`.
|
|
190
|
+
- For PostgreSQL release coverage, run `INTERAQT_POSTGRES_DATABASE=interaqt_test npm run test:postgres-scoped-sequence`.
|
|
191
|
+
|
|
185
192
|
#### 🔴 IMPORTANT: Use Storage APIs for Verification
|
|
186
193
|
When testing interactions, **directly use storage.find/findOne to verify results**. DO NOT create query interactions just for testing purposes:
|
|
187
194
|
|
|
@@ -83,12 +83,58 @@ Reactive computation is a **declarative way of defining data**:
|
|
|
83
83
|
|
|
84
84
|
### PostgreSQL Concurrency Guarantees
|
|
85
85
|
|
|
86
|
-
Built-in computations such as Count, Summation, Average, Every, Any, WeightedSummation, StateMachine, and data-based Transform use atomic state updates, row locks,
|
|
86
|
+
Built-in computations such as Count, Summation, Average, Every, Any, WeightedSummation, StateMachine, ScopedSequence, and data-based Transform use atomic state updates, row locks, unique indexes, or scoped sequence rows on PostgreSQL. They are safe when multiple Node.js processes share one PostgreSQL database.
|
|
87
87
|
|
|
88
88
|
Custom computations are different because interaqt cannot inspect user callback logic. `Custom.create()` defaults to `concurrency: 'serializable'`, so the framework promotes the transaction to PostgreSQL `SERIALIZABLE` and retries on `40001` / `40P01`. Use `concurrency: 'atomic-safe'` only when the custom computation is explicitly written with atomic state, idempotent patches, or other safe primitives.
|
|
89
89
|
|
|
90
90
|
Because retry replays the transaction attempt, `guard`, `mapEventData`, `resolve`, computation callbacks, `afterDispatch`, and `asyncReturn` must be deterministic and retry-safe. Put irreversible external IO in `postCommit` for interaction-specific effects or `recordMutationSideEffects` for mutation-driven effects; both run after the final commit.
|
|
91
91
|
|
|
92
|
+
### Scoped Atomic Sequences
|
|
93
|
+
|
|
94
|
+
Use `ScopedSequence` for per-scope serial numbers such as "next media number per project and prefix". This is a property computation, not a service call and not a `StateMachine(lastValue + 1)`.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { Entity, Property, ScopedSequence, UniqueConstraint } from 'interaqt'
|
|
98
|
+
|
|
99
|
+
const assetSerial = ScopedSequence.create({
|
|
100
|
+
name: 'projectAssetSerial',
|
|
101
|
+
scope: [
|
|
102
|
+
{ name: 'project', type: 'ref', base: Project, path: 'project' },
|
|
103
|
+
{ name: 'prefix', type: 'string', path: 'prefix' },
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const Media = Entity.create({
|
|
108
|
+
name: 'Media',
|
|
109
|
+
properties: [
|
|
110
|
+
Property.create({ name: 'project', type: 'id' }),
|
|
111
|
+
Property.create({ name: 'prefix', type: 'string' }),
|
|
112
|
+
Property.create({
|
|
113
|
+
name: 'serialNumber',
|
|
114
|
+
type: 'number',
|
|
115
|
+
computation: assetSerial,
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
constraints: [
|
|
119
|
+
UniqueConstraint.create({
|
|
120
|
+
name: 'uniqMediaProjectPrefixSerial',
|
|
121
|
+
properties: ['project', 'prefix', 'serialNumber'],
|
|
122
|
+
}),
|
|
123
|
+
],
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Rules:
|
|
128
|
+
|
|
129
|
+
- The host property must be `type: 'number'`.
|
|
130
|
+
- Do not set a `defaultValue` on the same property. The allocator supplies the value after the create mutation and before commit.
|
|
131
|
+
- Scope paths can only read stable primitive/ref values already present on the created record.
|
|
132
|
+
- Missing or type-mismatched scope values are errors.
|
|
133
|
+
- `allowManualValue: true` is for import/backfill and does not advance the counter.
|
|
134
|
+
- Existing data migration should use `initializeFrom` to seed every scope with `MAX(serialNumber)`.
|
|
135
|
+
- Keep the `UniqueConstraint`; the allocator prevents normal duplicates, and the constraint is the integrity backstop.
|
|
136
|
+
- PostgreSQL is the production-safe cross-process driver for this feature.
|
|
137
|
+
|
|
92
138
|
### Core Principle: Data Existence
|
|
93
139
|
|
|
94
140
|
In interaqt, all data has its "reason for existence":
|
|
@@ -149,6 +149,14 @@ INTERAQT_POSTGRES_DATABASE=interaqt_test npm run test:postgres-concurrency
|
|
|
149
149
|
|
|
150
150
|
This script fails when the database environment variable is missing. Plain `npm test` may skip PostgreSQL-specific tests in local lightweight environments.
|
|
151
151
|
|
|
152
|
+
For `ScopedSequence` changes, also run:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
INTERAQT_POSTGRES_DATABASE=interaqt_test npm run test:postgres-scoped-sequence
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Scoped sequence tests must cover allocation through `controller.dispatch`, not direct `storage.create`. Include first-value semantics, multi-scope isolation, manual import behavior, rollback/delete behavior, migration seeding from existing data, and real PostgreSQL two-controller concurrency.
|
|
159
|
+
|
|
152
160
|
### Key API Methods
|
|
153
161
|
|
|
154
162
|
#### 1. Controller APIs
|
|
@@ -525,6 +525,63 @@ Create a custom computation when built-in computations are not expressive enough
|
|
|
525
525
|
|
|
526
526
|
Custom callbacks may be replayed after retryable transaction failures. Do not perform irreversible external IO inside custom `compute`, `incrementalCompute`, `incrementalPatchCompute`, or `asyncReturn`; use `recordMutationSideEffects` for post-commit external work.
|
|
527
527
|
|
|
528
|
+
### ScopedSequence.create()
|
|
529
|
+
|
|
530
|
+
Create a transactional per-scope sequence allocator for a `number` property.
|
|
531
|
+
|
|
532
|
+
**Syntax**
|
|
533
|
+
```typescript
|
|
534
|
+
ScopedSequence.create(config: ScopedSequenceConfig): KlassInstance<typeof ScopedSequence>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Parameters**
|
|
538
|
+
- `config.name` (string, required): Sequence name, must match `/^[a-zA-Z0-9_]+$/`
|
|
539
|
+
- `config.scope` (array, required): Ordered scope items. Primitive items use `{ name, type: 'string' | 'number' | 'boolean', path }`; ref items use `{ name, type: 'ref', base: Entity, path }`
|
|
540
|
+
- `config.initialValue` (number, optional): Defaults to `0`; first automatic allocation returns `initialValue + step`
|
|
541
|
+
- `config.step` (positive integer, optional): Defaults to `1`
|
|
542
|
+
- `config.allowManualValue` (boolean, optional): Defaults to `false`; when true, a provided value is preserved and does not advance the counter
|
|
543
|
+
- `config.initializeFrom` (object, optional): Migration seed declaration using `{ record, valuePath, scope, aggregate: 'max' }`
|
|
544
|
+
|
|
545
|
+
**Examples**
|
|
546
|
+
```typescript
|
|
547
|
+
const assetSerial = ScopedSequence.create({
|
|
548
|
+
name: 'projectAssetSerial',
|
|
549
|
+
scope: [
|
|
550
|
+
{ name: 'project', type: 'ref', base: Project, path: 'project' },
|
|
551
|
+
{ name: 'prefix', type: 'string', path: 'prefix' }
|
|
552
|
+
],
|
|
553
|
+
initialValue: 0,
|
|
554
|
+
step: 1
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const Media = Entity.create({
|
|
558
|
+
name: 'Media',
|
|
559
|
+
properties: [
|
|
560
|
+
Property.create({ name: 'project', type: 'id' }),
|
|
561
|
+
Property.create({ name: 'prefix', type: 'string' }),
|
|
562
|
+
Property.create({
|
|
563
|
+
name: 'serialNumber',
|
|
564
|
+
type: 'number',
|
|
565
|
+
computation: assetSerial
|
|
566
|
+
})
|
|
567
|
+
],
|
|
568
|
+
constraints: [
|
|
569
|
+
UniqueConstraint.create({
|
|
570
|
+
name: 'uniqMediaProjectPrefixSerial',
|
|
571
|
+
properties: ['project', 'prefix', 'serialNumber']
|
|
572
|
+
})
|
|
573
|
+
]
|
|
574
|
+
})
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Important semantics**
|
|
578
|
+
- `ScopedSequence` only belongs on `Property.computation`, and the property must be `type: 'number'`.
|
|
579
|
+
- It is post-create/pre-commit, not an insert-time database default. Do not combine it with a non-null insert-time requirement.
|
|
580
|
+
- Scope paths must read stable values already present on the newly created host record.
|
|
581
|
+
- Keep a `UniqueConstraint` over scope fields plus the sequence property.
|
|
582
|
+
- PostgreSQL is production-safe for cross-connection allocation; PGLite and SQLite are local/test-level only.
|
|
583
|
+
- Existing data migrations must seed every future allocation scope. Do not use `initializeFrom.match` to seed only part of a sequence that will allocate for all host rows.
|
|
584
|
+
|
|
528
585
|
### StateMachine.create()
|
|
529
586
|
|
|
530
587
|
Create state machine computation.
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
Summation,
|
|
33
33
|
WeightedSummation,
|
|
34
34
|
Transform,
|
|
35
|
+
ScopedSequence,
|
|
35
36
|
StateMachine,
|
|
36
37
|
StateNode,
|
|
37
38
|
StateTransfer,
|
|
@@ -154,6 +155,7 @@ import {
|
|
|
154
155
|
Summation,
|
|
155
156
|
WeightedSummation,
|
|
156
157
|
Transform,
|
|
158
|
+
ScopedSequence,
|
|
157
159
|
StateMachine,
|
|
158
160
|
StateNode,
|
|
159
161
|
StateTransfer
|
|
@@ -200,8 +202,10 @@ const controller = new Controller({
|
|
|
200
202
|
|
|
201
203
|
4. **Filtered Entities**: Created using `Entity.create()` with `baseEntity` and `filterCondition`, not a separate import.
|
|
202
204
|
|
|
203
|
-
5. **Database Drivers**: Choose one based on your needs - PGLiteDB for in-memory testing, PostgreSQLDB for production, etc.
|
|
205
|
+
5. **Database Drivers**: Choose one based on your needs - PGLiteDB for in-memory testing, PostgreSQLDB for production, etc. `ScopedSequence` is production-safe for cross-connection/cross-process allocation on PostgreSQL; PGLiteDB and SQLiteDB are local/test-level only for scoped sequence concurrency.
|
|
204
206
|
|
|
205
207
|
6. **Transaction helpers**: `runWithTransactionRetry`, `isRetryableTransactionError`, and `isRequireSerializableRetry` are exported for advanced runtime integrations and tests. Most application code should use `Controller.dispatch()` or `system.storage.runInTransaction()` instead of calling retry helpers directly.
|
|
206
208
|
|
|
207
|
-
7. **Constraint helpers**: `UniqueConstraint`, `ConstraintViolationError`, `ConstraintSetupError`, `findConstraintViolationError`, and `normalizeDatabaseError` are exported for schema-level uniqueness and stable duplicate handling.
|
|
209
|
+
7. **Constraint helpers**: `UniqueConstraint`, `ConstraintViolationError`, `ConstraintSetupError`, `findConstraintViolationError`, and `normalizeDatabaseError` are exported for schema-level uniqueness and stable duplicate handling.
|
|
210
|
+
|
|
211
|
+
8. **Scoped serial allocation**: `ScopedSequence` is exported for number property computations that allocate per-scope serials. Always pair it with a `UniqueConstraint` over the scope fields plus the sequence property.
|
|
@@ -255,6 +255,44 @@ StateMachine.create({
|
|
|
255
255
|
});
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
+
### ❌ Using StateMachine or max+1 for Scoped Serial Numbers
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// ❌ WRONG: not safe across PostgreSQL connections
|
|
262
|
+
Property.create({
|
|
263
|
+
name: 'serialNumber',
|
|
264
|
+
type: 'number',
|
|
265
|
+
computation: StateMachine.create({
|
|
266
|
+
// lastValue + 1 is per-record state logic, not a transactional scoped counter
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ❌ WRONG: "SELECT MAX(serialNumber) + 1" races under concurrency
|
|
271
|
+
async function allocateSerial(projectId, prefix) {
|
|
272
|
+
const max = await db.query('select max(serialNumber) ...');
|
|
273
|
+
return max + 1;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
// ✅ CORRECT: declare the allocation as a property computation
|
|
279
|
+
const serial = ScopedSequence.create({
|
|
280
|
+
name: 'projectAssetSerial',
|
|
281
|
+
scope: [
|
|
282
|
+
{ name: 'project', type: 'ref', base: Project, path: 'project' },
|
|
283
|
+
{ name: 'prefix', type: 'string', path: 'prefix' }
|
|
284
|
+
]
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
Property.create({
|
|
288
|
+
name: 'serialNumber',
|
|
289
|
+
type: 'number',
|
|
290
|
+
computation: serial
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Always pair `ScopedSequence` with a `UniqueConstraint` over the scope fields plus the sequence property. For existing data, seed every scope through migration `initializeFrom`; do not partially seed a sequence that will later allocate for all host rows.
|
|
295
|
+
|
|
258
296
|
## 6. Testing Mistakes
|
|
259
297
|
|
|
260
298
|
### ❌ Using try-catch for Error Testing
|
|
@@ -8,6 +8,7 @@ interaqt now uses PostgreSQL as the concurrency coordinator for multi-process de
|
|
|
8
8
|
|
|
9
9
|
- Built-in computations use atomic state updates, row locks, unique indexes, or SERIALIZABLE retry where needed.
|
|
10
10
|
- PostgreSQL id generation uses native sequences instead of writing `_IDS_`.
|
|
11
|
+
- `ScopedSequence` uses an internal `_ScopedSequence_` table plus transactional `INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING` to allocate per-scope serial numbers safely across controllers and processes.
|
|
11
12
|
- Custom computations default to retryable `SERIALIZABLE` execution.
|
|
12
13
|
- Async return handling runs in a retryable transaction and applies freshness checks.
|
|
13
14
|
|
|
@@ -79,6 +80,28 @@ Do not write `_IDS_` to control ids. `_IDS_` is legacy input only.
|
|
|
79
80
|
|
|
80
81
|
PostgreSQL sequence gaps are normal after rollback or failed transactions. Do not depend on contiguous ids.
|
|
81
82
|
|
|
83
|
+
## ScopedSequence Atomic Counters
|
|
84
|
+
|
|
85
|
+
Use `ScopedSequence` when a number property needs a unique serial inside a business scope, for example `project + prefix + serialNumber`.
|
|
86
|
+
|
|
87
|
+
Key semantics:
|
|
88
|
+
|
|
89
|
+
- Allocation happens after the host record create mutation and before the dispatch transaction commits.
|
|
90
|
+
- First automatic value is `initialValue + step`.
|
|
91
|
+
- Rollback rolls back the sequence increment because counter state and business writes share the same transaction.
|
|
92
|
+
- Deleting a record does not decrement the counter.
|
|
93
|
+
- Manual values are rejected unless `allowManualValue: true`.
|
|
94
|
+
- `allowManualValue: true` is for import/backfill only and does not advance the counter.
|
|
95
|
+
- Keep a `UniqueConstraint` over the scope fields and allocated property as the database integrity backstop.
|
|
96
|
+
- PostgreSQL is the production-safe driver for cross-connection/cross-process allocation. PGLite and SQLite are test/local single-process options only.
|
|
97
|
+
|
|
98
|
+
Migration rules:
|
|
99
|
+
|
|
100
|
+
- Adding or changing a `ScopedSequence` is not an ordinary recompute. Approve it as unrebuildable plus a scoped sequence seed/no-seed decision.
|
|
101
|
+
- Use `initializeFrom` to seed counters from existing values with `MAX(valuePath)` per scope.
|
|
102
|
+
- Do not partial-seed with `initializeFrom.match` when future allocations cover all host rows; otherwise unseeded existing scopes can later collide.
|
|
103
|
+
- Removing a declared `ScopedSequence` must be reviewed explicitly. Do not silently drop the declaration from the manifest while leaving internal counter state behind.
|
|
104
|
+
|
|
82
105
|
## Transaction API
|
|
83
106
|
|
|
84
107
|
Use callback transactions:
|
|
@@ -103,3 +126,11 @@ INTERAQT_POSTGRES_DATABASE=interaqt_test npm run test:postgres-concurrency
|
|
|
103
126
|
```
|
|
104
127
|
|
|
105
128
|
This script intentionally fails when `INTERAQT_POSTGRES_DATABASE` is missing, so PostgreSQL concurrency coverage cannot silently skip in CI.
|
|
129
|
+
|
|
130
|
+
For `ScopedSequence`, also run:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
INTERAQT_POSTGRES_DATABASE=interaqt_test npm run test:postgres-scoped-sequence
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This is the critical acceptance test for cross-controller scoped counter allocation.
|
|
@@ -142,7 +142,7 @@ Remember: **Stop thinking "how to do", start thinking "what is"**!
|
|
|
142
142
|
18. [Performance Optimization](./17-performance-optimization.md) - Performance tips
|
|
143
143
|
19. [API Exports Reference](./18-api-exports-reference.md) - Complete list of available imports
|
|
144
144
|
20. [Common Anti-Patterns](./19-common-anti-patterns.md) - Mistakes to avoid
|
|
145
|
-
21. [PostgreSQL Concurrency Migration](./20-postgresql-concurrency-migration.md) - Retry, async, and
|
|
145
|
+
21. [PostgreSQL Concurrency Migration](./20-postgresql-concurrency-migration.md) - Retry, async, PostgreSQL ids, and ScopedSequence migration notes
|
|
146
146
|
|
|
147
147
|
## 📞 Need Help?
|
|
148
148
|
|
|
@@ -15,6 +15,8 @@ Phase 1.5 uses a two-step review workflow:
|
|
|
15
15
|
|
|
16
16
|
Migration supports additive schema changes, changed/new computation recompute, downstream propagation, filtered membership rebuild, destructive-scope review, post-backfill constraint verification, and explicit fact-to-computation takeover.
|
|
17
17
|
|
|
18
|
+
`ScopedSequence` is migration-managed state, not a recomputable derivation. Adding or changing a scoped sequence requires an explicit seed/no-seed decision, and removing a scoped sequence declaration must be treated as an explicit migration review item because existing `_ScopedSequence_` counter rows are internal state and must not be silently discarded.
|
|
19
|
+
|
|
18
20
|
Phase 1.5 does not guess or execute rename/copy/merge/split primitives. Rename candidates may be recorded for review, but compute-route migration will still obey physical layout and destructive-change safety gates.
|
|
19
21
|
|
|
20
22
|
---
|
|
@@ -275,6 +277,54 @@ Review checklist for takeover:
|
|
|
275
277
|
|
|
276
278
|
---
|
|
277
279
|
|
|
280
|
+
## ScopedSequence Migration
|
|
281
|
+
|
|
282
|
+
`ScopedSequence` allocates transactional per-scope counters for number properties. It is intentionally not full-recomputable: migration must seed or explicitly approve empty-state initialization instead of calling the computation for existing rows.
|
|
283
|
+
|
|
284
|
+
When adding a `ScopedSequence` to an existing property:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
Property.create({
|
|
288
|
+
name: 'serialNumber',
|
|
289
|
+
type: 'number',
|
|
290
|
+
computation: ScopedSequence.create({
|
|
291
|
+
name: 'projectAssetSerial',
|
|
292
|
+
scope: [
|
|
293
|
+
{ name: 'project', type: 'ref', base: Project, path: 'project' },
|
|
294
|
+
{ name: 'prefix', type: 'string', path: 'prefix' },
|
|
295
|
+
],
|
|
296
|
+
initializeFrom: {
|
|
297
|
+
record: Media,
|
|
298
|
+
valuePath: 'serialNumber',
|
|
299
|
+
scope: [
|
|
300
|
+
{ name: 'project', path: 'project' },
|
|
301
|
+
{ name: 'prefix', path: 'prefix' },
|
|
302
|
+
],
|
|
303
|
+
aggregate: 'max',
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
})
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Review rules:
|
|
310
|
+
|
|
311
|
+
- Approve the normal computation review as `decision: 'unrebuildable'`.
|
|
312
|
+
- If `initializeFrom` is present, approve the matching `scoped-sequence-seed` decision.
|
|
313
|
+
- If `initializeFrom` is absent, approve `scoped-sequence-no-seed` only when the host table is empty and the diff reports `expectedHostCount: 0`.
|
|
314
|
+
- `initializeFrom.valuePath` must match the target property and every matched existing row must have a valid numeric value.
|
|
315
|
+
- Seed every existing scope that the future property will allocate for. Do not use `initializeFrom.match` to seed only a subset while leaving other existing scopes with unseeded serials.
|
|
316
|
+
- Keep a database `UniqueConstraint` over the scope fields and sequence property.
|
|
317
|
+
- Changing `scope`, `initialValue`, `step`, `allowManualValue`, or initializer policy changes the allocation signature and requires explicit review.
|
|
318
|
+
- Removing a declared `ScopedSequence` must be reviewed explicitly; do not treat it as a harmless removed computation.
|
|
319
|
+
|
|
320
|
+
Testing rules:
|
|
321
|
+
|
|
322
|
+
- PGLite/SQLite are useful for local migration tests, but the production gate is real PostgreSQL.
|
|
323
|
+
- Run `npm run test:postgres-scoped-sequence` when scoped sequence allocation or migration behavior changes.
|
|
324
|
+
- For existing-data migration, test that the next allocation returns `max(existingSerial) + step` for every scope.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
278
328
|
## Event and Async Handlers
|
|
279
329
|
|
|
280
330
|
Event-based computations without full compute support require external rebuild handlers.
|
|
@@ -322,6 +372,8 @@ Approved diff cannot bypass core safety gates:
|
|
|
322
372
|
- Fact destructive schema changes remain blocked.
|
|
323
373
|
- Entity/relation output replacement still needs previous manifest ownership proof.
|
|
324
374
|
- Fact-to-computation takeover requires explicit `computation-takeover` approval and, for entity/relation targets, exact destructive-scope approval.
|
|
375
|
+
- ScopedSequence additions/changes require explicit seed/no-seed review and are not ordinary recompute changes.
|
|
376
|
+
- ScopedSequence removals require explicit review before the declaration disappears from the manifest.
|
|
325
377
|
- Async computations require an approved async completion decision and runtime handler.
|
|
326
378
|
- Event-based computations without full compute require an approved event rebuild decision and runtime handler.
|
|
327
379
|
- Destructive computed output requires exact approved ids.
|
|
@@ -412,10 +464,11 @@ This phase does not support converting handwritten properties to `StateMachine`
|
|
|
412
464
|
- [ ] Review logical model changes, storage changes, function hashes/text, required decisions, and safety output.
|
|
413
465
|
- [ ] Add one explicit decision for every required decision.
|
|
414
466
|
- [ ] For fact-to-computation takeover, approve both `computation-takeover` and the matching `computation: changed` decision.
|
|
467
|
+
- [ ] For ScopedSequence changes, approve `computation: unrebuildable` plus matching seed/no-seed decisions, and verify every existing scope is seeded.
|
|
415
468
|
- [ ] For entity/relation takeover, approve the exact destructive ids and rerun dry-run if the database changed.
|
|
416
469
|
- [ ] Provide runtime handlers for event rebuild and async completion decisions.
|
|
417
470
|
- [ ] Run `controller.migrate({ approvedDiff, dryRun: true, handlers })`.
|
|
418
471
|
- [ ] Confirm `blockingChanges` is empty and `rebuildPlan` is expected.
|
|
419
472
|
- [ ] Confirm destructive scopes match exactly when cleanup is intentional.
|
|
420
473
|
- [ ] Run `controller.migrate({ approvedDiff, handlers })`.
|
|
421
|
-
- [ ] Run integration tests for the production driver, especially PostgreSQL/MySQL.
|
|
474
|
+
- [ ] Run integration tests for the production driver, especially PostgreSQL/MySQL. For ScopedSequence, run `npm run test:postgres-scoped-sequence`.
|