feat(infospace): add L3 relation graph with VSM-aware triplets (S2.8)
Implements the L3 relation graph layer — a directed graph of (Subject,
Predicate, Object) triplets annotated with VSM channel codes and feedback
roles. Triplets are authored as markdown files under output/relations/,
parsed into RelationMeta dataclasses, and analysed with networkx.
New modules:
- markitect/infospace/relation_models.py — RelationMeta dataclass +
RELATION_TYPES controlled vocabulary (15 relation classes → VSM codes)
- markitect/infospace/relation_parser.py — parse_relation_file() and
parse_relations_directory()
New schema: examples/infospace-with-history/schemas/relation-schema-v1.0.md
— file naming convention, required sections, controlled vocabulary table
15 seed relation files covering the three core WoN feedback loops:
- Capital Accumulation loop (positive reinforcement, S1/S3)
- Market Price Balancing loop (negative feedback, S2/S3)
- Market Extent mutual dependency (S1/S2)
Plus structural relations: wages regulation, rent residual, price
decomposition, invisible hand coordination
CLI: markitect infospace relations [--entity SLUG] [--vsm FILTER]
[--loops] [--stats]
- Builds directed graph from parsed files
- Detects feedback loops via nx.simple_cycles()
- 6 loops found from 15 seed relations (3 intended + 3 emergent)
- --stats aggregates by VSM system code (strips parentheticals)
Config: InfospaceConfig gains relations_dir (default output/relations)
infospace.yaml: schemas.relation references relation-schema-v1.0.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ schemas:
|
||||
entity: schemas/economic-entity-schema-v1.0.md
|
||||
mapping: schemas/vsm-mapping-schema-v1.0.md
|
||||
analysis: schemas/chapter-analysis-schema-v1.0.md
|
||||
relation: schemas/relation-schema-v1.0.md
|
||||
|
||||
competency_questions: |
|
||||
1. How does Smith's division of labour map to VSM System 1 operations?
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Accumulation of Stock — enables — Division of Labour
|
||||
|
||||
## Subject
|
||||
|
||||
Accumulation of Stock
|
||||
|
||||
## Predicate
|
||||
|
||||
enables
|
||||
|
||||
## Object
|
||||
|
||||
Division of Labour
|
||||
|
||||
## Relation Type
|
||||
|
||||
enables
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S3 → S1 (capital stock managed at S3 deploys productive capacity at S1)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 6: "As soon as stock has accumulated in the hands of
|
||||
particular persons, some of them will naturally employ it in setting to work
|
||||
industrious people, whom they will supply with materials and subsistence."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Capital Accumulation loop (positive/reinforcing):**
|
||||
Accumulation of Stock → enables → Division of Labour → increases →
|
||||
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||
Accumulation of Stock.
|
||||
|
||||
This edge (enables) is the initiating link: accumulated capital is the
|
||||
prerequisite for organising specialised production. Without prior accumulation
|
||||
there is no employer to assign and coordinate different tasks.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Accumulation of Stock — increases supply that depresses — Market Price of Commodities
|
||||
|
||||
## Subject
|
||||
|
||||
Accumulation of Stock
|
||||
|
||||
## Predicate
|
||||
|
||||
increases supply, depressing
|
||||
|
||||
## Object
|
||||
|
||||
Market Price of Commodities
|
||||
|
||||
## Relation Type
|
||||
|
||||
regulates
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S3 → S2 (capital deployment increases supply, correcting price signal)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 7: "When the quantity brought to market exceeds the
|
||||
effectual demand, it cannot be all sold to those who are willing to pay
|
||||
the whole value of the rent, wages and profit, which must be paid in
|
||||
order to bring it thither. Some part must be sold to those who are
|
||||
willing to pay less, and the low price which they give for it must
|
||||
reduce the price of the whole."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Market Price Balancing loop (negative/balancing):**
|
||||
Market Price above Natural Price → attracts → Capital inflow →
|
||||
increases supply → depresses → Market Price back toward Natural Price.
|
||||
|
||||
This edge (increases supply, depressing) closes the balancing loop:
|
||||
directed capital raises output until oversupply brings price back down.
|
||||
This is the mechanism behind Smith's claim that natural price acts as
|
||||
the "central price, to which the prices of all commodities are
|
||||
continually gravitating."
|
||||
@@ -0,0 +1,40 @@
|
||||
# Division of Labour — constrains — Market Extent
|
||||
|
||||
## Subject
|
||||
|
||||
Division of Labour
|
||||
|
||||
## Predicate
|
||||
|
||||
limited by
|
||||
|
||||
## Object
|
||||
|
||||
Market Extent
|
||||
|
||||
## Relation Type
|
||||
|
||||
constrains
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 ← S1 (the coordination reach of markets bounds the scope of operational specialisation)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 3: "As it is the power of exchanging that gives occasion
|
||||
to the division of labour, so the extent of this division must always be
|
||||
limited by the extent of that power, or, in other words, by the extent
|
||||
of the market."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Market Expansion loop (positive/reinforcing):**
|
||||
Market Extent → enables greater Division of Labour → raises Productive
|
||||
Powers of Labour → generates surplus → expands Trade → expands
|
||||
Market Extent.
|
||||
|
||||
This edge (limited by) is the structural constraint that gives the loop
|
||||
its character: the level of specialisation that can be sustained depends
|
||||
on the size of the market that can absorb the specialised output.
|
||||
Smith's famous opening of Chapter III states this with unusual directness.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Division of Labour — increases — Productive Powers of Labour
|
||||
|
||||
## Subject
|
||||
|
||||
Division of Labour
|
||||
|
||||
## Predicate
|
||||
|
||||
increases
|
||||
|
||||
## Object
|
||||
|
||||
Productive Powers of Labour
|
||||
|
||||
## Relation Type
|
||||
|
||||
enables
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S1 → S1 (operational specialisation raises operational output per unit)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 1: "The greatest improvement in the productive powers of
|
||||
labour, and the greater part of the skill, dexterity, and judgment with
|
||||
which it is anywhere directed, or applied, seem to have been the effects
|
||||
of the division of labour."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Capital Accumulation loop (positive/reinforcing):**
|
||||
Accumulation of Stock → enables → Division of Labour → increases →
|
||||
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||
Accumulation of Stock.
|
||||
|
||||
This edge (increases) is the productivity mechanism: specialisation
|
||||
improves dexterity, saves time between tasks, and facilitates the
|
||||
invention of machinery — three distinct channels that all raise output
|
||||
per worker.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Invisible Hand Mechanism — coordinates — Accumulation of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Invisible Hand Mechanism
|
||||
|
||||
## Predicate
|
||||
|
||||
coordinates (directs capital allocation without central direction)
|
||||
|
||||
## Object
|
||||
|
||||
Accumulation of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
coordinates
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S4 → S3 (distributed environmental intelligence shapes capital allocation decisions)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book IV, Chapter 2: "He generally, indeed, neither intends to promote the
|
||||
public interest, nor knows how much he is promoting it... he intends only
|
||||
his own security; and by directing that industry in a manner whose produce
|
||||
may be of the greatest value, he intends only his own gain, and he is in
|
||||
this, as in many other cases, led by an invisible hand to promote an end
|
||||
which was no part of his intention."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
The Invisible Hand Mechanism is the S4 intelligence function that makes
|
||||
the Market Price Balancing loop self-organising. Individual capitalists
|
||||
seeking private profit collectively direct accumulation to where returns
|
||||
are highest, which (through the market price balancing loop) also serves
|
||||
as a resource allocation mechanism. This relation explains why the system
|
||||
does not require a planner — the S4 function is distributed across all
|
||||
capital owners simultaneously.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Market Extent — enables — Division of Labour
|
||||
|
||||
## Subject
|
||||
|
||||
Market Extent
|
||||
|
||||
## Predicate
|
||||
|
||||
enables (by providing demand for specialised output)
|
||||
|
||||
## Object
|
||||
|
||||
Division of Labour
|
||||
|
||||
## Relation Type
|
||||
|
||||
enables
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 → S1 (coordination reach of markets enables operational specialisation)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 3: "When the market is very small, no person can have
|
||||
any encouragement to dedicate himself entirely to one employment, for
|
||||
want of the power to exchange all that surplus part of the produce of his
|
||||
own labour, which is over and above his own consumption, for such parts
|
||||
of the produce of other men's labour as he has occasion for."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Market Expansion loop (positive/reinforcing):**
|
||||
Market Extent → enables → Division of Labour → raises Productive Powers
|
||||
of Labour → generates surplus → expands Trade → expands Market Extent.
|
||||
|
||||
Together with the constrains edge (Division of Labour ← Market Extent),
|
||||
this pair forms the mutual dependency at the heart of Book I: markets
|
||||
enable specialisation, and specialisation generates the surplus that
|
||||
expands markets.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Market Price of Commodities — attracts — Accumulation of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Market Price of Commodities
|
||||
|
||||
## Predicate
|
||||
|
||||
attracts (when above natural price)
|
||||
|
||||
## Object
|
||||
|
||||
Accumulation of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
coordinates
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 → S3 (price signal coordinates capital reallocation)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 7: "When the quantity brought to market falls short of the
|
||||
effectual demand, all those who are willing to pay the whole natural price
|
||||
cannot be supplied. Some of them will be willing to give more. A competition
|
||||
will immediately begin among them, and the market price will rise more or
|
||||
less above the natural price."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Market Price Balancing loop (negative/balancing):**
|
||||
Market Price above Natural Price → attracts → Capital inflow (Accumulation
|
||||
directed to this market) → increases Supply → depresses → Market Price
|
||||
back toward Natural Price.
|
||||
|
||||
This edge (attracts) is the signal-response link: a market price above
|
||||
natural price is a profit opportunity that draws capital from other uses.
|
||||
Smith's key insight is that this signal is self-cancelling — it eliminates
|
||||
the very excess that created it.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Market Price of Commodities — decomposes into — Profits of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Market Price of Commodities
|
||||
|
||||
## Predicate
|
||||
|
||||
decomposes into (profit as component)
|
||||
|
||||
## Object
|
||||
|
||||
Profits of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
produces
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 → S3 (price signal resolves into return on capital, which S3 manages)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 6: "In every society the price of every commodity
|
||||
finally resolves itself into some one or other, or all of those three
|
||||
parts; and in every improved society, all three enter more or less,
|
||||
as component parts, into the price of the far greater part of commodities."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
Part of the **Three-Component Decomposition** (see also wages and rent
|
||||
relations). This edge links the market price coordination signal (S2)
|
||||
to the capital management outcome (S3), establishing profit as the
|
||||
mechanism by which market performance translates into the fund for
|
||||
future accumulation. It is the bridge between the Market Price Balancing
|
||||
loop and the Capital Accumulation loop.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Market Price of Commodities — decomposes into — Wages of Labour
|
||||
|
||||
## Subject
|
||||
|
||||
Market Price of Commodities
|
||||
|
||||
## Predicate
|
||||
|
||||
decomposes into (wages as component)
|
||||
|
||||
## Object
|
||||
|
||||
Wages of Labour
|
||||
|
||||
## Relation Type
|
||||
|
||||
produces
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 → S2 (price as coordination signal resolves into its component coordination signals)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 6: "In every society the price of every commodity
|
||||
finally resolves itself into some one or other, or all of those three
|
||||
parts [wages, profit, rent]; and in every improved society, all three
|
||||
enter more or less, as component parts, into the price of the far greater
|
||||
part of commodities."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
This is one of the three price-decomposition relations (alongside market
|
||||
price → profits of stock, and market price → rent of land). Together
|
||||
they form the **Three-Component Decomposition**: the price system
|
||||
connects the coordination layer (S2) back to the distributional outcomes
|
||||
for all three factor markets. The decomposition is the analytical
|
||||
foundation from which Books I and II derive their structure.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Natural Price as Central Price — centres — Market Price of Commodities
|
||||
|
||||
## Subject
|
||||
|
||||
Natural Price as Central Price
|
||||
|
||||
## Predicate
|
||||
|
||||
centres
|
||||
|
||||
## Object
|
||||
|
||||
Market Price of Commodities
|
||||
|
||||
## Relation Type
|
||||
|
||||
coordinates
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 (natural price is the attractor in the S2 coordination field)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 7: "The natural price, therefore, is, as it were, the
|
||||
central price, to which the prices of all commodities are continually
|
||||
gravitating. Different accidents may sometimes keep them suspended a good
|
||||
deal above it, and sometimes force them down even somewhat below it."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
This relation is the attractor definition for the Market Price Balancing
|
||||
loop — natural price is the equilibrium that both loops (attraction of
|
||||
capital when above, flight of capital when below) converge on. It is the
|
||||
S2 coordination reference point around which oscillation is suppressed.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Productive Powers of Labour — raises — Profits of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Productive Powers of Labour
|
||||
|
||||
## Predicate
|
||||
|
||||
raises
|
||||
|
||||
## Object
|
||||
|
||||
Profits of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
produces
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S1 → S3 (operational productivity surplus allocated as profit at capital management level)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 6: "The value which the workmen add to the materials
|
||||
resolves itself into two parts, of which the one pays their wages, the
|
||||
other the profits of their employer upon the whole stock of materials
|
||||
and wages which he advanced."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Capital Accumulation loop (positive/reinforcing):**
|
||||
Accumulation of Stock → enables → Division of Labour → increases →
|
||||
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||
Accumulation of Stock.
|
||||
|
||||
This edge (raises) is the surplus-extraction link: the productivity gain
|
||||
produced by specialisation shows up as profit over and above wage costs,
|
||||
generating the fund from which further accumulation occurs.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Profits of Stock — funds — Accumulation of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Profits of Stock
|
||||
|
||||
## Predicate
|
||||
|
||||
funds
|
||||
|
||||
## Object
|
||||
|
||||
Accumulation of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
produces
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S3 → S3 (profit retained by capital managers re-enters the stock managed at S3)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book II, Chapter 3: "Whatever a person saves from his revenue he adds to
|
||||
his capital, and either employs it himself in maintaining an additional
|
||||
number of productive hands, or enables some other person to do so, by
|
||||
lending it to him for an interest."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Capital Accumulation loop (positive/reinforcing):**
|
||||
Accumulation of Stock → enables → Division of Labour → increases →
|
||||
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||
Accumulation of Stock.
|
||||
|
||||
This edge (funds) closes the loop: profit not consumed becomes saving,
|
||||
and saving becomes the new stock that enables the next round of division
|
||||
of labour. This is why Smith regards frugality as a public virtue — it
|
||||
fuels productive capacity rather than dissipating it.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Rent of Land — regulated by — Market Price of Commodities
|
||||
|
||||
## Subject
|
||||
|
||||
Rent of Land
|
||||
|
||||
## Predicate
|
||||
|
||||
regulated by (rent is a residual after wages and profit are paid)
|
||||
|
||||
## Object
|
||||
|
||||
Market Price of Commodities
|
||||
|
||||
## Relation Type
|
||||
|
||||
is regulated by
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S2 → S3 (price signal determines what residual rent the land can command)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 11: "Rent, it is to be observed, therefore, enters into
|
||||
the composition of the price of commodities in a different manner from
|
||||
wages and profit. High or low wages and profit are the causes of high or
|
||||
low price; high or low rent is the effect of it."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
This relation establishes rent as a **residual** in the price decomposition
|
||||
rather than a cost that determines price. It places Rent of Land in a
|
||||
structurally subordinate position to the Market Price mechanism — rent
|
||||
takes what is left after labour and capital have been paid, rather than
|
||||
competing with them as a co-equal cost component. This is Smith's
|
||||
sharpest break from the physiocrats.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Wages of Labour — regulated by — Accumulation of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Wages of Labour
|
||||
|
||||
## Predicate
|
||||
|
||||
regulated by (set by the demand for labour, which tracks capital growth)
|
||||
|
||||
## Object
|
||||
|
||||
Accumulation of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
is regulated by
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S3 → S2 (capital stock sets demand for labour, which sets the wage coordination signal)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 8: "The demand for those who live by wages, therefore,
|
||||
necessarily increases with the increase of the revenue and stock of every
|
||||
country, and cannot possibly increase without it. The increase of revenue
|
||||
and stock is the increase of national wealth."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
**Wages Expansion loop (positive/reinforcing):**
|
||||
Accumulation of Stock → raises demand for labour → raises Wages of Labour
|
||||
→ raises consumer capacity → stimulates Productive Powers of Labour →
|
||||
generates surplus → funds Accumulation of Stock.
|
||||
|
||||
This edge (regulated by) establishes the first link: the wage rate
|
||||
responds to the growth of capital stock, not to the current stock level
|
||||
directly. Rapid accumulation raises wages; stationary stock does not.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Wages of Labour — regulated by — Profits of Stock
|
||||
|
||||
## Subject
|
||||
|
||||
Wages of Labour
|
||||
|
||||
## Predicate
|
||||
|
||||
regulated by (bounded above by)
|
||||
|
||||
## Object
|
||||
|
||||
Profits of Stock
|
||||
|
||||
## Relation Type
|
||||
|
||||
is regulated by
|
||||
|
||||
## VSM Channel
|
||||
|
||||
S3 → S2 (capital management sets the upper bound on the coordination signal)
|
||||
|
||||
## Evidence
|
||||
|
||||
Book I, Chapter 8: "In order to bring up a family, the labour of the
|
||||
husband and wife together must, even in the lowest species of common
|
||||
labour, be able to earn something more than what is precisely necessary
|
||||
for their own maintenance; but in what is commonly called common labour
|
||||
it is rare that a single labourer earns much more... The interests of
|
||||
the two parties are by no means the same. The workmen desire to get as
|
||||
much, the masters to give as little as possible."
|
||||
|
||||
## Feedback Role
|
||||
|
||||
This relation establishes the distributional constraint that bounds
|
||||
the Wages loop. Profits of Stock and Wages of Labour are in permanent
|
||||
tension: every increase in one (holding productivity constant) reduces
|
||||
the other. This is Smith's clearest statement of class interest as a
|
||||
structural feature of market economies.
|
||||
139
examples/infospace-with-history/schemas/relation-schema-v1.0.md
Normal file
139
examples/infospace-with-history/schemas/relation-schema-v1.0.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Relation Triplet Schema — v1.0
|
||||
|
||||
Each file in `output/relations/` captures a single directed relation between
|
||||
two entities from the L1 entity collection. Relations are the edges of the
|
||||
L3 relation graph.
|
||||
|
||||
---
|
||||
|
||||
## Required Sections
|
||||
|
||||
### H1 — Relation title
|
||||
|
||||
Format: `# Subject — predicate phrase — Object`
|
||||
|
||||
Example: `# Division of Labour — limited by — Market Extent`
|
||||
|
||||
---
|
||||
|
||||
### Subject
|
||||
|
||||
The entity that is the **source** of the relation — the thing doing the
|
||||
enabling, constraining, producing, etc.
|
||||
|
||||
Use the entity's exact title as it appears in its L1 entity file (H1 heading).
|
||||
|
||||
---
|
||||
|
||||
### Predicate
|
||||
|
||||
A short phrase from the **controlled relation vocabulary** (see below), or
|
||||
a natural language variant that maps to one of those classes.
|
||||
|
||||
Examples: `limited by`, `enables`, `is regulated by`, `produces`
|
||||
|
||||
---
|
||||
|
||||
### Object
|
||||
|
||||
The entity that is the **target** of the relation — the thing being enabled,
|
||||
constrained, regulated, etc.
|
||||
|
||||
Use the entity's exact title as it appears in its L1 entity file.
|
||||
|
||||
---
|
||||
|
||||
### Relation Type
|
||||
|
||||
The **semantic class** of the predicate, drawn from the controlled vocabulary:
|
||||
|
||||
| Relation Type | Meaning | Default VSM Channel |
|
||||
|---|---|---|
|
||||
| `enables` | Subject makes object possible or more effective | S1 → S1 |
|
||||
| `constrains` | Subject limits or bounds the object | S1 ← S1 |
|
||||
| `regulates` | Subject actively governs the object | S3 → S1 |
|
||||
| `is regulated by` | Object governs the subject | S1 ← S3 |
|
||||
| `coordinates` | Subject reduces oscillation or misalignment | S2 |
|
||||
| `produces` | Subject generates the object as output | S1 |
|
||||
| `consumes` | Subject depletes the object | S1 |
|
||||
| `monitors` | Subject observes and reports on object | S3* |
|
||||
| `audits` | Subject verifies compliance of object | S3* |
|
||||
| `adapts to` | Subject adjusts in response to object | S4 |
|
||||
| `anticipates` | Subject predicts or models the object | S4 |
|
||||
| `defines` | Subject sets the identity or rules of object | S5 → any |
|
||||
| `is defined by` | Object sets the identity or rules of subject | any ← S5 |
|
||||
| `contradicts` | Subject is in direct logical conflict with object | any |
|
||||
| `tensions with` | Subject and object exist in productive tension | any |
|
||||
|
||||
---
|
||||
|
||||
### VSM Channel
|
||||
|
||||
The Viable System Model systems involved in this relation, with direction.
|
||||
|
||||
Format: `S<n> → S<m>` or `S<n>` if the relation is within a single system.
|
||||
|
||||
Examples:
|
||||
- `S1 → S2` — operation drives a coordination signal
|
||||
- `S3 → S1` — management regulates operations
|
||||
- `S2` — pure coordination within S2
|
||||
- `S4 → S5` — intelligence informing policy
|
||||
|
||||
Use `S3*` for audit loops (Beer's algedonic channel).
|
||||
|
||||
---
|
||||
|
||||
### Evidence
|
||||
|
||||
Direct textual evidence from Adam Smith's text. Preferred format:
|
||||
|
||||
```
|
||||
Book I, Chapter 3: "The division of labour is limited by the extent of the market."
|
||||
```
|
||||
|
||||
Use short quotes when available. Chapter reference alone is acceptable when
|
||||
no single sentence captures the relation.
|
||||
|
||||
---
|
||||
|
||||
### Feedback Role (optional)
|
||||
|
||||
If this relation is part of a named feedback loop, describe its role here.
|
||||
Name the loop and state what the relation contributes.
|
||||
|
||||
Example:
|
||||
```
|
||||
Part of the Market Expansion loop: larger market → more specialisation
|
||||
→ higher productivity → greater surplus → expanded trade → larger market.
|
||||
This edge (limited by) is the constraining link that turns the loop balancing
|
||||
when market extent cannot grow further.
|
||||
```
|
||||
|
||||
Omit or leave empty if the relation is not part of a known feedback loop.
|
||||
|
||||
---
|
||||
|
||||
## File Naming
|
||||
|
||||
Use the pattern: `<subject-slug>--<relation-type>--<object-slug>.md`
|
||||
|
||||
Where slugs are derived from entity titles using the same slugification as
|
||||
entity files (lowercase, spaces and punctuation replaced with underscores).
|
||||
|
||||
Example: `division_of_labour--constrains--market_extent.md`
|
||||
|
||||
If two entities have the same relation type between them (unusual), append
|
||||
a numeric suffix: `...-2.md`.
|
||||
|
||||
---
|
||||
|
||||
## Completeness Criteria
|
||||
|
||||
A relation file is **complete** if:
|
||||
- All four required sections are present and non-empty
|
||||
- The Relation Type is from the controlled vocabulary
|
||||
- The VSM Channel is a valid VSM system designation
|
||||
- Evidence references a specific Book and Chapter
|
||||
|
||||
A relation file is **acceptable** if:
|
||||
- Required sections present but Evidence is missing (mark as `(unverified)`)
|
||||
@@ -299,6 +299,126 @@ def eval_summary(config_path: Optional[str], update_metrics: bool):
|
||||
click.echo(f"\nUpdated metrics.yaml: per_entity_mean = {mean_overall:.4f}")
|
||||
|
||||
|
||||
# ── relations ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@infospace_commands.command()
|
||||
@click.option("--config", "config_path", default=None, help="Path to infospace.yaml.")
|
||||
@click.option("--entity", "entity_slug", default=None,
|
||||
help="Show only relations involving this entity slug.")
|
||||
@click.option("--vsm", "vsm_filter", default=None,
|
||||
help="Show only relations whose VSM channel contains this string (e.g. S2, S3).")
|
||||
@click.option("--loops", "loops_only", is_flag=True, default=False,
|
||||
help="Show only feedback loops (cycles in the relation graph).")
|
||||
@click.option("--stats", "stats_only", is_flag=True, default=False,
|
||||
help="Show aggregate statistics only, no individual relations.")
|
||||
def relations(config_path: Optional[str], entity_slug: Optional[str],
|
||||
vsm_filter: Optional[str], loops_only: bool, stats_only: bool):
|
||||
"""Show the L3 relation graph — triplets, feedback loops, and VSM channels."""
|
||||
cfg, cfg_path = _load_config_or_exit(config_path)
|
||||
root = cfg_path.parent
|
||||
|
||||
from markitect.infospace.relation_parser import parse_relations_directory
|
||||
|
||||
relations_dir = root / cfg.relations_dir
|
||||
if not relations_dir.is_dir():
|
||||
click.echo("No relations directory found. Create output/relations/ and add relation files.")
|
||||
return
|
||||
|
||||
all_relations = parse_relations_directory(relations_dir)
|
||||
if not all_relations:
|
||||
click.echo("No relation files found in " + str(relations_dir))
|
||||
return
|
||||
|
||||
# Build directed graph for cycle detection
|
||||
try:
|
||||
import networkx as nx
|
||||
G = nx.DiGraph()
|
||||
for r in all_relations:
|
||||
G.add_edge(r.subject_slug, r.object_slug,
|
||||
predicate=r.predicate,
|
||||
relation_type=r.relation_type,
|
||||
vsm_channel=r.vsm_channel,
|
||||
slug=r.slug)
|
||||
except ImportError:
|
||||
G = None
|
||||
|
||||
# Find feedback loops
|
||||
loops = []
|
||||
if G is not None:
|
||||
try:
|
||||
loops = list(nx.simple_cycles(G))
|
||||
except Exception:
|
||||
loops = []
|
||||
|
||||
# Stats summary
|
||||
import re as _re
|
||||
|
||||
def _vsm_code(channel: str) -> str:
|
||||
"""Strip parenthetical description, returning just the system code (e.g. 'S3 → S1')."""
|
||||
return _re.sub(r'\s*\(.*', '', channel).strip() or channel
|
||||
|
||||
n = len(all_relations)
|
||||
vsm_counts: dict = {}
|
||||
type_counts: dict = {}
|
||||
for r in all_relations:
|
||||
vsm_counts[_vsm_code(r.vsm_channel)] = vsm_counts.get(_vsm_code(r.vsm_channel), 0) + 1
|
||||
type_counts[r.relation_type] = type_counts.get(r.relation_type, 0) + 1
|
||||
|
||||
click.echo(f"Relation graph — {n} relations")
|
||||
if G is not None:
|
||||
click.echo(f" Entities in graph: {G.number_of_nodes()}")
|
||||
click.echo(f" Feedback loops: {len(loops)}")
|
||||
click.echo()
|
||||
|
||||
if stats_only:
|
||||
click.echo("Relation types:")
|
||||
for rt, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||
click.echo(f" {rt:<25} {count:>4}")
|
||||
click.echo()
|
||||
click.echo("VSM channels:")
|
||||
for ch, count in sorted(vsm_counts.items(), key=lambda x: -x[1]):
|
||||
click.echo(f" {ch:<20} {count:>4}")
|
||||
return
|
||||
|
||||
# Feedback loops section
|
||||
if loops or loops_only:
|
||||
if loops:
|
||||
click.echo(f"Feedback loops ({len(loops)}):")
|
||||
for i, cycle in enumerate(loops, 1):
|
||||
click.echo(f" Loop {i}: {' → '.join(cycle)} → {cycle[0]}")
|
||||
click.echo()
|
||||
elif loops_only:
|
||||
click.echo("No feedback loops detected in current relation set.")
|
||||
return
|
||||
|
||||
if loops_only:
|
||||
return
|
||||
|
||||
# Filter relations
|
||||
filtered = all_relations
|
||||
if entity_slug:
|
||||
filtered = [r for r in filtered
|
||||
if entity_slug in (r.subject_slug, r.object_slug)]
|
||||
if not filtered:
|
||||
click.echo(f"No relations found involving '{entity_slug}'.")
|
||||
return
|
||||
if vsm_filter:
|
||||
filtered = [r for r in filtered if vsm_filter in r.vsm_channel]
|
||||
if not filtered:
|
||||
click.echo(f"No relations with VSM channel containing '{vsm_filter}'.")
|
||||
return
|
||||
|
||||
# Display relations
|
||||
click.echo(f"{'Subject':<35} {'Predicate':<30} {'Object':<35} {'VSM'}")
|
||||
click.echo("-" * 110)
|
||||
for r in filtered:
|
||||
subj = r.subject[:33] + ".." if len(r.subject) > 35 else r.subject
|
||||
obj = r.object[:33] + ".." if len(r.object) > 35 else r.object
|
||||
pred = r.predicate[:28] + ".." if len(r.predicate) > 30 else r.predicate
|
||||
click.echo(f"{subj:<35} {pred:<30} {obj:<35} {r.vsm_channel}")
|
||||
|
||||
|
||||
# ── viability ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ class InfospaceConfig:
|
||||
entities_dir: str = "output/entities"
|
||||
evaluations_dir: str = "output/evaluations"
|
||||
metrics_dir: str = "output/metrics"
|
||||
relations_dir: str = "output/relations"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {"topic": self.topic.to_dict()}
|
||||
@@ -276,6 +277,8 @@ class InfospaceConfig:
|
||||
d["evaluations_dir"] = self.evaluations_dir
|
||||
if self.metrics_dir != "output/metrics":
|
||||
d["metrics_dir"] = self.metrics_dir
|
||||
if self.relations_dir != "output/relations":
|
||||
d["relations_dir"] = self.relations_dir
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@@ -301,6 +304,7 @@ class InfospaceConfig:
|
||||
entities_dir=data.get("entities_dir", "output/entities"),
|
||||
evaluations_dir=data.get("evaluations_dir", "output/evaluations"),
|
||||
metrics_dir=data.get("metrics_dir", "output/metrics"),
|
||||
relations_dir=data.get("relations_dir", "output/relations"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
72
markitect/infospace/relation_models.py
Normal file
72
markitect/infospace/relation_models.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Data models for L3 relation triplets.
|
||||
|
||||
A relation triplet is the fundamental unit of the relation graph:
|
||||
Subject --[Predicate]--> Object
|
||||
|
||||
Each triplet is stored as a markdown file in ``output/relations/``.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
# Controlled relation vocabulary — maps semantic class to VSM channel
|
||||
RELATION_TYPES = {
|
||||
"enables": "S1 → S1",
|
||||
"constrains": "S1 ← S1",
|
||||
"regulates": "S3 → S1",
|
||||
"is regulated by": "S1 ← S3",
|
||||
"coordinates": "S2",
|
||||
"produces": "S1",
|
||||
"consumes": "S1",
|
||||
"monitors": "S3*",
|
||||
"audits": "S3*",
|
||||
"adapts to": "S4",
|
||||
"anticipates": "S4",
|
||||
"defines": "S5 → any",
|
||||
"is defined by": "any ← S5",
|
||||
"contradicts": "any",
|
||||
"tensions with": "any",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RelationMeta:
|
||||
"""Structured metadata for a single relation triplet.
|
||||
|
||||
Attributes:
|
||||
slug: Unique identifier, e.g.
|
||||
``division_of_labour--constrains--market_extent``
|
||||
subject: Human-readable title of the subject entity
|
||||
subject_slug: Slug of the subject entity (links to L1)
|
||||
predicate: Human-readable predicate phrase, e.g. "limited by"
|
||||
object: Human-readable title of the object entity
|
||||
object_slug: Slug of the object entity (links to L1)
|
||||
relation_type: Semantic class from the controlled vocabulary
|
||||
vsm_channel: VSM systems involved, e.g. "S1 → S2"
|
||||
evidence: Source text quote or chapter reference
|
||||
feedback_role: Description of role in a feedback loop (if any)
|
||||
source_path: Absolute path to the ``.md`` file
|
||||
"""
|
||||
|
||||
slug: str
|
||||
subject: str
|
||||
subject_slug: str
|
||||
predicate: str
|
||||
object: str
|
||||
object_slug: str
|
||||
relation_type: str
|
||||
vsm_channel: str
|
||||
evidence: str = ""
|
||||
feedback_role: str = ""
|
||||
source_path: str = ""
|
||||
|
||||
@property
|
||||
def is_feedback_member(self) -> bool:
|
||||
"""True if this relation participates in a named feedback loop."""
|
||||
return bool(self.feedback_role.strip())
|
||||
|
||||
def edge(self) -> tuple:
|
||||
"""Return a (subject_slug, object_slug, predicate) edge tuple."""
|
||||
return (self.subject_slug, self.object_slug, self.predicate)
|
||||
137
markitect/infospace/relation_parser.py
Normal file
137
markitect/infospace/relation_parser.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Relation triplet parser.
|
||||
|
||||
Reads structured :class:`RelationMeta` objects from relation markdown
|
||||
files in ``output/relations/``.
|
||||
|
||||
File format::
|
||||
|
||||
# Subject — predicate — Object
|
||||
|
||||
## Subject
|
||||
Subject Entity Title
|
||||
|
||||
## Predicate
|
||||
predicate phrase
|
||||
|
||||
## Object
|
||||
Object Entity Title
|
||||
|
||||
## Relation Type
|
||||
constrains
|
||||
|
||||
## VSM Channel
|
||||
S1 → S2
|
||||
|
||||
## Evidence
|
||||
Book I, Chapter 3: "..."
|
||||
|
||||
## Feedback Role
|
||||
Part of the Market Expansion loop: ...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from markitect.core.parser import parse_markdown_to_ast
|
||||
from markitect.core.section_tree import (
|
||||
build_section_tree,
|
||||
extract_section_text,
|
||||
slugify,
|
||||
)
|
||||
from .relation_models import RelationMeta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_h2(tree_root: dict, slug: str) -> Optional[dict]:
|
||||
for child in tree_root.get("children", []):
|
||||
if child["level"] == 2 and child["slug"] == slug:
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def _section_text(root: dict, slug: str) -> str:
|
||||
node = _find_h2(root, slug)
|
||||
return extract_section_text(node).strip() if node else ""
|
||||
|
||||
|
||||
def _slug_from_title(title: str) -> str:
|
||||
"""Convert entity title to slug (same as slugify used in entity_parser)."""
|
||||
return slugify(title)
|
||||
|
||||
|
||||
def parse_relation_file(path: Path) -> RelationMeta:
|
||||
"""Parse a single relation markdown file into :class:`RelationMeta`.
|
||||
|
||||
Raises:
|
||||
ValueError: If required sections are missing.
|
||||
"""
|
||||
content = path.read_text(encoding="utf-8")
|
||||
tokens = parse_markdown_to_ast(content)
|
||||
tree = build_section_tree(tokens)
|
||||
|
||||
# Find H1
|
||||
h1 = next(
|
||||
(c for c in tree["children"] if c["level"] == 1),
|
||||
None,
|
||||
)
|
||||
if h1 is None:
|
||||
raise ValueError(f"No H1 heading in {path}")
|
||||
|
||||
root = h1
|
||||
|
||||
subject = _section_text(root, "subject")
|
||||
predicate = _section_text(root, "predicate")
|
||||
obj = _section_text(root, "object")
|
||||
relation_type = _section_text(root, "relation_type")
|
||||
vsm_channel = _section_text(root, "vsm_channel")
|
||||
evidence = _section_text(root, "evidence")
|
||||
feedback_role = _section_text(root, "feedback_role")
|
||||
|
||||
if not subject:
|
||||
raise ValueError(f"Missing ## Subject in {path}")
|
||||
if not predicate:
|
||||
raise ValueError(f"Missing ## Predicate in {path}")
|
||||
if not obj:
|
||||
raise ValueError(f"Missing ## Object in {path}")
|
||||
|
||||
subject_slug = _slug_from_title(subject)
|
||||
object_slug = _slug_from_title(obj)
|
||||
|
||||
# Derive canonical slug from file stem
|
||||
slug = path.stem
|
||||
|
||||
return RelationMeta(
|
||||
slug=slug,
|
||||
subject=subject,
|
||||
subject_slug=subject_slug,
|
||||
predicate=predicate,
|
||||
object=obj,
|
||||
object_slug=object_slug,
|
||||
relation_type=relation_type,
|
||||
vsm_channel=vsm_channel,
|
||||
evidence=evidence,
|
||||
feedback_role=feedback_role,
|
||||
source_path=str(path),
|
||||
)
|
||||
|
||||
|
||||
def parse_relations_directory(
|
||||
directory: Path,
|
||||
) -> List[RelationMeta]:
|
||||
"""Parse all relation files in *directory*.
|
||||
|
||||
Malformed files are skipped with a warning.
|
||||
"""
|
||||
relations: List[RelationMeta] = []
|
||||
for md_file in sorted(directory.glob("*.md")):
|
||||
try:
|
||||
relations.append(parse_relation_file(md_file))
|
||||
except Exception as exc:
|
||||
logger.warning("Skipping relation file %s: %s", md_file.name, exc)
|
||||
return relations
|
||||
Reference in New Issue
Block a user